From 279503e3f31a38e5a0e11820da29214ca40f8c7f Mon Sep 17 00:00:00 2001 From: danimtb Date: Fri, 18 Jul 2025 21:21:46 +0200 Subject: [PATCH 1/5] pkg-sign commands --- conan/api/subapi/cache.py | 13 +++ conan/cli/commands/cache.py | 58 +++++++++++++ conan/internal/rest/pkg_sign.py | 83 +++++++++++++++---- .../command/cache/test_cache_sign.py | 47 +++++++++++ 4 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 test/integration/command/cache/test_cache_sign.py diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index d65bbcd188b..92f3e5eae6e 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -16,6 +16,7 @@ from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference +from conan.internal.rest.pkg_sign import PkgSignaturesPlugin from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import rmdir, mkdir, remove, save @@ -75,6 +76,18 @@ def check_integrity(self, package_list): checker = IntegrityChecker(cache) checker.check(package_list) + def sign(self, package_list): + """Sign packages with the signing plugin""" + cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) + pkg_signer = PkgSignaturesPlugin(cache, self.conan_api.home_folder) + pkg_signer.sign(package_list, action="cache") + + def verify(self, package_list): + """Verify packages with the signing plugin""" + cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) + pkg_signer = PkgSignaturesPlugin(cache, self.conan_api.home_folder) + pkg_signer.verify_pkglist(package_list) + def clean(self, package_list, source=True, build=True, download=True, temp=True, backup_sources=False): """ diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py index 975ff02b3c3..e0693103de3 100644 --- a/conan/cli/commands/cache.py +++ b/conan/cli/commands/cache.py @@ -150,6 +150,64 @@ def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args): ConanOutput().success("Integrity check: ok") +@conan_subcommand() +def cache_sign(conan_api: ConanAPI, parser, subparser, *args): + """ + Sign packages + """ + subparser.add_argument("pattern", nargs="?", + help="Selection pattern for references to check integrity for") + subparser.add_argument("-l", "--list", action=OnceArgument, + help="Package list of packages to check integrity for") + subparser.add_argument('-p', '--package-query', action=OnceArgument, + help="Only the packages matching a specific query, e.g., " + "os=Windows AND (arch=x86 OR compiler=gcc)") + args = parser.parse_args(*args) + + if args.pattern is None and args.list is None: + raise ConanException("Missing pattern or package list file") + if args.pattern and args.list: + raise ConanException("Cannot specify both pattern and list") + + if args.list: + listfile = make_abs_path(args.list) + multi_package_list = MultiPackagesList.load(listfile) + package_list = multi_package_list["Local Cache"] + else: + ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*") + package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) + conan_api.cache.sign(package_list) + + +@conan_subcommand() +def cache_verify(conan_api: ConanAPI, parser, subparser, *args): + """ + Check siugnature + """ + subparser.add_argument("pattern", nargs="?", + help="Selection pattern for references to check integrity for") + subparser.add_argument("-l", "--list", action=OnceArgument, + help="Package list of packages to check integrity for") + subparser.add_argument('-p', '--package-query', action=OnceArgument, + help="Only the packages matching a specific query, e.g., " + "os=Windows AND (arch=x86 OR compiler=gcc)") + args = parser.parse_args(*args) + + if args.pattern is None and args.list is None: + raise ConanException("Missing pattern or package list file") + if args.pattern and args.list: + raise ConanException("Cannot specify both pattern and list") + + if args.list: + listfile = make_abs_path(args.list) + multi_package_list = MultiPackagesList.load(listfile) + package_list = multi_package_list["Local Cache"] + else: + ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*") + package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) + conan_api.cache.verify(package_list) + + @conan_subcommand(formatters={"text": print_list_text, "json": print_list_json}) def cache_save(conan_api: ConanAPI, parser, subparser, *args): diff --git a/conan/internal/rest/pkg_sign.py b/conan/internal/rest/pkg_sign.py index baa8e8c72cc..4c42574ae8b 100644 --- a/conan/internal/rest/pkg_sign.py +++ b/conan/internal/rest/pkg_sign.py @@ -1,44 +1,95 @@ import os +from conan.api.output import ConanOutput from conan.internal.cache.conan_reference_layout import METADATA from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file -from conan.internal.util.files import mkdir +from conan.internal.util.files import mkdir, sha256sum + +SIGN_SUMMARY_FILENAME = "sign-summary.json" + +SIGN_SUMMARY_CONTENT = { + "provider": None, + "method": None, + "files": {} +} + + +def is_signed(signature_folder): + return os.path.exists(os.path.join(signature_folder, SIGN_SUMMARY_FILENAME)) class PkgSignaturesPlugin: def __init__(self, cache, home_folder): self._cache = cache signer = HomePaths(home_folder).sign_plugin_path + self._plugin_sign_function = self._plugin_verify_function = None if os.path.isfile(signer): mod, _ = load_python_file(signer) - # TODO: At the moment it requires both methods sign and verify, but that might be relaxed - self._plugin_sign_function = mod.sign - self._plugin_verify_function = mod.verify - else: - self._plugin_sign_function = self._plugin_verify_function = None + try: + self._plugin_sign_function = mod.sign + except AttributeError: + pass + try: + self._plugin_verify_function = mod.verify + except AttributeError: + pass + + def create_summary(self, files, artifacts_folder): + checksums = {} + for fname in os.listdir(artifacts_folder): + file_path = os.path.join(artifacts_folder, fname) + if os.path.isfile(file_path): + sha256 = sha256sum(file_path) + checksums[fname] = sha256 + sorted_checksums = dict(sorted(checksums.items())) + content = SIGN_SUMMARY_CONTENT.copy() + content["files"] = sorted_checksums - def sign(self, upload_data): + def sign(self, upload_data, action="upload"): # cache, upload, if self._plugin_sign_function is None: + ConanOutput().error("Package signing plugin: sign function not found") return def _sign(ref, files, folder): metadata_sign = os.path.join(folder, METADATA, "sign") mkdir(metadata_sign) - self._plugin_sign_function(ref, artifacts_folder=folder, signature_folder=metadata_sign) - for f in os.listdir(metadata_sign): - files[f"{METADATA}/sign/{f}"] = os.path.join(metadata_sign, f) + self._plugin_sign_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, + sign_summary=self.create_summary(folder)) - for rref, recipe_bundle in upload_data.refs().items(): - if recipe_bundle["upload"]: - _sign(rref, recipe_bundle["files"], self._cache.recipe_layout(rref).download_export()) - for pref, pkg_bundle in upload_data.prefs(rref, recipe_bundle).items(): - if pkg_bundle["upload"]: - _sign(pref, pkg_bundle["files"], self._cache.pkg_layout(pref).download_package()) + if action == "upload": + for rref, recipe_bundle in upload_data.refs().items(): + if recipe_bundle["upload"]: + _sign(rref, recipe_bundle["files"], self._cache.recipe_layout(rref).download_export()) + for pref, pkg_bundle in upload_data.prefs(rref, recipe_bundle).items(): + if pkg_bundle["upload"]: + _sign(pref, pkg_bundle["files"], self._cache.pkg_layout(pref).download_package()) + else: + for rref, recipe_bundle in upload_data.refs().items(): + if recipe_bundle: + _sign(rref, [], self._cache.recipe_layout(rref).download_export()) + for pref, pkg_bundle in upload_data.prefs(rref, recipe_bundle).items(): + if pkg_bundle: + _sign(pref, [], self._cache.pkg_layout(pref).download_package()) def verify(self, ref, folder, files): if self._plugin_verify_function is None: + ConanOutput().error("Package signing plugin: verify function not found") return metadata_sign = os.path.join(folder, METADATA, "sign") self._plugin_verify_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, files=files) + + def verify_pkglist(self, pkg_list, action="cache"): # cache, install, upload + if self._plugin_verify_function is None: + ConanOutput().error("Package signing plugin: verify function not found") + return + + for rref, recipe_bundle in pkg_list.refs().items(): + if recipe_bundle: + rref_folder = self._cache.recipe_layout(rref).download_export() + self.verify(rref, rref_folder, os.listdir(rref_folder)) + for pref, pkg_bundle in pkg_list.prefs(rref, recipe_bundle).items(): + if pkg_bundle: + pref_folder = self._cache.pkg_layout(pref).download_package() + self.verify(pref, pref_folder, os.listdir(pref_folder)) diff --git a/test/integration/command/cache/test_cache_sign.py b/test/integration/command/cache/test_cache_sign.py new file mode 100644 index 00000000000..af634c621b0 --- /dev/null +++ b/test/integration/command/cache/test_cache_sign.py @@ -0,0 +1,47 @@ +import os +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient + + +def test_pkg_sign_no_plugin(): + c = TestClient() + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + c.run("create .") + c.run("cache sign *") + assert "ERROR: Package signing plugin: sign function not found" in c.out + c.run("cache verify *") + assert "ERROR: Package signing plugin: verify function not found" in c.out + + +def test_pkg_sign_basic(): + c = TestClient() + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + signer = textwrap.dedent(r""" + def sign(ref, artifacts_folder, signature_folder): + print(f"Signing package {ref.repr_notime()}") + """) + c.save_home({"extensions/plugins/sign/sign.py": signer}) + c.run("create .") + c.run("cache sign *") + assert "Signing package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" in c.out + assert "Signing package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" \ + ":da39a3ee5e6b4b0d3255bfef95601890afd80709#0ba8627bd47edc3a501e8f0eb9a79e5e" in c.out + + +def test_pkg_verify_basic(): + c = TestClient() + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + signer = textwrap.dedent(r""" + def verify(ref, artifacts_folder, signature_folder, files): + print(f"Verifying package {ref.repr_notime()}") + """) + c.save_home({"extensions/plugins/sign/sign.py": signer}) + c.run("create .") + c.run("cache verify *") + assert "Verifying package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" in c.out + assert "Verifying package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" \ + ":da39a3ee5e6b4b0d3255bfef95601890afd80709#0ba8627bd47edc3a501e8f0eb9a79e5e" in c.out From 13dd0f7c835cff92fe43641cfe11a49735589411 Mon Sep 17 00:00:00 2001 From: danimtb Date: Wed, 13 Aug 2025 11:59:06 +0200 Subject: [PATCH 2/5] add sign_tools --- conan/internal/rest/pkg_sign.py | 78 +++++++++++++------ .../command/cache/test_cache_sign.py | 8 +- test/integration/test_pkg_signing.py | 2 +- test/unittests/tools/files/test_sign_tools.py | 49 ++++++++++++ 4 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 test/unittests/tools/files/test_sign_tools.py diff --git a/conan/internal/rest/pkg_sign.py b/conan/internal/rest/pkg_sign.py index 4c42574ae8b..f4e9e07f9d3 100644 --- a/conan/internal/rest/pkg_sign.py +++ b/conan/internal/rest/pkg_sign.py @@ -1,22 +1,63 @@ +import copy +import json import os from conan.api.output import ConanOutput from conan.internal.cache.conan_reference_layout import METADATA from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file -from conan.internal.util.files import mkdir, sha256sum +from conan.internal.util.files import load, mkdir, save, sha256sum -SIGN_SUMMARY_FILENAME = "sign-summary.json" -SIGN_SUMMARY_CONTENT = { - "provider": None, - "method": None, - "files": {} -} +class PkgSignaturesTools: + SIGN_SUMMARY_CONTENT = { + "provider": None, + "method": None, + "files": {} + } + SIGN_SUMMARY_FILENAME = "sign-summary.json" -def is_signed(signature_folder): - return os.path.exists(os.path.join(signature_folder, SIGN_SUMMARY_FILENAME)) + def __init__(self, artifacts_folder, signature_folder): + self._artifacts_folder = artifacts_folder + self._signature_folder = signature_folder + + def get_summary_file_path(self): + return os.path.join(self._signature_folder, self.SIGN_SUMMARY_FILENAME) + + def is_pkg_signed(self): + return os.path.isfile(self.get_summary_file_path()) + + def create_summary_content(self): + """ + Creates the summary content as a dictionary for manipulation + @return: Dictionary with the summary content + """ + checksums = {} + for fname in os.listdir(self._artifacts_folder): + file_path = os.path.join(self._artifacts_folder, fname) + if os.path.isfile(file_path): + sha256 = sha256sum(file_path) + checksums[fname] = sha256 + sorted_checksums = dict(sorted(checksums.items())) + content = copy.deepcopy(self.SIGN_SUMMARY_CONTENT) + content["files"] = sorted_checksums + return content + + def load_summary(self): + """" + Loads the summary file from the signature folder + """ + return json.loads(load(self.get_summary_file_path())) + + def save_summary(self, content): + """ + Saves the content of the summary to the signature folder using SIGN_SUMMARY_FILENAME as the + file name + @param content: Content of the summary file + @return: + """ + save(self.get_summary_file_path(), json.dumps(content)) class PkgSignaturesPlugin: @@ -35,27 +76,18 @@ def __init__(self, cache, home_folder): except AttributeError: pass - def create_summary(self, files, artifacts_folder): - checksums = {} - for fname in os.listdir(artifacts_folder): - file_path = os.path.join(artifacts_folder, fname) - if os.path.isfile(file_path): - sha256 = sha256sum(file_path) - checksums[fname] = sha256 - sorted_checksums = dict(sorted(checksums.items())) - content = SIGN_SUMMARY_CONTENT.copy() - content["files"] = sorted_checksums - def sign(self, upload_data, action="upload"): # cache, upload, if self._plugin_sign_function is None: ConanOutput().error("Package signing plugin: sign function not found") return def _sign(ref, files, folder): + output = ConanOutput(scope=f"{ref.repr_notime()}") metadata_sign = os.path.join(folder, METADATA, "sign") mkdir(metadata_sign) + sign_tools = PkgSignaturesTools(folder, metadata_sign) self._plugin_sign_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, - sign_summary=self.create_summary(folder)) + output=output, sign_tools=sign_tools) if action == "upload": for rref, recipe_bundle in upload_data.refs().items(): @@ -76,9 +108,11 @@ def verify(self, ref, folder, files): if self._plugin_verify_function is None: ConanOutput().error("Package signing plugin: verify function not found") return + output = ConanOutput(scope=f"{ref.repr_notime()}") metadata_sign = os.path.join(folder, METADATA, "sign") + sign_tools = PkgSignaturesTools(folder, metadata_sign) self._plugin_verify_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, - files=files) + files=files, output=output, sign_tools=sign_tools) def verify_pkglist(self, pkg_list, action="cache"): # cache, install, upload if self._plugin_verify_function is None: diff --git a/test/integration/command/cache/test_cache_sign.py b/test/integration/command/cache/test_cache_sign.py index af634c621b0..8061fb2cf95 100644 --- a/test/integration/command/cache/test_cache_sign.py +++ b/test/integration/command/cache/test_cache_sign.py @@ -21,8 +21,8 @@ def test_pkg_sign_basic(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) signer = textwrap.dedent(r""" - def sign(ref, artifacts_folder, signature_folder): - print(f"Signing package {ref.repr_notime()}") + def sign(ref, artifacts_folder, signature_folder, output, sign_tools): + output.info(f"Signing package {ref.repr_notime()}") """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.run("create .") @@ -36,8 +36,8 @@ def test_pkg_verify_basic(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) signer = textwrap.dedent(r""" - def verify(ref, artifacts_folder, signature_folder, files): - print(f"Verifying package {ref.repr_notime()}") + def verify(ref, artifacts_folder, signature_folder, files, output, sign_tools): + output.info(f"Verifying package {ref.repr_notime()}") """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.run("create .") diff --git a/test/integration/test_pkg_signing.py b/test/integration/test_pkg_signing.py index d29d21ef80d..d992b4a58fd 100644 --- a/test/integration/test_pkg_signing.py +++ b/test/integration/test_pkg_signing.py @@ -13,7 +13,7 @@ def test_pkg_sign(): signer = textwrap.dedent(r""" import os - def sign(ref, artifacts_folder, signature_folder): + def sign(ref, artifacts_folder, signature_folder, **kwargs): print("Signing ref: ", ref) print("Signing folder: ", artifacts_folder) files = [] diff --git a/test/unittests/tools/files/test_sign_tools.py b/test/unittests/tools/files/test_sign_tools.py new file mode 100644 index 00000000000..cab0f2e4cb1 --- /dev/null +++ b/test/unittests/tools/files/test_sign_tools.py @@ -0,0 +1,49 @@ +import os + +import pytest + +from conan.internal.rest.pkg_sign import PkgSignaturesTools +from conan.test.utils.tools import temp_folder, save_files + + +@pytest.fixture +def pkg_sign_tools(): + main_folder = temp_folder() + artifacts_folder = os.path.join(main_folder, "af") + os.mkdir(artifacts_folder) + signature_folder = os.path.join(main_folder, "sf") + os.mkdir(signature_folder) + save_files(artifacts_folder, {"conan_package.tgz": "", "conanmanifest.txt": ""}) + return PkgSignaturesTools(artifacts_folder, signature_folder) + + +def test_get_summary_file_path(pkg_sign_tools): + sfp = pkg_sign_tools.get_summary_file_path() + assert f"sf{os.path.sep}sign-summary.json" in sfp + + +def test_create_summary_content(pkg_sign_tools): + c = pkg_sign_tools.create_summary_content() + assert c.get("method") is None + assert c.get("provider") is None + assert c.get("files").get("conan_package.tgz") + assert c.get("files").get("conanmanifest.txt") + + +def test_save_load_summary(pkg_sign_tools): + c = pkg_sign_tools.create_summary_content() + c["provider"] = "conan" + c["method"] = "sigstore" + pkg_sign_tools.save_summary(c) + assert os.path.exists(os.path.join(pkg_sign_tools._signature_folder, "sign-summary.json")) + summary = pkg_sign_tools.load_summary() + assert summary.get("provider") == "conan" + assert summary.get("method") == "sigstore" + assert list(summary.get("files").keys()) == ["conan_package.tgz", "conanmanifest.txt"] + + +def test_is_pkg_signed(pkg_sign_tools): + assert not pkg_sign_tools.is_pkg_signed() + c = pkg_sign_tools.create_summary_content() + pkg_sign_tools.save_summary(c) + assert pkg_sign_tools.is_pkg_signed() From d8b0454a96898da4fe3ab7ebb26b8d332daee51a Mon Sep 17 00:00:00 2001 From: danimtb Date: Thu, 14 Aug 2025 13:19:49 +0200 Subject: [PATCH 3/5] add output formatters --- conan/api/subapi/cache.py | 14 +-- conan/cli/commands/cache.py | 35 ++++-- conan/internal/rest/pkg_sign.py | 100 +++++++++++++----- .../command/cache/test_cache_sign.py | 72 ++++++++++++- test/integration/test_pkg_signing.py | 64 +++++++++++ test/unittests/tools/files/test_sign_tools.py | 2 + 6 files changed, 245 insertions(+), 42 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 3c360e7f7b8..581f2c0d647 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -79,15 +79,17 @@ def check_integrity(self, package_list): def sign(self, package_list): """Sign packages with the signing plugin""" - cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) - pkg_signer = PkgSignaturesPlugin(cache, self.conan_api.home_folder) - pkg_signer.sign(package_list, action="cache") + cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) + pkg_signer = PkgSignaturesPlugin(cache, self._conan_api.home_folder) + results = pkg_signer.sign(package_list, context="cache") + return {"results": results, "context": "cache", "action": "sign"} def verify(self, package_list): """Verify packages with the signing plugin""" - cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) - pkg_signer = PkgSignaturesPlugin(cache, self.conan_api.home_folder) - pkg_signer.verify_pkglist(package_list) + cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) + pkg_signer = PkgSignaturesPlugin(cache, self._conan_api.home_folder) + results = pkg_signer.verify_pkglist(package_list, context="cache") + return {"results": results, "context": "cache", "action": "verify"} def clean(self, package_list, source=True, build=True, download=True, temp=True, backup_sources=False): diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py index e0693103de3..dc8af03fe9e 100644 --- a/conan/cli/commands/cache.py +++ b/conan/cli/commands/cache.py @@ -2,7 +2,7 @@ from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, MultiPackagesList -from conan.api.output import cli_out_write, ConanOutput +from conan.api.output import cli_out_write, ConanOutput, Color from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.commands.list import print_list_text, print_list_json @@ -15,6 +15,25 @@ def json_export(data): cli_out_write(json.dumps({"cache_path": data})) +def print_cache_sign_verify_text(data): + elements = data.get("results") + if elements: + title = "Verification" if data.get("action") == "verify" else "Signing" + cli_out_write(f"[Package signing plugin] {title} results:", fg=Color.BRIGHT_BLUE) + for ref, result in elements.items(): + cli_out_write(f"- {ref}", fg=Color.BRIGHT_BLUE) + if result is None: + result = "Ok" + color = Color.BRIGHT_YELLOW if "warn" in result else Color.BRIGHT_WHITE + color = Color.BRIGHT_RED if "fail" in result else color + cli_out_write(f" {result}", fg=color) + + +def print_cache_sign_verify_json(data): + myjson = json.dumps(data, indent=4) + cli_out_write(myjson) + + @conan_command(group="Consumer") def cache(conan_api: ConanAPI, parser, *args): """ @@ -150,10 +169,11 @@ def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args): ConanOutput().success("Integrity check: ok") -@conan_subcommand() +@conan_subcommand(formatters={"text": print_cache_sign_verify_text, + "json": print_cache_sign_verify_json}) def cache_sign(conan_api: ConanAPI, parser, subparser, *args): """ - Sign packages + Sign packages with the Package Singing Plugin """ subparser.add_argument("pattern", nargs="?", help="Selection pattern for references to check integrity for") @@ -176,13 +196,14 @@ def cache_sign(conan_api: ConanAPI, parser, subparser, *args): else: ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) - conan_api.cache.sign(package_list) + return conan_api.cache.sign(package_list) -@conan_subcommand() +@conan_subcommand(formatters={"text": print_cache_sign_verify_text, + "json": print_cache_sign_verify_json}) def cache_verify(conan_api: ConanAPI, parser, subparser, *args): """ - Check siugnature + Check the signature of packages with the Package Singing Plugin """ subparser.add_argument("pattern", nargs="?", help="Selection pattern for references to check integrity for") @@ -205,7 +226,7 @@ def cache_verify(conan_api: ConanAPI, parser, subparser, *args): else: ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) - conan_api.cache.verify(package_list) + return conan_api.cache.verify(package_list) @conan_subcommand(formatters={"text": print_list_text, diff --git a/conan/internal/rest/pkg_sign.py b/conan/internal/rest/pkg_sign.py index f4e9e07f9d3..5d341ada852 100644 --- a/conan/internal/rest/pkg_sign.py +++ b/conan/internal/rest/pkg_sign.py @@ -3,6 +3,7 @@ import os from conan.api.output import ConanOutput +from conan.errors import ConanException from conan.internal.cache.conan_reference_layout import METADATA from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file @@ -26,7 +27,11 @@ def get_summary_file_path(self): return os.path.join(self._signature_folder, self.SIGN_SUMMARY_FILENAME) def is_pkg_signed(self): - return os.path.isfile(self.get_summary_file_path()) + try: + c = self.load_summary() + except FileNotFoundError: + return False + return bool(c.get("provider") and c.get("method")) def create_summary_content(self): """ @@ -57,16 +62,18 @@ def save_summary(self, content): @param content: Content of the summary file @return: """ + assert content.get("provider") + assert content.get("method") save(self.get_summary_file_path(), json.dumps(content)) class PkgSignaturesPlugin: def __init__(self, cache, home_folder): self._cache = cache - signer = HomePaths(home_folder).sign_plugin_path + self.sign_plugin_path = HomePaths(home_folder).sign_plugin_path self._plugin_sign_function = self._plugin_verify_function = None - if os.path.isfile(signer): - mod, _ = load_python_file(signer) + if os.path.isfile(self.sign_plugin_path): + mod, _ = load_python_file(self.sign_plugin_path) try: self._plugin_sign_function = mod.sign except AttributeError: @@ -76,54 +83,93 @@ def __init__(self, cache, home_folder): except AttributeError: pass - def sign(self, upload_data, action="upload"): # cache, upload, + def sign(self, upload_data, context="upload"): # cache, upload, + results = {} if self._plugin_sign_function is None: - ConanOutput().error("Package signing plugin: sign function not found") - return + ConanOutput().error(f"[Package signing plugin] sign() function not found in " + f"{self.sign_plugin_path}") + return results - def _sign(ref, files, folder): + def _sign(ref, files, folder, context="upload"): output = ConanOutput(scope=f"{ref.repr_notime()}") metadata_sign = os.path.join(folder, METADATA, "sign") mkdir(metadata_sign) sign_tools = PkgSignaturesTools(folder, metadata_sign) - self._plugin_sign_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, - output=output, sign_tools=sign_tools) - - if action == "upload": + try: + result = self._plugin_sign_function(ref, artifacts_folder=folder, + signature_folder=metadata_sign, output=output, + sign_tools=sign_tools) + except ConanException as e: + result = _handle_failure(e, context, ref) + # Add files to the pkglist/bundle + for f in os.listdir(metadata_sign): + files[f"{METADATA}/sign/{f}"] = os.path.join(metadata_sign, f) + return {ref.repr_notime(): result} + + if context == "upload": for rref, recipe_bundle in upload_data.refs().items(): if recipe_bundle["upload"]: - _sign(rref, recipe_bundle["files"], self._cache.recipe_layout(rref).download_export()) + result = _sign(rref, recipe_bundle["files"], + self._cache.recipe_layout(rref).download_export()) + results.update(result) for pref, pkg_bundle in upload_data.prefs(rref, recipe_bundle).items(): if pkg_bundle["upload"]: - _sign(pref, pkg_bundle["files"], self._cache.pkg_layout(pref).download_package()) + result = _sign(pref, pkg_bundle["files"], + self._cache.pkg_layout(pref).download_package()) + results.update(result) else: for rref, recipe_bundle in upload_data.refs().items(): if recipe_bundle: - _sign(rref, [], self._cache.recipe_layout(rref).download_export()) + result = _sign(rref, {}, self._cache.recipe_layout(rref).download_export(), + context) + results.update(result) for pref, pkg_bundle in upload_data.prefs(rref, recipe_bundle).items(): if pkg_bundle: - _sign(pref, [], self._cache.pkg_layout(pref).download_package()) + result = _sign(pref, {}, self._cache.pkg_layout(pref).download_package(), + context) + results.update(result) + return results - def verify(self, ref, folder, files): + def verify(self, ref, folder, files, context="install"): if self._plugin_verify_function is None: - ConanOutput().error("Package signing plugin: verify function not found") - return + ConanOutput().error(f"[Package signing plugin] verify() function not found in " + f"{self.sign_plugin_path}") + return {} output = ConanOutput(scope=f"{ref.repr_notime()}") metadata_sign = os.path.join(folder, METADATA, "sign") sign_tools = PkgSignaturesTools(folder, metadata_sign) - self._plugin_verify_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, - files=files, output=output, sign_tools=sign_tools) - - def verify_pkglist(self, pkg_list, action="cache"): # cache, install, upload + try: + result = self._plugin_verify_function(ref, artifacts_folder=folder, + signature_folder=metadata_sign, files=files, + output=output, sign_tools=sign_tools) + except ConanException as e: + result = _handle_failure(e, context, ref) + return {ref.repr_notime(): result} + + def verify_pkglist(self, pkg_list, context="cache"): # cache, install, upload + results = {} if self._plugin_verify_function is None: - ConanOutput().error("Package signing plugin: verify function not found") - return + ConanOutput().error(f"[Package signing plugin] verify() function not found in " + f"{self.sign_plugin_path}") + return results for rref, recipe_bundle in pkg_list.refs().items(): if recipe_bundle: rref_folder = self._cache.recipe_layout(rref).download_export() - self.verify(rref, rref_folder, os.listdir(rref_folder)) + result = self.verify(rref, rref_folder, os.listdir(rref_folder), context) + results.update(result) for pref, pkg_bundle in pkg_list.prefs(rref, recipe_bundle).items(): if pkg_bundle: pref_folder = self._cache.pkg_layout(pref).download_package() - self.verify(pref, pref_folder, os.listdir(pref_folder)) + result = self.verify(pref, pref_folder, os.listdir(pref_folder), context) + results.update(result) + return results + + +def _handle_failure(exception, action, ref): + exception_msg = str(exception) + if action in ["upload", "install"]: + raise ConanException(f"{ref.repr_notime()}: {exception_msg}") + else: + error_msg = f"Failed: {exception_msg}" if exception_msg else "Failed" + return error_msg diff --git a/test/integration/command/cache/test_cache_sign.py b/test/integration/command/cache/test_cache_sign.py index 8061fb2cf95..dea08cfeedb 100644 --- a/test/integration/command/cache/test_cache_sign.py +++ b/test/integration/command/cache/test_cache_sign.py @@ -1,3 +1,4 @@ +import json import os import textwrap @@ -12,9 +13,9 @@ def test_pkg_sign_no_plugin(): c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) c.run("create .") c.run("cache sign *") - assert "ERROR: Package signing plugin: sign function not found" in c.out + assert "ERROR: [Package signing plugin] sign() function not found" in c.out c.run("cache verify *") - assert "ERROR: Package signing plugin: verify function not found" in c.out + assert "ERROR: [Package signing plugin] verify() function not found" in c.out def test_pkg_sign_basic(): @@ -38,6 +39,7 @@ def test_pkg_verify_basic(): signer = textwrap.dedent(r""" def verify(ref, artifacts_folder, signature_folder, files, output, sign_tools): output.info(f"Verifying package {ref.repr_notime()}") + return "success" """) c.save_home({"extensions/plugins/sign/sign.py": signer}) c.run("create .") @@ -45,3 +47,69 @@ def verify(ref, artifacts_folder, signature_folder, files, output, sign_tools): assert "Verifying package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" in c.out assert "Verifying package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" \ ":da39a3ee5e6b4b0d3255bfef95601890afd80709#0ba8627bd47edc3a501e8f0eb9a79e5e" in c.out + + +def test_pkg_sign_exception(): + c = TestClient() + signer = textwrap.dedent(r""" + from conan.errors import ConanException + + def sign(ref, artifacts_folder, signature_folder, output, sign_tools): + output.info(f"Signing package {ref.repr_notime()}") + if "lib" in ref.repr_notime(): + raise ConanException("error signing package") + return "success" + """) + c.save_home({"extensions/plugins/sign/sign.py": signer}) + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + c.run("create .") + c.save({"conanfile.py": GenConanfile("lib", "0.1")}) + c.run("create .") + c.run("cache sign *") + assert "Signing package lib/0.1#dbe307e08b1a344fef76f60c85c0c4e8" in c.out + assert "Signing package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" in c.out + assert "[Package signing plugin] Signing results:" in c.out + assert "- lib/0.1#dbe307e08b1a344fef76f60c85c0c4e8\n" \ + " Failed: error signing package" in c.out + assert "- pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164\n" \ + " success" in c.out + # test json output + c.run("cache sign * -f json") + data = json.loads(c.stdout) + assert data["action"] == "sign" + assert data["results"]["lib/0.1#dbe307e08b1a344fef76f60c85c0c4e8"] == \ + "Failed: error signing package" + assert data["results"]["pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164"] == "success" + + +def test_pkg_verify_exception(): + c = TestClient() + signer = textwrap.dedent(r""" + from conan.errors import ConanException + + def verify(ref, artifacts_folder, signature_folder, files, output, sign_tools): + output.info(f"Verifying package {ref.repr_notime()}") + if "lib" in ref.repr_notime(): + raise ConanException("bad signature verification") + return "success" + """) + c.save_home({"extensions/plugins/sign/sign.py": signer}) + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + c.run("create .") + c.save({"conanfile.py": GenConanfile("lib", "0.1")}) + c.run("create .") + c.run("cache verify *") + assert "Verifying package lib/0.1#dbe307e08b1a344fef76f60c85c0c4e8" in c.out + assert "Verifying package pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164" in c.out + assert "[Package signing plugin] Verification results:" in c.out + assert "- lib/0.1#dbe307e08b1a344fef76f60c85c0c4e8\n" \ + " Failed: bad signature verification" in c.out + assert "- pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164\n" \ + " success" in c.out + # test json output + c.run("cache verify * -f json") + data = json.loads(c.stdout) + assert data["action"] == "verify" + assert data["results"]["lib/0.1#dbe307e08b1a344fef76f60c85c0c4e8"] == \ + "Failed: bad signature verification" + assert data["results"]["pkg/0.1#485dad6cb11e2fa99d9afbe44a57a164"] == "success" diff --git a/test/integration/test_pkg_signing.py b/test/integration/test_pkg_signing.py index c8ecfa1c414..158d0488538 100644 --- a/test/integration/test_pkg_signing.py +++ b/test/integration/test_pkg_signing.py @@ -54,3 +54,67 @@ def verify(ref, artifacts_folder, signature_folder, files, **kwargs): assert "Verifying ref: pkg/0.1" in c.out assert "VERIFYING conanfile.py" not in c.out # It doesn't re-verify previous contents assert "VERIFYING conan_sources.tgz" in c.out + + +def test_pkg_sign_with_tools(): + c = TestClient(default_server_user=True) + c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports("export/*") + .with_exports_sources("export_sources/*").with_package_file("myfile", "mycontents!"), + "export/file1.txt": "file1!", + "export_sources/file2.txt": "file2!"}) + signer = textwrap.dedent(r""" + import os + + def sign(ref, artifacts_folder, signature_folder, output, sign_tools): + output.info("Signing reference") + output.info(f"Signing folder: {artifacts_folder}") + files = [] + c = sign_tools.create_summary_content() + c["provider"] = "the provider" + c["method"] = "the method" + sign_tools.save_summary(c) + signature = sign_tools.get_summary_file_path() + ".asc" + files = [] + for f in sorted(os.listdir(artifacts_folder)): + if os.path.isfile(os.path.join(artifacts_folder, f)): + files.append(f) + open(signature, "w").write("\n".join(files)) + contents = open(signature).read() + output.info(f"Sign contents: {contents}") + + def verify(ref, artifacts_folder, signature_folder, files, output, sign_tools): + if not sign_tools.is_pkg_signed(): + output.warning("Package not signed, skipping verification") + return "not signed" + output.info("Verifying reference") + output.info(f"Verifying folder {artifacts_folder}") + summary = sign_tools.load_summary() + output.info(f"Verifying sign provider: {summary.get('provider')}") + output.info(f"Verifying sign method: {summary.get('method')}") + signature = sign_tools.get_summary_file_path() + ".asc" + contents = open(signature).read() + output.info(f"Verifying contents: {contents}") + for f in files: + output.info(f"Verifying file {f}") + if os.path.isfile(os.path.join(artifacts_folder, f)): + assert f in contents + return "ok" + """) + c.save_home({"extensions/plugins/sign/sign.py": signer}) + c.run("create .") + c.run("cache verify *") + assert "WARN: Package not signed, skipping verification" in c.out + c.run("upload * -r=default -c") + assert "pkg/0.1#5e2d444a24c6bdf96fc141053eb3bb7a: Signing reference" in c.out + c.run("remove * -c") + c.run("install --requires=pkg/0.1") + assert "Verifying sign method: the method" in c.out + assert "Verifying sign provider: the provider" in c.out + assert "pkg/0.1#5e2d444a24c6bdf96fc141053eb3bb7a: Verifying reference" in c.out + assert "Verifying file conanfile.py" in c.out + assert "Verifying file conan_sources.tgz" not in c.out # Sources not retrieved now + assert "Verifying file conan_package.tgz" in c.out + # Lets force the retrieval of the sources + c.run("install --requires=pkg/0.1 --build=*") + assert "Verifying file conanfile.py" not in c.out # It doesn't re-verify previous contents + assert "Verifying file conan_sources.tgz" in c.out diff --git a/test/unittests/tools/files/test_sign_tools.py b/test/unittests/tools/files/test_sign_tools.py index cab0f2e4cb1..311ba3897b5 100644 --- a/test/unittests/tools/files/test_sign_tools.py +++ b/test/unittests/tools/files/test_sign_tools.py @@ -45,5 +45,7 @@ def test_save_load_summary(pkg_sign_tools): def test_is_pkg_signed(pkg_sign_tools): assert not pkg_sign_tools.is_pkg_signed() c = pkg_sign_tools.create_summary_content() + c["provider"] = "the provider" + c["method"] = "the method" pkg_sign_tools.save_summary(c) assert pkg_sign_tools.is_pkg_signed() From a1d53684e203d2dc1f7645462ec5dca2cc9a0b9b Mon Sep 17 00:00:00 2001 From: danimtb Date: Thu, 14 Aug 2025 13:33:08 +0200 Subject: [PATCH 4/5] add todo --- conan/internal/rest/pkg_sign.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conan/internal/rest/pkg_sign.py b/conan/internal/rest/pkg_sign.py index 5d341ada852..106a8b20322 100644 --- a/conan/internal/rest/pkg_sign.py +++ b/conan/internal/rest/pkg_sign.py @@ -169,6 +169,7 @@ def verify_pkglist(self, pkg_list, context="cache"): # cache, install, upload def _handle_failure(exception, action, ref): exception_msg = str(exception) if action in ["upload", "install"]: + # TODO: Mark folder with set_dirty(artifacts_folder) raise ConanException(f"{ref.repr_notime()}: {exception_msg}") else: error_msg = f"Failed: {exception_msg}" if exception_msg else "Failed" From 7619da9084b6efb9e4239642906670523b9b7292 Mon Sep 17 00:00:00 2001 From: danimtb Date: Thu, 14 Aug 2025 17:17:52 +0200 Subject: [PATCH 5/5] add cannonical test --- conan/cli/commands/cache.py | 2 +- conan/internal/rest/pkg_sign.py | 4 +- test/integration/test_pkg_signing.py | 114 ++++++++++++++++----------- 3 files changed, 72 insertions(+), 48 deletions(-) diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py index dc8af03fe9e..10ffd4517bc 100644 --- a/conan/cli/commands/cache.py +++ b/conan/cli/commands/cache.py @@ -23,7 +23,7 @@ def print_cache_sign_verify_text(data): for ref, result in elements.items(): cli_out_write(f"- {ref}", fg=Color.BRIGHT_BLUE) if result is None: - result = "Ok" + result = "Signed" if data.get("action") == "sign" else "Signature verified" color = Color.BRIGHT_YELLOW if "warn" in result else Color.BRIGHT_WHITE color = Color.BRIGHT_RED if "fail" in result else color cli_out_write(f" {result}", fg=color) diff --git a/conan/internal/rest/pkg_sign.py b/conan/internal/rest/pkg_sign.py index 106a8b20322..319ede28a44 100644 --- a/conan/internal/rest/pkg_sign.py +++ b/conan/internal/rest/pkg_sign.py @@ -99,7 +99,7 @@ def _sign(ref, files, folder, context="upload"): result = self._plugin_sign_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, output=output, sign_tools=sign_tools) - except ConanException as e: + except (ConanException, AssertionError) as e: result = _handle_failure(e, context, ref) # Add files to the pkglist/bundle for f in os.listdir(metadata_sign): @@ -142,7 +142,7 @@ def verify(self, ref, folder, files, context="install"): result = self._plugin_verify_function(ref, artifacts_folder=folder, signature_folder=metadata_sign, files=files, output=output, sign_tools=sign_tools) - except ConanException as e: + except (ConanException, AssertionError) as e: result = _handle_failure(e, context, ref) return {ref.repr_notime(): result} diff --git a/test/integration/test_pkg_signing.py b/test/integration/test_pkg_signing.py index 158d0488538..c0841d8931e 100644 --- a/test/integration/test_pkg_signing.py +++ b/test/integration/test_pkg_signing.py @@ -56,65 +56,89 @@ def verify(ref, artifacts_folder, signature_folder, files, **kwargs): assert "VERIFYING conan_sources.tgz" in c.out -def test_pkg_sign_with_tools(): +def test_pkg_sign_canonical(): c = TestClient(default_server_user=True) - c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_exports("export/*") - .with_exports_sources("export_sources/*").with_package_file("myfile", "mycontents!"), - "export/file1.txt": "file1!", - "export_sources/file2.txt": "file2!"}) + c.save({"conanfile1.py": GenConanfile("lib1ok", "0.1"), + "conanfile2.py": GenConanfile("lib2fail", "0.1"), # This pkg fails when installed + "conanfile3.py": GenConanfile("lib3fail", "0.1")}) # This pkg should always fail + c.run("create conanfile1.py") + c.run("create conanfile2.py") + c.run("create conanfile3.py") signer = textwrap.dedent(r""" import os + from conan.errors import ConanException def sign(ref, artifacts_folder, signature_folder, output, sign_tools): output.info("Signing reference") output.info(f"Signing folder: {artifacts_folder}") - files = [] + if sign_tools.is_pkg_signed(): + summary = sign_tools.load_summary() + if summary.get("provider") != "conan-client": + output.warning("Package already signed by another provider") + return "Warn: Package already signed by another provider" + output.info("Package already signed by the same provider") + return "Package already signed by the same provider" + c = sign_tools.create_summary_content() - c["provider"] = "the provider" - c["method"] = "the method" + c["method"] = "sigstore" + + if "lib3fail" in str(ref): + raise ConanException("sign failed") + elif "lib2fail" in str(ref): + c["provider"] = "this will fail to verify" + else: + c["provider"] = "conan-client" sign_tools.save_summary(c) - signature = sign_tools.get_summary_file_path() + ".asc" - files = [] - for f in sorted(os.listdir(artifacts_folder)): - if os.path.isfile(os.path.join(artifacts_folder, f)): - files.append(f) - open(signature, "w").write("\n".join(files)) - contents = open(signature).read() - output.info(f"Sign contents: {contents}") + output.info("Signature ok") def verify(ref, artifacts_folder, signature_folder, files, output, sign_tools): - if not sign_tools.is_pkg_signed(): - output.warning("Package not signed, skipping verification") - return "not signed" output.info("Verifying reference") - output.info(f"Verifying folder {artifacts_folder}") + if not sign_tools.is_pkg_signed(): + raise ConanException("Package is not signed") + + if "lib3fail" in str(ref): + raise ConanException("verify failed") summary = sign_tools.load_summary() - output.info(f"Verifying sign provider: {summary.get('provider')}") - output.info(f"Verifying sign method: {summary.get('method')}") - signature = sign_tools.get_summary_file_path() + ".asc" - contents = open(signature).read() - output.info(f"Verifying contents: {contents}") - for f in files: - output.info(f"Verifying file {f}") - if os.path.isfile(os.path.join(artifacts_folder, f)): - assert f in contents - return "ok" + assert summary.get("provider") == "conan-client", "wrong provider" + output.info("Verification ok") """) c.save_home({"extensions/plugins/sign/sign.py": signer}) - c.run("create .") + + # Cache verify command does not fail if package is not signed c.run("cache verify *") - assert "WARN: Package not signed, skipping verification" in c.out - c.run("upload * -r=default -c") - assert "pkg/0.1#5e2d444a24c6bdf96fc141053eb3bb7a: Signing reference" in c.out + assert "lib1ok/0.1#a5e2af5522a1edcab963447eec649700\n Failed: Package is not signed" in c.out + assert "lib2fail/0.1#70a185be5a95af3dde25b74ae800b2f2\n Failed: Package is not signed" in c.out + assert "lib3fail/0.1#09ccc766ddd11c96aa78307b3f166fd6\n Failed: Package is not signed" in c.out + + # Cache sign command does not fail if a package fails to sign, but it reports it + c.run("cache sign *") + assert "lib1ok/0.1#a5e2af5522a1edcab963447eec649700\n Signed" in c.out + assert "lib2fail/0.1#70a185be5a95af3dde25b74ae800b2f2\n Signed" in c.out + assert "lib3fail/0.1#09ccc766ddd11c96aa78307b3f166fd6\n Failed: sign failed" in c.out + + # Upload sign fails if package signing fails + c.run("upload * -c -r default", assert_error=True) + assert "lib1ok/0.1#a5e2af5522a1edcab963447eec649700: Package already signed by the same provider" in c.out + assert "lib2fail/0.1#70a185be5a95af3dde25b74ae800b2f2: WARN: Package already signed by another provider" in c.out + assert "ERROR: lib3fail/0.1#09ccc766ddd11c96aa78307b3f166fd6: sign failed" in c.out + + # If upload sign failed, no packages should be uploaded + c.run("list * -r default") + assert "WARN: There are no matching recipe references" in c.out + + # Upload packages individually to avoid previous failure + c.run("upload lib1ok* -c -r default") + c.run("upload lib2fail* -c -r default") c.run("remove * -c") - c.run("install --requires=pkg/0.1") - assert "Verifying sign method: the method" in c.out - assert "Verifying sign provider: the provider" in c.out - assert "pkg/0.1#5e2d444a24c6bdf96fc141053eb3bb7a: Verifying reference" in c.out - assert "Verifying file conanfile.py" in c.out - assert "Verifying file conan_sources.tgz" not in c.out # Sources not retrieved now - assert "Verifying file conan_package.tgz" in c.out - # Lets force the retrieval of the sources - c.run("install --requires=pkg/0.1 --build=*") - assert "Verifying file conanfile.py" not in c.out # It doesn't re-verify previous contents - assert "Verifying file conan_sources.tgz" in c.out + + # Install verify command should fail if package is signed by another provider + c.run("install --requires lib1ok/0.1 --requires lib2fail/0.1 -r default", assert_error=True) + assert "lib1ok/0.1#a5e2af5522a1edcab963447eec649700: Verification ok" in c.out + assert "lib2fail/0.1#70a185be5a95af3dde25b74ae800b2f2: wrong provider" in c.out + + # Packages that failed in install verification should not appear as installed + c.run("list *") + assert "lib1ok" in c.out + assert "lib2fail" not in c.out + c.run("cache verify *") + assert "lib1ok/0.1#a5e2af5522a1edcab963447eec649700\n Signature verified" in c.out