From 06ca302ea8b08ea3002bba87433f02a006435175 Mon Sep 17 00:00:00 2001 From: danimtb Date: Fri, 21 Nov 2025 13:21:27 +0100 Subject: [PATCH 1/5] Add package signing plugin example --- examples/extensions/README.md | 4 + examples/extensions/plugins/sign/readme.md | 16 ++++ examples/extensions/plugins/sign/sign.py | 94 ++++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 examples/extensions/plugins/sign/readme.md create mode 100644 examples/extensions/plugins/sign/sign.py diff --git a/examples/extensions/README.md b/examples/extensions/README.md index 42c7fe29..3515b889 100644 --- a/examples/extensions/README.md +++ b/examples/extensions/README.md @@ -7,3 +7,7 @@ ### [Use custom deployers](extensions/deployers/) - Learn how to create a custom deployer in Conan. [Docs](https://docs.conan.io/2/reference/extensions/deployers.html) + +### [Package signing plugin example with OpenSSL](extensions/plugins/sign) + +- Learn how to create a package signing plugin in Conan. [Docs](https://docs.conan.io/2/reference/extensions/package_signing.html) diff --git a/examples/extensions/plugins/sign/readme.md b/examples/extensions/plugins/sign/readme.md new file mode 100644 index 00000000..9124e846 --- /dev/null +++ b/examples/extensions/plugins/sign/readme.md @@ -0,0 +1,16 @@ + +## Package signing plugin example with Openssl + +To run the package signing example, make sure you are using Conan with the changes in this branch: + + - https://github.com/danimtb/conan/tree/feature/improve_pkg-sign + +Steps to test the example: + +- Copy the ``sign.py`` file to your Conan home at ```CONAN_HOME/extensions/plugins/sign/sing.py```. +- Generate your signing keys (see comment at the top of sign.py) and place them next to the ``sign.py`` file. +- Create a new package to test the sign and verify commands: ``conan new cmake_lib -d name=hello -d version=1.0``. +- Sign the package: ``conan cache sign hello/1.0``. +- Verify the package signature: ```conan cache verify hello/1.0```. +- You can also use the ``conan upload`` command, and the packages should be signed automatically. +- You can also use the ``conan install`` command, and the packages should be verified automatically. diff --git a/examples/extensions/plugins/sign/sign.py b/examples/extensions/plugins/sign/sign.py new file mode 100644 index 00000000..2f605e57 --- /dev/null +++ b/examples/extensions/plugins/sign/sign.py @@ -0,0 +1,94 @@ +""" +Plugin to sign/verify Conan packages with OpenSSL. + +Requirements: The following executables should be installed and in the PATH. + - openssl + +To use this sigstore plugins, first generate a compatible keypair and define the environment variables for the keys: + + $ openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 + +And extract the public key: + + $ openssl pkey -in private_key.pem -pubout -out public_key.pem + +The private_key.pem and public_key.pem files should be placed next to this plugins's file sign.py +(inside the CONAN_HOME/extensions/plugins/sing folder). +""" + + +import os +import subprocess +from conan.api.output import ConanOutput +from conan.errors import ConanException +from conan.tools.pkg_signing.plugin import (create_summary_content, get_summary_file_path, + load_summary, save_summary) + + +def _run_command(command): + result = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, # returns strings instead of bytes + check=False # we'll manually handle error checking + ) + + if result.returncode != 0: + raise subprocess.CalledProcessError( + result.returncode, result.args, output=result.stdout, stderr=result.stderr + ) + + +def sign(ref, artifacts_folder, signature_folder, **kwargs): + c = create_summary_content(artifacts_folder) + c["method"] = "openssl-dgst" + c["provider"] = "conan-client" + save_summary(signature_folder, c) + + # openssl dgst -sha256 -sign private_key.pem -out document.sig document.txt + summary_filepath = get_summary_file_path(signature_folder) + signature_filepath = f"{summary_filepath}.sig" + if os.path.isfile(signature_filepath): + ConanOutput().warning(f"Package {ref.repr_notime()} was already signed") + privkey_filepath = os.path.join(os.path.dirname(__file__), "private_key.pem") + openssl_sign_cmd = [ + "openssl", + "dgst", + "-sha256", + "-sign", privkey_filepath, + "-out", signature_filepath, + summary_filepath, + ] + try: + _run_command(openssl_sign_cmd) + except Exception as exc: + raise ConanException(f"Error signing artifact {summary_filepath}: {exc}") + + +def verify(ref, artifacts_folder, signature_folder, files, **kwargs): + summary_filepath = get_summary_file_path(signature_folder) + signature_filepath = f"{summary_filepath}.sig" + pubkey_filepath = os.path.join(os.path.dirname(__file__), "public_key.pem") + if not os.path.isfile(signature_filepath): + raise ConanException("Signature file does not exist") + + summary = load_summary(signature_folder) + # The provider is useful to choose the correct public key to verify packages with + provider = summary.get("provider") + if provider != "conan-client": + return f"Warn: The provider does not match (conan-client [expected] != {provider} [actual])" + + # openssl dgst -sha256 -verify public_key.pem -signature document.sig document.txt + openssl_verify_cmd = [ + "openssl", + "dgst", + "-sha256", + "-verify", pubkey_filepath, + "-signature", signature_filepath, + summary_filepath, + ] + try: + _run_command(openssl_verify_cmd) + except Exception as exc: + raise ConanException(f"Error verifying signature {signature_filepath}: {exc}") From e7313ee620fd0ff5b548a668d67eb202feb5f7f7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 21 Nov 2025 13:29:21 +0100 Subject: [PATCH 2/5] Update examples/extensions/plugins/sign/readme.md Co-authored-by: Carlos Zoido --- examples/extensions/plugins/sign/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/extensions/plugins/sign/readme.md b/examples/extensions/plugins/sign/readme.md index 9124e846..7aa052a7 100644 --- a/examples/extensions/plugins/sign/readme.md +++ b/examples/extensions/plugins/sign/readme.md @@ -7,7 +7,7 @@ To run the package signing example, make sure you are using Conan with the chang Steps to test the example: -- Copy the ``sign.py`` file to your Conan home at ```CONAN_HOME/extensions/plugins/sign/sing.py```. +- Copy the ``sign.py`` file to your Conan home at ```CONAN_HOME/extensions/plugins/sign/sign.py```. - Generate your signing keys (see comment at the top of sign.py) and place them next to the ``sign.py`` file. - Create a new package to test the sign and verify commands: ``conan new cmake_lib -d name=hello -d version=1.0``. - Sign the package: ``conan cache sign hello/1.0``. From 83b4499ed66599839b00418c0745238d48385eb0 Mon Sep 17 00:00:00 2001 From: danimtb Date: Fri, 21 Nov 2025 13:42:01 +0100 Subject: [PATCH 3/5] update --- examples/extensions/plugins/sign/readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/extensions/plugins/sign/readme.md b/examples/extensions/plugins/sign/readme.md index 9124e846..049452db 100644 --- a/examples/extensions/plugins/sign/readme.md +++ b/examples/extensions/plugins/sign/readme.md @@ -9,7 +9,8 @@ Steps to test the example: - Copy the ``sign.py`` file to your Conan home at ```CONAN_HOME/extensions/plugins/sign/sing.py```. - Generate your signing keys (see comment at the top of sign.py) and place them next to the ``sign.py`` file. -- Create a new package to test the sign and verify commands: ``conan new cmake_lib -d name=hello -d version=1.0``. +- Generate a new project to test the sign and verify commands: ``conan new cmake_lib -d name=hello -d version=1.0``. +- Create the package: ``conan create``. - Sign the package: ``conan cache sign hello/1.0``. - Verify the package signature: ```conan cache verify hello/1.0```. - You can also use the ``conan upload`` command, and the packages should be signed automatically. From 7038381876fdec2594eaf6f7cc26b77d8828ab37 Mon Sep 17 00:00:00 2001 From: danimtb Date: Thu, 4 Dec 2025 11:41:10 +0100 Subject: [PATCH 4/5] minor update --- examples/extensions/plugins/sign/sign.py | 36 ++++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/examples/extensions/plugins/sign/sign.py b/examples/extensions/plugins/sign/sign.py index 2f605e57..c579dedf 100644 --- a/examples/extensions/plugins/sign/sign.py +++ b/examples/extensions/plugins/sign/sign.py @@ -26,6 +26,7 @@ def _run_command(command): + ConanOutput().info(f"Running command: {' '.join(command)}") result = subprocess.run( command, stdout=subprocess.PIPE, @@ -77,18 +78,23 @@ def verify(ref, artifacts_folder, signature_folder, files, **kwargs): # The provider is useful to choose the correct public key to verify packages with provider = summary.get("provider") if provider != "conan-client": - return f"Warn: The provider does not match (conan-client [expected] != {provider} [actual])" - - # openssl dgst -sha256 -verify public_key.pem -signature document.sig document.txt - openssl_verify_cmd = [ - "openssl", - "dgst", - "-sha256", - "-verify", pubkey_filepath, - "-signature", signature_filepath, - summary_filepath, - ] - try: - _run_command(openssl_verify_cmd) - except Exception as exc: - raise ConanException(f"Error verifying signature {signature_filepath}: {exc}") + raise ConanException(f"The provider does not match (conan-client [expected] != {provider} [actual])." + f"Cannot get a public key to verify the package") + + method = summary.get("method") + if method == "openssl-dgst": + # openssl dgst -sha256 -verify public_key.pem -signature document.sig document.txt + openssl_verify_cmd = [ + "openssl", + "dgst", + "-sha256", + "-verify", pubkey_filepath, + "-signature", signature_filepath, + summary_filepath, + ] + try: + _run_command(openssl_verify_cmd) + except Exception as exc: + raise ConanException(f"Error verifying signature {signature_filepath}: {exc}") + else: + raise ConanException(f"Sign method {method} not supported. Cannot verify package") From 03e91864c03537423c099ff0f794f00de1d4b8b1 Mon Sep 17 00:00:00 2001 From: danimtb Date: Tue, 16 Dec 2025 17:34:44 +0100 Subject: [PATCH 5/5] update plugin example --- examples/extensions/plugins/sign/sign.py | 58 ++++++++++--------- .../libcurl/download_image/ci_test_example.py | 2 +- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/examples/extensions/plugins/sign/sign.py b/examples/extensions/plugins/sign/sign.py index c579dedf..0e72fe14 100644 --- a/examples/extensions/plugins/sign/sign.py +++ b/examples/extensions/plugins/sign/sign.py @@ -12,7 +12,8 @@ $ openssl pkey -in private_key.pem -pubout -out public_key.pem -The private_key.pem and public_key.pem files should be placed next to this plugins's file sign.py +The private_key.pem and public_key.pem files should be placed inside a folder named withe the provider's name +('conan-client' for this example). The conan-client folder should be next to this plugins's file sign.py (inside the CONAN_HOME/extensions/plugins/sing folder). """ @@ -21,8 +22,8 @@ import subprocess from conan.api.output import ConanOutput from conan.errors import ConanException -from conan.tools.pkg_signing.plugin import (create_summary_content, get_summary_file_path, - load_summary, save_summary) +from conan.tools.pkg_signing.plugin import (get_manifest_filepath, load_manifest, + load_signatures, verify_files_checksums) def _run_command(command): @@ -42,47 +43,52 @@ def _run_command(command): def sign(ref, artifacts_folder, signature_folder, **kwargs): - c = create_summary_content(artifacts_folder) - c["method"] = "openssl-dgst" - c["provider"] = "conan-client" - save_summary(signature_folder, c) - - # openssl dgst -sha256 -sign private_key.pem -out document.sig document.txt - summary_filepath = get_summary_file_path(signature_folder) - signature_filepath = f"{summary_filepath}.sig" + provider = "conan-client" # This maps to the folder containing the signing keys (for simplicity) + manifest_filepath = get_manifest_filepath(signature_folder) + signature_filename = "pkgsign-manifest.json.sig" + signature_filepath = os.path.join(signature_folder, signature_filename) if os.path.isfile(signature_filepath): ConanOutput().warning(f"Package {ref.repr_notime()} was already signed") - privkey_filepath = os.path.join(os.path.dirname(__file__), "private_key.pem") + + privkey_filepath = os.path.join(os.path.dirname(__file__), provider, "private_key.pem") + # openssl dgst -sha256 -sign private_key.pem -out document.sig document.txt openssl_sign_cmd = [ "openssl", "dgst", "-sha256", "-sign", privkey_filepath, "-out", signature_filepath, - summary_filepath, + manifest_filepath ] try: _run_command(openssl_sign_cmd) except Exception as exc: raise ConanException(f"Error signing artifact {summary_filepath}: {exc}") + return [{"method": "openssl-dgst", + "provider": provider, + "sign_artifacts": {"signature": signature_filename}}] def verify(ref, artifacts_folder, signature_folder, files, **kwargs): - summary_filepath = get_summary_file_path(signature_folder) - signature_filepath = f"{summary_filepath}.sig" - pubkey_filepath = os.path.join(os.path.dirname(__file__), "public_key.pem") + verify_files_checksums(signature_folder, files) + + signature = load_signatures(signature_folder).get("signatures")[0] + signature_filename = signature.get("sign_artifacts").get("signature") + signature_filepath = os.path.join(signature_folder, signature_filename) if not os.path.isfile(signature_filepath): raise ConanException("Signature file does not exist") - summary = load_summary(signature_folder) # The provider is useful to choose the correct public key to verify packages with - provider = summary.get("provider") - if provider != "conan-client": - raise ConanException(f"The provider does not match (conan-client [expected] != {provider} [actual])." - f"Cannot get a public key to verify the package") - - method = summary.get("method") - if method == "openssl-dgst": + expected_provider = "conan-client" + signature_provider = signature.get("provider") + if signature_provider != expected_provider: + raise ConanException(f"The provider does not match ({expected_provider} [expected] != {signature_provider} " + "[actual]). Cannot get a public key to verify the package") + pubkey_filepath = os.path.join(os.path.dirname(__file__), "conan-client", "public_key.pem") + + manifest_filepath = get_manifest_filepath(signature_folder) + signature_method = signature.get("method") + if signature_method == "openssl-dgst": # openssl dgst -sha256 -verify public_key.pem -signature document.sig document.txt openssl_verify_cmd = [ "openssl", @@ -90,11 +96,11 @@ def verify(ref, artifacts_folder, signature_folder, files, **kwargs): "-sha256", "-verify", pubkey_filepath, "-signature", signature_filepath, - summary_filepath, + manifest_filepath, ] try: _run_command(openssl_verify_cmd) except Exception as exc: raise ConanException(f"Error verifying signature {signature_filepath}: {exc}") else: - raise ConanException(f"Sign method {method} not supported. Cannot verify package") + raise ConanException(f"Sign method {signature_method} not supported. Cannot verify package") diff --git a/examples/libraries/libcurl/download_image/ci_test_example.py b/examples/libraries/libcurl/download_image/ci_test_example.py index e357638a..cc17ae1d 100644 --- a/examples/libraries/libcurl/download_image/ci_test_example.py +++ b/examples/libraries/libcurl/download_image/ci_test_example.py @@ -3,7 +3,7 @@ print("libcurl and stb example") -# not using a conanfile because that will be created by the CLion plugin, in case someone just wants to +# not using a conanfile because that will be created by the CLion plugins, in case someone just wants to # copy this code to its folder so that the user does not find any conflicting file run("conan install --requires=stb/cci.20240531 --build=missing -g CMakeDeps -g CMakeToolchain --output-folder=build") run("conan install --requires=libcurl/8.12.1 --build=missing -g CMakeDeps -g CMakeToolchain --output-folder=build")