From 2af35f906df5c61a10de23a1dff61ddeecbe5776 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 14 Dec 2022 18:39:43 +0100 Subject: [PATCH 1/4] signer: move GPGSigner to _gpg_signer.py Signed-off-by: Lukas Puehringer --- securesystemslib/signer/__init__.py | 2 +- securesystemslib/signer/_gpg_signer.py | 66 ++++++++++++++++++++++++++ securesystemslib/signer/_signer.py | 62 +----------------------- 3 files changed, 68 insertions(+), 62 deletions(-) create mode 100644 securesystemslib/signer/_gpg_signer.py diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index 0b065074..2cac6674 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -5,12 +5,12 @@ Some implementations are provided by default but more can be added by users. """ from securesystemslib.signer._gcp_signer import GCPSigner +from securesystemslib.signer._gpg_signer import GPGSigner from securesystemslib.signer._hsm_signer import HSMSigner from securesystemslib.signer._key import KEY_FOR_TYPE_AND_SCHEME, Key, SSlibKey from securesystemslib.signer._signature import GPGSignature, Signature from securesystemslib.signer._signer import ( SIGNER_FOR_URI_SCHEME, - GPGSigner, SecretsHandler, Signer, SSlibSigner, diff --git a/securesystemslib/signer/_gpg_signer.py b/securesystemslib/signer/_gpg_signer.py new file mode 100644 index 00000000..84ae3736 --- /dev/null +++ b/securesystemslib/signer/_gpg_signer.py @@ -0,0 +1,66 @@ +"""Signer implementation for OpenPGP """ +from typing import Optional + +import securesystemslib.gpg.functions as gpg +from securesystemslib.signer._key import Key +from securesystemslib.signer._signature import GPGSignature +from securesystemslib.signer._signer import Key, SecretsHandler, Signer + + +class GPGSigner(Signer): + """A securesystemslib gpg implementation of the "Signer" interface. + + Provides a sign method to generate a cryptographic signature with gpg, using + an RSA, DSA or EdDSA private key identified by the keyid on the instance. + """ + + def __init__( + self, keyid: Optional[str] = None, homedir: Optional[str] = None + ): + self.keyid = keyid + self.homedir = homedir + + @classmethod + def from_priv_key_uri( + cls, + priv_key_uri: str, + public_key: Key, + secrets_handler: Optional[SecretsHandler] = None, + ) -> "GPGSigner": + raise NotImplementedError("Incompatible with private key URIs") + + def sign(self, payload: bytes) -> GPGSignature: + """Signs a given payload by the key assigned to the GPGSigner instance. + + Calls the gpg command line utility to sign the passed content with the + key identified by the passed keyid from the gpg keyring at the passed + homedir. + + The executed base command is defined in + securesystemslib.gpg.constants.gpg_sign_command. + + Arguments: + payload: The bytes to be signed. + + Raises: + securesystemslib.exceptions.FormatError: + If the keyid was passed and does not match + securesystemslib.formats.KEYID_SCHEMA. + + ValueError: the gpg command failed to create a valid signature. + OSError: the gpg command is not present or non-executable. + securesystemslib.exceptions.UnsupportedLibraryError: the gpg + command is not available, or the cryptography library is + not installed. + securesystemslib.gpg.exceptions.CommandError: the gpg command + returned a non-zero exit code. + securesystemslib.gpg.exceptions.KeyNotFoundError: the used gpg + version is not fully supported and no public key can be found + for short keyid. + + Returns: + Returns a "GPGSignature" class instance. + """ + + sig_dict = gpg.create_signature(payload, self.keyid, self.homedir) + return GPGSignature(**sig_dict) diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index 42a82819..6a767ed9 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -6,10 +6,9 @@ from typing import Callable, Dict, Optional, Type from urllib import parse -import securesystemslib.gpg.functions as gpg import securesystemslib.keys as sslib_keys from securesystemslib.signer._key import Key, SSlibKey -from securesystemslib.signer._signature import GPGSignature, Signature +from securesystemslib.signer._signature import Signature logger = logging.getLogger(__name__) @@ -212,62 +211,3 @@ def sign(self, payload: bytes) -> Signature: """ sig_dict = sslib_keys.create_signature(self.key_dict, payload) return Signature(**sig_dict) - - -class GPGSigner(Signer): - """A securesystemslib gpg implementation of the "Signer" interface. - - Provides a sign method to generate a cryptographic signature with gpg, using - an RSA, DSA or EdDSA private key identified by the keyid on the instance. - """ - - def __init__( - self, keyid: Optional[str] = None, homedir: Optional[str] = None - ): - self.keyid = keyid - self.homedir = homedir - - @classmethod - def from_priv_key_uri( - cls, - priv_key_uri: str, - public_key: Key, - secrets_handler: Optional[SecretsHandler] = None, - ) -> "GPGSigner": - raise NotImplementedError("Incompatible with private key URIs") - - def sign(self, payload: bytes) -> GPGSignature: - """Signs a given payload by the key assigned to the GPGSigner instance. - - Calls the gpg command line utility to sign the passed content with the - key identified by the passed keyid from the gpg keyring at the passed - homedir. - - The executed base command is defined in - securesystemslib.gpg.constants.gpg_sign_command. - - Arguments: - payload: The bytes to be signed. - - Raises: - securesystemslib.exceptions.FormatError: - If the keyid was passed and does not match - securesystemslib.formats.KEYID_SCHEMA. - - ValueError: the gpg command failed to create a valid signature. - OSError: the gpg command is not present or non-executable. - securesystemslib.exceptions.UnsupportedLibraryError: the gpg - command is not available, or the cryptography library is - not installed. - securesystemslib.gpg.exceptions.CommandError: the gpg command - returned a non-zero exit code. - securesystemslib.gpg.exceptions.KeyNotFoundError: the used gpg - version is not fully supported and no public key can be found - for short keyid. - - Returns: - Returns a "GPGSignature" class instance. - """ - - sig_dict = gpg.create_signature(payload, self.keyid, self.homedir) - return GPGSignature(**sig_dict) From d70a244b4433bc9908bc613b578867b1948279ac Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 14 Dec 2022 18:48:56 +0100 Subject: [PATCH 2/4] signer: make GPGSigner.sign return Signature Adds two private conversion helpers to translate from the old signature format to Signature and vice-versa, i.e. - change "signature" field name to "sig", - list additional gpg signature field "other_headers" under Signature's "unrecognized_fields" The helpers are used in GPGSigner.sign and (in a follow-up PR) in GPGKey.verify_signature. Also removes the no longer needed GPGSignature class. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/__init__.py | 2 +- securesystemslib/signer/_gpg_signer.py | 28 ++++++++++---- securesystemslib/signer/_signature.py | 52 -------------------------- tests/test_signer.py | 21 ++--------- 4 files changed, 25 insertions(+), 78 deletions(-) diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index 2cac6674..d5c816d1 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -8,7 +8,7 @@ from securesystemslib.signer._gpg_signer import GPGSigner from securesystemslib.signer._hsm_signer import HSMSigner from securesystemslib.signer._key import KEY_FOR_TYPE_AND_SCHEME, Key, SSlibKey -from securesystemslib.signer._signature import GPGSignature, Signature +from securesystemslib.signer._signature import Signature from securesystemslib.signer._signer import ( SIGNER_FOR_URI_SCHEME, SecretsHandler, diff --git a/securesystemslib/signer/_gpg_signer.py b/securesystemslib/signer/_gpg_signer.py index 84ae3736..8e89b6b3 100644 --- a/securesystemslib/signer/_gpg_signer.py +++ b/securesystemslib/signer/_gpg_signer.py @@ -1,10 +1,9 @@ """Signer implementation for OpenPGP """ -from typing import Optional +from typing import Dict, Optional import securesystemslib.gpg.functions as gpg from securesystemslib.signer._key import Key -from securesystemslib.signer._signature import GPGSignature -from securesystemslib.signer._signer import Key, SecretsHandler, Signer +from securesystemslib.signer._signer import SecretsHandler, Signature, Signer class GPGSigner(Signer): @@ -29,7 +28,20 @@ def from_priv_key_uri( ) -> "GPGSigner": raise NotImplementedError("Incompatible with private key URIs") - def sign(self, payload: bytes) -> GPGSignature: + @staticmethod + def _to_gpg_sig(sig: Signature) -> Dict: + """Helper to convert Signature -> internal gpg signature format.""" + sig_dict = sig.to_dict() + sig_dict["signature"] = sig_dict.pop("sig") + return sig_dict + + @staticmethod + def _from_gpg_sig(sig_dict: Dict) -> Signature: + """Helper to convert internal gpg signature format -> Signature.""" + sig_dict["sig"] = sig_dict.pop("signature") + return Signature.from_dict(sig_dict) + + def sign(self, payload: bytes) -> Signature: """Signs a given payload by the key assigned to the GPGSigner instance. Calls the gpg command line utility to sign the passed content with the @@ -59,8 +71,8 @@ def sign(self, payload: bytes) -> GPGSignature: for short keyid. Returns: - Returns a "GPGSignature" class instance. + Signature. """ - - sig_dict = gpg.create_signature(payload, self.keyid, self.homedir) - return GPGSignature(**sig_dict) + return self._from_gpg_sig( + gpg.create_signature(payload, self.keyid, self.homedir) + ) diff --git a/securesystemslib/signer/_signature.py b/securesystemslib/signer/_signature.py index 190afc3c..1869125d 100644 --- a/securesystemslib/signer/_signature.py +++ b/securesystemslib/signer/_signature.py @@ -81,55 +81,3 @@ def to_dict(self) -> Dict: "sig": self.signature, **self.unrecognized_fields, } - - -class GPGSignature(Signature): - """A container class containing information about a gpg signature. - - Besides the signature, it also contains other meta information - needed to uniquely identify the key used to generate the signature. - - Attributes: - keyid: HEX string used as a unique identifier of the key. - signature: HEX string representing the signature. - other_headers: HEX representation of additional GPG headers. - """ - - def __init__( - self, - keyid: str, - signature: str, - other_headers: str, - ): - super().__init__(keyid, signature) - self.other_headers = other_headers - - @classmethod - def from_dict(cls, signature_dict: Dict) -> "GPGSignature": - """Creates a GPGSignature object from its JSON/dict representation. - - Args: - signature_dict: Dict containing valid "keyid", "signature" and - "other_fields" fields. - - Raises: - KeyError: If any of the "keyid", "sig" or "other_headers" fields - are missing from the signature_dict. - - Returns: - GPGSignature instance. - """ - - return cls( - signature_dict["keyid"], - signature_dict["signature"], - signature_dict["other_headers"], - ) - - def to_dict(self) -> Dict: - """Returns the JSON-serializable dictionary representation of self.""" - return { - "keyid": self.keyid, - "signature": self.signature, - "other_headers": self.other_headers, - } diff --git a/tests/test_signer.py b/tests/test_signer.py index 5224858b..7218198b 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -22,7 +22,6 @@ from securesystemslib.signer import ( KEY_FOR_TYPE_AND_SCHEME, SIGNER_FOR_URI_SCHEME, - GPGSignature, GPGSigner, Key, SecretsHandler, @@ -398,11 +397,11 @@ def tearDownClass(cls): def test_gpg_sign_and_verify_object_with_default_key(self): """Create a signature using the default key on the keyring.""" - + # pylint: disable=protected-access signer = GPGSigner(homedir=self.gnupg_home) signature = signer.sign(self.test_data) - signature_dict = signature.to_dict() + signature_dict = GPGSigner._to_gpg_sig(signature) key_data = export_pubkey(self.default_keyid, self.gnupg_home) self.assertTrue(verify_sig(signature_dict, key_data, self.test_data)) @@ -410,28 +409,16 @@ def test_gpg_sign_and_verify_object_with_default_key(self): def test_gpg_sign_and_verify_object(self): """Create a signature using a specific key on the keyring.""" - + # pylint: disable=protected-access signer = GPGSigner(self.signing_subkey_keyid, self.gnupg_home) signature = signer.sign(self.test_data) - signature_dict = signature.to_dict() + signature_dict = GPGSigner._to_gpg_sig(signature) key_data = export_pubkey(self.signing_subkey_keyid, self.gnupg_home) self.assertTrue(verify_sig(signature_dict, key_data, self.test_data)) self.assertFalse(verify_sig(signature_dict, key_data, self.wrong_data)) - def test_gpg_serialization(self): - """Tests from_dict and to_dict methods of GPGSignature.""" - - sig_dict = { - "keyid": "f4f90403af58eef6", - "signature": "c39f86e70e12e70e11d87eb7e3ab7d3b", - "other_headers": "d8f8a89b5d71f07b842a", - } - - signature = GPGSignature.from_dict(sig_dict) - self.assertEqual(sig_dict, signature.to_dict()) - # Run the unit tests. if __name__ == "__main__": From 2a22f9f3d07ae7def1d3a8fff0dbb8e6a25d41b7 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 14 Dec 2022 19:37:57 +0100 Subject: [PATCH 3/4] signer: polish GPGSigner docs Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_gpg_signer.py | 41 ++++++++++++-------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/securesystemslib/signer/_gpg_signer.py b/securesystemslib/signer/_gpg_signer.py index 8e89b6b3..6d48b0ee 100644 --- a/securesystemslib/signer/_gpg_signer.py +++ b/securesystemslib/signer/_gpg_signer.py @@ -7,10 +7,19 @@ class GPGSigner(Signer): - """A securesystemslib gpg implementation of the "Signer" interface. + """OpenPGP Signer + + Runs command in ``GNUPG`` environment variable to sign, fallback commands are + ``gpg2`` and ``gpg``. + + Supported signing schemes are: "pgp+rsa-pkcsv1.5", "pgp+dsa-fips-180-2" and + "pgp+eddsa-ed25519", with SHA-256 hashing. + + + Arguments: + keyid: GnuPG local user signing key id. If not passed, the default key is used. + homedir: GnuPG home directory path. If not passed, the default homedir is used. - Provides a sign method to generate a cryptographic signature with gpg, using - an RSA, DSA or EdDSA private key identified by the keyid on the instance. """ def __init__( @@ -42,33 +51,21 @@ def _from_gpg_sig(sig_dict: Dict) -> Signature: return Signature.from_dict(sig_dict) def sign(self, payload: bytes) -> Signature: - """Signs a given payload by the key assigned to the GPGSigner instance. - - Calls the gpg command line utility to sign the passed content with the - key identified by the passed keyid from the gpg keyring at the passed - homedir. - - The executed base command is defined in - securesystemslib.gpg.constants.gpg_sign_command. + """Signs payload with ``gpg``. Arguments: - payload: The bytes to be signed. + payload: bytes to be signed. Raises: - securesystemslib.exceptions.FormatError: - If the keyid was passed and does not match - securesystemslib.formats.KEYID_SCHEMA. - - ValueError: the gpg command failed to create a valid signature. + ValueError: The gpg command failed to create a valid signature. OSError: the gpg command is not present or non-executable. - securesystemslib.exceptions.UnsupportedLibraryError: the gpg + securesystemslib.exceptions.UnsupportedLibraryError: The gpg command is not available, or the cryptography library is not installed. - securesystemslib.gpg.exceptions.CommandError: the gpg command + securesystemslib.gpg.exceptions.CommandError: The gpg command returned a non-zero exit code. - securesystemslib.gpg.exceptions.KeyNotFoundError: the used gpg - version is not fully supported and no public key can be found - for short keyid. + securesystemslib.gpg.exceptions.KeyNotFoundError: The used gpg + version is not fully supported. Returns: Signature. From 863ebebd0a96306a7ea3414eb5010327a407d14d Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 11 Jan 2023 12:42:27 +0100 Subject: [PATCH 4/4] signer: add GPG signature data structure test Assert that GPG Signature instances have the correct extra field, and test conversion to and from legacy format. Signed-off-by: Lukas Puehringer --- tests/test_signer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_signer.py b/tests/test_signer.py index 7218198b..c296f495 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -419,6 +419,19 @@ def test_gpg_sign_and_verify_object(self): self.assertTrue(verify_sig(signature_dict, key_data, self.test_data)) self.assertFalse(verify_sig(signature_dict, key_data, self.wrong_data)) + def test_gpg_signature_data_structure(self): + """Test custom fields and legacy data structure in gpg signatures.""" + # pylint: disable=protected-access + signer = GPGSigner(homedir=self.gnupg_home) + sig = signer.sign(self.test_data) + self.assertIn("other_headers", sig.unrecognized_fields) + + sig_dict = GPGSigner._to_gpg_sig(sig) + self.assertIn("signature", sig_dict) + self.assertNotIn("sig", sig_dict) + sig2 = GPGSigner._from_gpg_sig(sig_dict) + self.assertEqual(sig, sig2) + # Run the unit tests. if __name__ == "__main__":