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..f4debdf5 --- /dev/null +++ b/examples/extensions/plugins/sign/readme.md @@ -0,0 +1,17 @@ + +## 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/sign.py```. +- Generate your signing keys (see comment at the top of sign.py) and place them next to the ``sign.py`` file. +- 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. +- 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..0e72fe14 --- /dev/null +++ b/examples/extensions/plugins/sign/sign.py @@ -0,0 +1,106 @@ +""" +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 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). +""" + + +import os +import subprocess +from conan.api.output import ConanOutput +from conan.errors import ConanException +from conan.tools.pkg_signing.plugin import (get_manifest_filepath, load_manifest, + load_signatures, verify_files_checksums) + + +def _run_command(command): + ConanOutput().info(f"Running command: {' '.join(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): + 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__), 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, + 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): + 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") + + # The provider is useful to choose the correct public key to verify packages with + 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", + "dgst", + "-sha256", + "-verify", pubkey_filepath, + "-signature", signature_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 {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")