diff --git a/examples/extensions.rst b/examples/extensions.rst index cb0dbb772c7e..756ba0bc20bf 100644 --- a/examples/extensions.rst +++ b/examples/extensions.rst @@ -14,3 +14,4 @@ Conan extensions examples extensions/commands/custom_commands extensions/deployers/builtin_deployers extensions/deployers/custom_deployers + extensions/package_signing/package_signing diff --git a/examples/extensions/package_signing/package_signing.rst b/examples/extensions/package_signing/package_signing.rst new file mode 100644 index 000000000000..dd0c45c8ac79 --- /dev/null +++ b/examples/extensions/package_signing/package_signing.rst @@ -0,0 +1,12 @@ +.. _examples_extensions_package_signing: + + +Package Signing Plugin +====================== + + +.. toctree:: + :maxdepth: 2 + + + package_signing_with_openssl \ No newline at end of file diff --git a/examples/extensions/package_signing/package_signing_with_openssl.rst b/examples/extensions/package_signing/package_signing_with_openssl.rst new file mode 100644 index 000000000000..42328673297b --- /dev/null +++ b/examples/extensions/package_signing/package_signing_with_openssl.rst @@ -0,0 +1,253 @@ +.. _examples_extensions_package_signing_openssl: + +Signing packages with OpenSSL +============================= + +This is an example of a Package Signing Plugin implementation using the `OpensSSL digest tool `_. +You will need to have ``openssl`` installed at the system level and available in your ``PATH``. + +.. include:: ../../../common/experimental_warning.inc + +This example is available in the examples2 repository: `examples/extensions/plugins/openssl_sign `_. + +.. note:: + + OpenSSL is used here for demonstration purposes only. The Package Signing + plugin mechanism is backend-agnostic, and you could implement a similar + plugin using other tools available in your system (for example, ``gpg``), + with minimal changes to the signing and verification commands. + +Generating the signing keys ++++++++++++++++++++++++++++ + +To sign and verify the packages using the plugin, first, we will need a public and private key. + +To generate the keys using the ``openssl`` executable, we can run: + +.. code-block:: bash + + $ openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 + +This will generate the private key used to sign the packages. + +Now, we can get the public key from it with this command: + +.. code-block:: bash + + $ openssl pkey -in private_key.pem -pubout -out public_key.pem + +The plugin will use this public key to verify the packages. + + +Configuring the plugin +++++++++++++++++++++++ + +.. caution:: + + This example stores a private key next to the plugin for simplicity. **Do not do this in production**. + Instead, load the signing key from environment variables or a secret manager, or delegate signing to a remote signing service. + **Always keep the private key out of the Conan cache and out of source control**. + +1. Copy the ``examples/extensions/plugins/openssl_sign/sign.py`` file to your Conan home at ``CONAN_HOME/extensions/plugins/sign/sign.py``. + +1. Copy the ``sign.py`` file to your Conan home: + + ``CONAN_HOME/extensions/plugins/sign/sign.py`` + +2. Place the generated keys in a folder named after your provider + (``my-organization`` in this example), next to ``sign.py``: + +Your final folder structure should look like this: + +.. code-block:: text + + CONAN_HOME/ + └── extensions/ + └── plugins/ + └── sign/ + ├── sign.py + └── my-organization/ + ├── private_key.pem + └── public_key.pem + +The ``my-organization`` folder serves as the **provider** in this example, and it is used by the plugin to identify the organization that owns the keys. + +.. tip:: + + The Package Signing plugin is installed in the Conan configuration folder, so they can be easily distributed as part of the client + configuration using the :ref:`conan config install` command. + + +Implementation +++++++++++++++ + +The plugin's implementation is very straightforward. + +For signing packages, the `sign()` function is defined, where the packages are signed by the :command:`openssl dgst` command: + +.. code-block:: python + + def sign(ref, artifacts_folder, signature_folder, **kwargs) + ... + openssl_sign_cmd = [ + "openssl", + "dgst", + "-sha256", + "-sign", privkey_filepath, + "-out", signature_filepath, + manifest_filepath + ] + try: + _run_command(openssl_sign_cmd) + ConanOutput().success(f"Package signed for reference {ref}") + except Exception as exc: + raise ConanException(f"Error signing artifact: {exc}") + ... + +There, the manifest ``pkgsign-manifest.json`` (created right before ``sign()`` function is called) is used to sign the package, +as it contains the filenames and checksums of the artifacts of the package. + +The signature file is saved into the ``signature_filepath`` (the signature folder at ``/metadata/sign``), and finally, the +metadata of the signature is returned as a dictionary in a list: + +.. code-block:: python + + def sign(ref, artifacts_folder, signature_folder, **kwargs) + ... + return [{"method": "openssl-dgst", + "provider": "my-organization", + "sign_artifacts": { + "manifest": "pkgsign-manifest.json", + "signature": signature_filename}}] + +This information saved in a file ``pkgsign-signatures.json`` placed in the signature folder, so it can be used in the `verify()` to +verify the package signature against the correct provider keys, with the correct signing method (``openssl-dgst`` for this example) +and using the signature files in ``sign_artifacts``. + +For verifying packages, the `verify()` function is defined. + +First, the ``pkgsign-signatures.json`` is loaded to read the metadata of the signatures (multiple signatures are supported): + +.. code-block:: python + + def verify(ref, artifacts_folder, signature_folder, files, **kwargs): + ... + signatures = json.loads(f.read()).get("signatures") + ... + for signature in signatures: + signature_filename = signature.get("sign_artifacts").get("signature") + signature_filepath = os.path.join(signature_folder, signature_filename) + ... + provider = signature.get("provider") + signature_method = signature.get("method") + ... + +Then, the ``provider`` information is used to select the correct public key for verification that use the right signature verification +``method`` (``openssl-dgst`` for this example) and run the :command:`openssl dgst -verify` command: + +.. code-block:: python + + def verify(ref, artifacts_folder, signature_folder, files, **kwargs): + ... + openssl_verify_cmd = [ + "openssl", + "dgst", + "-sha256", + "-verify", pubkey_filepath, + "-signature", signature_filepath, + manifest_filepath, + ] + try: + _run_command(openssl_verify_cmd) + ConanOutput().success(f"Package verified for reference {ref}") + except Exception as exc: + raise ConanException(f"Error verifying signature {signature_filepath}: {exc}") + +The ``verify()`` function does not return any value in case the package is correct. If the verification fails, then a ``ConanException()`` should be raised. + +Signing packages +++++++++++++++++ + +Now that the plugin is configured, we can create a package and sign it afterwards: + +.. code-block:: bash + + $ conan new cmake_lib -d name=hello -d version=1.0 + $ conan create + +For signing the recipe and package, use the dedicated command: + +.. code-block:: bash + + $ conan cache sign hello/1.0 + + hello/1.0: Compressing conan_sources.tgz + hello/1.0:dee9f7f985eb1c20e3c41afaa8c35e2a34b5ae0b: Compressing conan_package.tgz + Running command: openssl dgst -sha256 -sign C:\Users\user\.conan2\extensions\plugins\sign\my-organization\private_key.pem -out C:\Users\user\.conan2\p\hello092ffa809a9a1\d\metadata\sign\pkgsign-manifest.json.sig C:\Users\user\.conan2\p\hello092ffa809a9a1\d\metadata\sign\pkgsign-manifest.json + Package signed for reference hello/1.0 + Running command: openssl dgst -sha256 -sign C:\Users\user\.conan2\extensions\plugins\sign\my-organization\private_key.pem -out C:\Users\user\.conan2\p\b\hello5b13c694fef4a\d\metadata\sign\pkgsign-manifest.json.sig C:\Users\user\.conan2\p\b\hello5b13c694fef4a\d\metadata\sign\pkgsign-manifest.json + Package signed for reference hello/1.0:dee9f7f985eb1c20e3c41afaa8c35e2a34b5ae0b + [Package sign] Results: + + hello/1.0 + revisions + 53321bba8793db6fea5ea1a98dd6f3d6 + packages + dee9f7f985eb1c20e3c41afaa8c35e2a34b5ae0b + revisions + 4b1eaf2e27996cb39cb3774f185fcd8e + + [Package sign] Summary: OK=2, FAILED=0 + +As you see, the command is executing the ``sign()`` function of the plugin that uses the ``openssl`` executable to sign the recipe and the package with a command similar to: + +.. code-block:: bash + + $ openssl dgst -sha256 -sign private_key.pem -out pkgsign-manifest.json.sig pkgsign-manifest.json + +And it is also using the conan-generated ``pkgsign-manifest.json`` file to create the signature. +You can read more about this manifest file at :ref:`reference_extensions_package_signing`. + + +Verifying packages +++++++++++++++++++ + +For verifying the recipe and package, use the dedicated command: + +.. code-block:: bash + + $ conan cache verify hello/1.0 + + [Package sign] Checksum verified for file conan_sources.tgz (4ce077cbea9ce87a481b5d6dbb50bd791f4e37e931754cdeb40aeb017baed66c). + [Package sign] Checksum verified for file conanfile.py (0ec44c268f0f255ab59a246c3d13ae6dbd487dea7635b584236b701047f92ba0). + [Package sign] Checksum verified for file conanmanifest.txt (f7f00bb74ed8469a367ed02faded3c763130da9b63dae23916b2a4f099625b15). + Running command: openssl dgst -sha256 -verify C:\Users\user\.conan2\extensions\plugins\sign\my-organization\public_key.pem -signature C:\Users\user\.conan2\p\hello092ffa809a9a1\d\metadata\sign\pkgsign-manifest.json.sig C:\Users\user\.conan2\p\hello092ffa809a9a1\d\metadata\sign\pkgsign-manifest.json + Package verified for reference hello/1.0 + [Package sign] Checksum verified for file conan_package.tgz (5cc1b9e330fe5bb6ad5904db45d78ecd6bdc71bcc18eff8d19a1ed126ba5a5aa). + [Package sign] Checksum verified for file conaninfo.txt (f80367b17176346e10640ed813d6d2f1c45ed526822ff71066696179d16e2f2f). + [Package sign] Checksum verified for file conanmanifest.txt (91429ce32c2d0a99de6459a589ac9c35933ed65165ee5c564b6534da57fdfa65). + Running command: openssl dgst -sha256 -verify C:\Users\user\.conan2\extensions\plugins\sign\my-organization\public_key.pem -signature C:\Users\user\.conan2\p\b\hello5b13c694fef4a\d\metadata\sign\pkgsign-manifest.json.sig C:\Users\user\.conan2\p\b\hello5b13c694fef4a\d\metadata\sign\pkgsign-manifest.json + Package verified for reference hello/1.0:dee9f7f985eb1c20e3c41afaa8c35e2a34b5ae0b + [Package sign] Results: + + hello/1.0 + revisions + 53321bba8793db6fea5ea1a98dd6f3d6 + packages + dee9f7f985eb1c20e3c41afaa8c35e2a34b5ae0b + revisions + 4b1eaf2e27996cb39cb3774f185fcd8e + + [Package sign] Summary: OK=2, FAILED=0 + +As you see, Conan is performing an internal checksum verification for the files and calling the ``verify()`` function of the plugin that uses +the ``openssl`` executable to verify the recipe and the package with a command similar to: + +.. code-block:: bash + + $ openssl dgst -sha256 -verify public_key.pem -signature pkgsign-manifest.json.sig pkgsign-manifest.json + +.. seealso:: + + If you want to create your own package signing plugin, check the reference documentation at + :ref:`reference_extensions_package_signing`. diff --git a/reference/extensions/package_signing.rst b/reference/extensions/package_signing.rst index 334fab5ce378..ab491db216f8 100644 --- a/reference/extensions/package_signing.rst +++ b/reference/extensions/package_signing.rst @@ -211,3 +211,10 @@ Here is a usual flow for signing and verifying packages: The :command:`conan upload` command **will not automatically sign** the packages since Conan 2.26.0. Please make sure to use the :command:`conan cache sign` command to **sign the packages before uploading them**, and **update your plugin** to conform to the new implementation. + +Plugin implementation examples +============================== + +Here you can find some implementation examples of the plugin so they can serve as guidance to develop your own: + +- :ref:`examples_extensions_package_signing_openssl`.