From 6b9dd8597ff55328bf107f402a0f65f859bd31f5 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 30 May 2023 16:43:21 +0200 Subject: [PATCH 01/16] signer: refactor SSlibSigner This patch refactors the SSlibSigner signing implementation, akin to #585, which refactored signature verification (see PR for details about legacy code and rationale). Unlike, #585 this patch does not only condense and move the code for singing, but creates a new hierarchy of signers to achieve two additional goals: 1. Provide tiny rsa, ecdsa and ed25519 pyca/cryptography Signer implementations, which are independent of private key serialization formats, above all, of the proprietary legacy keydict format. This is particularly interesting, when refactoring existing or designing new key generation or import interfaces, where it would be annoying to move back and forth over the legacy keydict. 2. Preserve SSlibSigner including its internal legacy keydict data structure. SSlibSigner is and remains a backwards-compatibility crutch. Breaking its existing users to make it a little less awkward would defeat that purpose. And even though the Signer API doc says that the internal data structure is not part of the public API, users may rely on it (python-tuf actually does so at least in tests and demos). To achieve these goals, SSlibSigner becomes a container for the newly added CryptoSigner class, whose implementations can also be used as independent Signers, and above all created or imported, with very few lines of pyca/cryptography code. **Caveat:** Latest python-tuf tests pass against this patch, except for one, which expects a keydict deserialization failure in `sign`, which now happens in `__init__` initialization time. This seems feasible to fix in python-tuf. Also note that private key format errors are now ValueErrors and no longer unreliably either FormatErrors or sometimes UnsupportedAlgorithmErrors. **Future work (will ticketize):** - Signing schemes strings should not be hardcoded all over the place but defined once in constants for all of securesystemslib. - There is some duplicate code for scheme string dissection and algorithm selection, which could be unified for all signers and public keys. - (bonus) #585 considered creating separate RSAKey, ECDSAKey, ED25519Key classes, but ended up putting everything into SSlibKey. Now that we have separate signers for each of these key types, which have a field for the corresponding public key object, it might make sense to reconsider this separation. This would give us a more robust data model, where e.g. allowed signing schemes are only validated once in the public key constructor and are thus validated implicitly in the signer constructor. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_signer.py | 183 ++++++++++++++++++++++++++++- tests/test_dsse.py | 11 +- tests/test_signer.py | 31 ++--- 3 files changed, 194 insertions(+), 31 deletions(-) diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index a61d3ad9..5229fc04 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -3,15 +3,49 @@ import logging import os from abc import ABCMeta, abstractmethod -from typing import Any, Callable, Dict, Optional, Type +from typing import Any, Callable, Dict, Optional, Type, cast from urllib import parse import securesystemslib.keys as sslib_keys +from securesystemslib.exceptions import UnsupportedLibraryError from securesystemslib.formats import encode_canonical from securesystemslib.hash import digest from securesystemslib.signer._key import Key, SSlibKey from securesystemslib.signer._signature import Signature +CRYPTO_IMPORT_ERROR = None +try: + from cryptography.hazmat.primitives.asymmetric.ec import ( + ECDSA, + EllipticCurvePrivateKey, + ) + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + ) + from cryptography.hazmat.primitives.asymmetric.padding import ( + MGF1, + PSS, + AsymmetricPadding, + PKCS1v15, + ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + AsymmetricPadding, + RSAPrivateKey, + ) + from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes + from cryptography.hazmat.primitives.hashes import ( + SHA224, + SHA256, + SHA384, + SHA512, + HashAlgorithm, + ) + from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, + ) +except ImportError: + CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required" + logger = logging.getLogger(__name__) # NOTE Signer dispatch table is defined here so it's usable by Signer, @@ -164,6 +198,7 @@ class SSlibSigner(Signer): def __init__(self, key_dict: Dict): self.key_dict = key_dict + self._crypto_signer = CryptoSigner.from_securesystemslib_key(key_dict) @classmethod def from_priv_key_uri( @@ -212,6 +247,7 @@ def from_priv_key_uri( keydict = public_key.to_securesystemslib_key() keydict["keyval"]["private"] = private + return cls(keydict) def sign(self, payload: bytes) -> Signature: @@ -225,5 +261,146 @@ def sign(self, payload: bytes) -> Signature: securesystemslib.exceptions.UnsupportedAlgorithmError: Signing errors. """ - sig_dict = sslib_keys.create_signature(self.key_dict, payload) - return Signature(**sig_dict) + return self._crypto_signer.sign(payload) + + +class CryptoSigner(Signer, metaclass=ABCMeta): + """Base class for PYCA/cryptography Signer implementations.""" + + def __init__(self, public_key: SSlibKey): + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + self.public_key = public_key + + @classmethod + def from_securesystemslib_key( + cls, key_dict: Dict[str, Any] + ) -> "CryptoSigner": + """Factory to create CryptoSigner from securesystemslib private key dict.""" + private = key_dict["keyval"]["private"] + public_key = SSlibKey.from_securesystemslib_key(key_dict) + + private_key: PrivateKeyTypes + if public_key.keytype == "rsa": + private_key = cast( + RSAPrivateKey, + load_pem_private_key(private.encode(), password=None), + ) + return RSASigner(public_key, private_key) + + if public_key.keytype == "ecdsa": + private_key = cast( + EllipticCurvePrivateKey, + load_pem_private_key(private.encode(), password=None), + ) + return ECDSASigner(public_key, private_key) + + if public_key.keytype == "ed25519": + private_key = Ed25519PrivateKey.from_private_bytes( + bytes.fromhex(private) + ) + return Ed25519Signer(public_key, private_key) + + raise ValueError(f"unsupported keytype: {public_key.keytype}") + + @classmethod + def from_priv_key_uri( + cls, + priv_key_uri: str, + public_key: Key, + secrets_handler: Optional[SecretsHandler] = None, + ) -> "Signer": + # Do not raise NotImplementedError to appease pylint for all subclasses + raise RuntimeError("use SSlibSigner.from_priv_key_uri") + + +class RSASigner(CryptoSigner): + """pyca/cryptography rsa signer implementation""" + + def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"): + if public_key.scheme not in [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ]: + raise ValueError(f"unsupported scheme {public_key.scheme}") + + super().__init__(public_key) + self._private_key = private_key + padding_name, hash_name = public_key.scheme.split("-")[1:] + self._algorithm = self._get_hash_algorithm(hash_name) + self._padding = self._get_rsa_padding(padding_name, self._algorithm) + + @staticmethod + def _get_hash_algorithm(name: str) -> "HashAlgorithm": + """Helper to return hash algorithm for name.""" + algorithm: HashAlgorithm + if name == "sha224": + algorithm = SHA224() + if name == "sha256": + algorithm = SHA256() + if name == "sha384": + algorithm = SHA384() + if name == "sha512": + algorithm = SHA512() + + return algorithm + + @staticmethod + def _get_rsa_padding( + name: str, hash_algorithm: "HashAlgorithm" + ) -> "AsymmetricPadding": + """Helper to return rsa signature padding for name.""" + padding: AsymmetricPadding + if name == "pss": + padding = PSS( + mgf=MGF1(hash_algorithm), salt_length=PSS.DIGEST_LENGTH + ) + + if name == "pkcs1v15": + padding = PKCS1v15() + + return padding + + def sign(self, payload: bytes) -> Signature: + sig = self._private_key.sign(payload, self._padding, self._algorithm) + return Signature(self.public_key.keyid, sig.hex()) + + +class ECDSASigner(CryptoSigner): + """pyca/cryptography ecdsa signer implementation""" + + def __init__( + self, public_key: SSlibKey, private_key: "EllipticCurvePrivateKey" + ): + if public_key.scheme != "ecdsa-sha2-nistp256": + raise ValueError(f"unsupported scheme {public_key.scheme}") + + super().__init__(public_key) + self._private_key = private_key + self._signature_algorithm = ECDSA(SHA256()) + + def sign(self, payload: bytes) -> Signature: + sig = self._private_key.sign(payload, self._signature_algorithm) + return Signature(self.public_key.keyid, sig.hex()) + + +class Ed25519Signer(CryptoSigner): + """pyca/cryptography ecdsa signer implementation""" + + def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"): + if public_key.scheme != "ed25519": + raise ValueError(f"unsupported scheme {public_key.scheme}") + + super().__init__(public_key) + self._private_key = private_key + + def sign(self, payload: bytes) -> Signature: + sig = self._private_key.sign(payload) + return Signature(self.public_key.keyid, sig.hex()) diff --git a/tests/test_dsse.py b/tests/test_dsse.py index ffadeb77..836eb394 100644 --- a/tests/test_dsse.py +++ b/tests/test_dsse.py @@ -5,11 +5,7 @@ import securesystemslib.keys as KEYS from securesystemslib.dsse import Envelope -from securesystemslib.exceptions import ( - FormatError, - UnsupportedAlgorithmError, - VerificationError, -) +from securesystemslib.exceptions import VerificationError from securesystemslib.signer import Signature, SSlibKey, SSlibSigner @@ -96,9 +92,8 @@ def test_sign_and_verify(self): # Test for invalid scheme. valid_scheme = key_dict["scheme"] key_dict["scheme"] = "invalid_scheme" - signer = SSlibSigner(key_dict) - with self.assertRaises((FormatError, UnsupportedAlgorithmError)): - envelope_obj.sign(signer) + with self.assertRaises(ValueError): + signer = SSlibSigner(key_dict) # Sign the payload. key_dict["scheme"] = valid_scheme diff --git a/tests/test_signer.py b/tests/test_signer.py index 942c948b..52dd695f 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -11,7 +11,6 @@ from securesystemslib.exceptions import ( CryptoError, FormatError, - UnsupportedAlgorithmError, UnverifiedSignatureError, VerificationError, ) @@ -397,25 +396,17 @@ def test_sslib_signer_sign(self): ) self.assertTrue(verified, "Incorrect signature.") - # Removing private key from "scheme_dict". - private = scheme_dict["keyval"]["private"] - scheme_dict["keyval"]["private"] = "" - sslib_signer.key_dict = scheme_dict - - with self.assertRaises((ValueError, FormatError)): - sslib_signer.sign(self.DATA) - - scheme_dict["keyval"]["private"] = private - - # Test for invalid signature scheme. - valid_scheme = scheme_dict["scheme"] - scheme_dict["scheme"] = "invalid_scheme" - sslib_signer = SSlibSigner(scheme_dict) - - with self.assertRaises((UnsupportedAlgorithmError, FormatError)): - sslib_signer.sign(self.DATA) - - scheme_dict["scheme"] = valid_scheme + # Assert error for invalid private key data + bad_private = copy.deepcopy(scheme_dict) + bad_private["keyval"]["private"] = "" + with self.assertRaises(ValueError): + SSlibSigner(bad_private) + + # Assert error for invalid scheme + invalid_scheme = copy.deepcopy(scheme_dict) + invalid_scheme["scheme"] = "invalid_scheme" + with self.assertRaises(ValueError): + SSlibSigner(invalid_scheme) def test_custom_signer(self): # setup From 7b5a13a0323710680c7b6fd8d87ae2eca4c4a6b3 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 2 Jun 2023 16:32:08 +0200 Subject: [PATCH 02/16] test: test SSlibSigner with all supported schemes Also moves error tests to separate function. Signed-off-by: Lukas Puehringer --- tests/test_signer.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_signer.py b/tests/test_signer.py index 52dd695f..d46c06bf 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -384,8 +384,29 @@ def fake_handler(_) -> str: with self.assertRaises(CryptoError): signer = Signer.from_priv_key_uri(uri, pubkey, fake_handler) - def test_sslib_signer_sign(self): - for scheme_dict in self.keys: + def test_sslib_signer_sign_all_schemes(self): + rsa_key, ed25519_key, ecdsa_key = self.keys + keys = [] + for scheme in [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ]: + key = copy.deepcopy(rsa_key) + key["scheme"] = scheme + keys.append(key) + + self.assertEqual(ecdsa_key["scheme"], "ecdsa-sha2-nistp256") + self.assertEqual(ed25519_key["scheme"], "ed25519") + keys += [ecdsa_key, ed25519_key] + + # Test sign/verify for each supported scheme + for scheme_dict in keys: # Test generation of signatures. sslib_signer = SSlibSigner(scheme_dict) sig_obj = sslib_signer.sign(self.DATA) @@ -396,6 +417,9 @@ def test_sslib_signer_sign(self): ) self.assertTrue(verified, "Incorrect signature.") + def test_sslib_signer_errors(self): + # Test basic initialization errors for each keytype + for scheme_dict in self.keys: # Assert error for invalid private key data bad_private = copy.deepcopy(scheme_dict) bad_private["keyval"]["private"] = "" From 788332e78a4b3cb2d6b8f1e3d714cd626c63b35b Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 26 Jun 2023 14:26:32 +0200 Subject: [PATCH 03/16] signer: move '_get_keyid' to signer._utils Default keyid computation is non-public functionality, which is currently used in Signer methods only, but will also be used in Key methods in the future. To avoid circular imports and protected-access, the method is moved to an internal signer-specific utils module. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_azure_signer.py | 3 ++- securesystemslib/signer/_gcp_signer.py | 3 ++- securesystemslib/signer/_hsm_signer.py | 3 ++- securesystemslib/signer/_signer.py | 16 ---------------- securesystemslib/signer/_sigstore_signer.py | 3 ++- securesystemslib/signer/_spx_signer.py | 3 ++- securesystemslib/signer/_utils.py | 20 ++++++++++++++++++++ tests/test_signer.py | 14 ++++++++------ 8 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 securesystemslib/signer/_utils.py diff --git a/securesystemslib/signer/_azure_signer.py b/securesystemslib/signer/_azure_signer.py index dbe5d6b5..3ecf6aac 100644 --- a/securesystemslib/signer/_azure_signer.py +++ b/securesystemslib/signer/_azure_signer.py @@ -13,6 +13,7 @@ Signer, SSlibKey, ) +from securesystemslib.signer._utils import compute_default_keyid AZURE_IMPORT_ERROR = None try: @@ -231,7 +232,7 @@ def import_(cls, az_vault_name: str, az_key_name: str) -> Tuple[str, Key]: keytype, scheme = cls._get_keytype_and_scheme(key_vault_key.key.crv) keyval = {"public": pem.decode("utf-8")} - keyid = cls._get_keyid(keytype, scheme, keyval) + keyid = compute_default_keyid(keytype, scheme, keyval) public_key = SSlibKey(keyid, keytype, scheme, keyval) priv_key_uri = key_vault_key.key.kid.replace("https:", "azurekms:") diff --git a/securesystemslib/signer/_gcp_signer.py b/securesystemslib/signer/_gcp_signer.py index f0755e73..5f436a1f 100644 --- a/securesystemslib/signer/_gcp_signer.py +++ b/securesystemslib/signer/_gcp_signer.py @@ -13,6 +13,7 @@ Signer, SSlibKey, ) +from securesystemslib.signer._utils import compute_default_keyid logger = logging.getLogger(__name__) @@ -103,7 +104,7 @@ def import_(cls, gcp_keyid: str) -> Tuple[str, Key]: ) from e keyval = {"public": kms_pubkey.pem} - keyid = cls._get_keyid(keytype, scheme, keyval) + keyid = compute_default_keyid(keytype, scheme, keyval) public_key = SSlibKey(keyid, keytype, scheme, keyval) return f"{cls.SCHEME}:{gcp_keyid}", public_key diff --git a/securesystemslib/signer/_hsm_signer.py b/securesystemslib/signer/_hsm_signer.py index e34e49e5..8434cf52 100644 --- a/securesystemslib/signer/_hsm_signer.py +++ b/securesystemslib/signer/_hsm_signer.py @@ -15,6 +15,7 @@ from securesystemslib.signer._key import Key, SSlibKey from securesystemslib.signer._signature import Signature from securesystemslib.signer._signer import SecretsHandler, Signer +from securesystemslib.signer._utils import compute_default_keyid # pylint: disable=wrong-import-position CRYPTO_IMPORT_ERROR = None @@ -321,7 +322,7 @@ def import_( keyval = {"public": public_pem} scheme = _SCHEME_FOR_CURVE[curve] - keyid = cls._get_keyid(KEY_TYPE_ECDSA, scheme, keyval) + keyid = compute_default_keyid(KEY_TYPE_ECDSA, scheme, keyval) key = SSlibKey(keyid, KEY_TYPE_ECDSA, scheme, keyval) return uri, key diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index 5229fc04..70776342 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -8,8 +8,6 @@ import securesystemslib.keys as sslib_keys from securesystemslib.exceptions import UnsupportedLibraryError -from securesystemslib.formats import encode_canonical -from securesystemslib.hash import digest from securesystemslib.signer._key import Key, SSlibKey from securesystemslib.signer._signature import Signature @@ -153,20 +151,6 @@ def from_priv_key_uri( priv_key_uri, public_key, secrets_handler ) - @staticmethod - def _get_keyid(keytype: str, scheme, keyval: Dict[str, Any]) -> str: - """Get keyid as sha256 hexdigest of the cjson representation of key fields.""" - data = encode_canonical( - { - "keytype": keytype, - "scheme": scheme, - "keyval": keyval, - } - ).encode("utf-8") - hasher = digest("sha256") - hasher.update(data) - return hasher.hexdigest() - class SSlibSigner(Signer): """A securesystemslib signer implementation. diff --git a/securesystemslib/signer/_sigstore_signer.py b/securesystemslib/signer/_sigstore_signer.py index 866c12e3..f090b60d 100644 --- a/securesystemslib/signer/_sigstore_signer.py +++ b/securesystemslib/signer/_sigstore_signer.py @@ -18,6 +18,7 @@ Signature, Signer, ) +from securesystemslib.signer._utils import compute_default_keyid IMPORT_ERROR = "sigstore library required to use 'sigstore-oidc' keys" @@ -189,7 +190,7 @@ def import_( keytype = SigstoreKey.DEFAULT_KEY_TYPE scheme = SigstoreKey.DEFAULT_SCHEME keyval = {"identity": identity, "issuer": issuer} - keyid = cls._get_keyid(keytype, scheme, keyval) + keyid = compute_default_keyid(keytype, scheme, keyval) key = SigstoreKey(keyid, keytype, scheme, keyval) uri = cls._get_uri(ambient) diff --git a/securesystemslib/signer/_spx_signer.py b/securesystemslib/signer/_spx_signer.py index 433940e4..95edf73d 100644 --- a/securesystemslib/signer/_spx_signer.py +++ b/securesystemslib/signer/_spx_signer.py @@ -13,6 +13,7 @@ from securesystemslib.signer._key import Key from securesystemslib.signer._signature import Signature from securesystemslib.signer._signer import SecretsHandler, Signer +from securesystemslib.signer._utils import compute_default_keyid SPX_IMPORT_ERROR = None try: @@ -54,7 +55,7 @@ def from_bytes(cls, public: bytes) -> "SpxKey": scheme = cls.DEFAULT_SCHEME keyval = {"public": public.hex()} - keyid = SpxSigner._get_keyid( # pylint: disable=protected-access + keyid = compute_default_keyid( # pylint: disable=protected-access keytype, scheme, keyval ) return cls(keyid, keytype, scheme, keyval) diff --git a/securesystemslib/signer/_utils.py b/securesystemslib/signer/_utils.py new file mode 100644 index 00000000..8cd1d62d --- /dev/null +++ b/securesystemslib/signer/_utils.py @@ -0,0 +1,20 @@ +"""Signer utils for internal use. """ + +from typing import Any, Dict + +from securesystemslib.formats import encode_canonical +from securesystemslib.hash import digest + + +def compute_default_keyid(keytype: str, scheme, keyval: Dict[str, Any]) -> str: + """Return sha256 hexdigest of the canonical json of the key.""" + data = encode_canonical( + { + "keytype": keytype, + "scheme": scheme, + "keyval": keyval, + } + ).encode("utf-8") + hasher = digest("sha256") + hasher.update(data) + return hasher.hexdigest() diff --git a/tests/test_signer.py b/tests/test_signer.py index d46c06bf..24177ad0 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -31,6 +31,7 @@ SSlibSigner, generate_spx_key_pair, ) +from securesystemslib.signer._utils import compute_default_keyid class TestKey(unittest.TestCase): @@ -642,24 +643,25 @@ def test_gpg_key__eq__(self): class TestUtils(unittest.TestCase): - """Test Signer utility methods.""" + """Test utility methods.""" - def test_get_keyid(self): - # pylint: disable=protected-access + def test_compute_default_keyid(self): self.assertEqual( - Signer._get_keyid("rsa", "rsassa-pss-sha256", {"public": "abcd"}), + compute_default_keyid( + "rsa", "rsassa-pss-sha256", {"public": "abcd"} + ), "7b56b88ae790729d4e359d3fc5e889f1e0669a2e71a12d00e87473870c73fbcf", ) # Unsupported keys can have default keyids too self.assertEqual( - Signer._get_keyid("foo", "bar", {"baz": "qux"}), + compute_default_keyid("foo", "bar", {"baz": "qux"}), "e3471be0598305190ba82f6f8043f4df52f3fbe471fdc187223bd9ade92abebb", ) # Invalid keys cannot with self.assertRaises(FormatError): - Signer._get_keyid("foo", "bar", {"baz": 1.1}) + compute_default_keyid("foo", "bar", {"baz": 1.1}) @unittest.skipIf(os.name == "nt", "PySPX n/a on Windows") From 0c54aa77f9539f43eda487c171ee0c4722bd3456 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 26 Jun 2023 14:46:52 +0200 Subject: [PATCH 04/16] key: add 'FIXME' comment for bad method name Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_key.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index 7863b886..b8632fca 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -220,7 +220,11 @@ def to_dict(self) -> Dict[str, Any]: return self._to_dict() def _from_pem(self) -> "PublicKeyTypes": - """Helper to load public key instance from PEM-formatted keyval.""" + """Helper to load public key instance from PEM-formatted keyval. + + # FIXME: Sounds like it's an SSlibKey factory, but isn't. Should think + of a better name or refactor _verify! + """ public_bytes = self.keyval["public"].encode("utf-8") return load_pem_public_key(public_bytes) From 7952c3fae40fdf6012a952902e17dc6ca242dc9c Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 26 Jun 2023 15:45:31 +0200 Subject: [PATCH 05/16] signer: add test key files in standard PEM format Add test keys for all three 'sslib' keytypes, using standard encoding and format: - PEM/PKCS8 for private keys, and - PEM/PKCS8 for encrypted private keys, and - PEM/subjectPublicKeyInfo for public keys. These are the PEM formats recommended and supported by pyca/cryptography. All keys were created with 'openssl' (LibreSSL 3.3.6 for rsa and ecdsa; and OpenSSL 3.1.1 for ed25519): ``` # RSA openssl genpkey -algorithm rsa \ -out rsa_private.pem openssl pkcs8 -in rsa_private.pem -topk8 -passout pass:hunter2 \ -out rsa_private_encrypted.pem openssl rsa -in rsa_private.pem -pubout \ -out rsa_public.pem # ECDSA (NIST P-256) openssl genpkey -algorithm EC \ -pkeyopt ec_paramgen_curve:P-256 \ -pkeyopt ec_param_enc:named_curve \ -out ecdsa_private.pem openssl pkcs8 -in ecdsa_private.pem -topk8 -passout pass:hunter2 \ -out ecdsa_private_encrypted.pem openssl pkey -in ecdsa_private.pem -pubout \ -out ecdsa_public.pem # Ed25519 openssl genpkey -algorithm Ed25519 \ -out ed25519_private.pem openssl pkcs8 -in ed25519_private.pem -topk8 -passout pass:hunter2 \ -out ed25519_private_encrypted.pem openssl pkey -in ed25519_private.pem -pubout \ -out ed25519_public.pem ``` Signed-off-by: Lukas Puehringer --- tests/data/pems/ecdsa_private.pem | 5 ++++ tests/data/pems/ecdsa_private_encrypted.pem | 6 ++++ tests/data/pems/ecdsa_public.pem | 4 +++ tests/data/pems/ed25519_private.pem | 3 ++ tests/data/pems/ed25519_private_encrypted.pem | 6 ++++ tests/data/pems/ed25519_public.pem | 3 ++ tests/data/pems/rsa_private.pem | 28 ++++++++++++++++++ tests/data/pems/rsa_private_encrypted.pem | 29 +++++++++++++++++++ tests/data/pems/rsa_public.pem | 9 ++++++ 9 files changed, 93 insertions(+) create mode 100644 tests/data/pems/ecdsa_private.pem create mode 100644 tests/data/pems/ecdsa_private_encrypted.pem create mode 100644 tests/data/pems/ecdsa_public.pem create mode 100644 tests/data/pems/ed25519_private.pem create mode 100644 tests/data/pems/ed25519_private_encrypted.pem create mode 100644 tests/data/pems/ed25519_public.pem create mode 100644 tests/data/pems/rsa_private.pem create mode 100644 tests/data/pems/rsa_private_encrypted.pem create mode 100644 tests/data/pems/rsa_public.pem diff --git a/tests/data/pems/ecdsa_private.pem b/tests/data/pems/ecdsa_private.pem new file mode 100644 index 00000000..d7b5a7f7 --- /dev/null +++ b/tests/data/pems/ecdsa_private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIP90wnfOXb44C0fH +xubol3Vc2jsIYgBTQq0Oh84d3RWhRANCAARwthJnIUZ4p1Y23l1YVue/o303IcKh +Q0twboZkjEvA3xDwxR0d047EaQOfIFFImkhn+v+gMQJJPB8JiF2iDB4s +-----END PRIVATE KEY----- diff --git a/tests/data/pems/ecdsa_private_encrypted.pem b/tests/data/pems/ecdsa_private_encrypted.pem new file mode 100644 index 00000000..50536f98 --- /dev/null +++ b/tests/data/pems/ecdsa_private_encrypted.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGwMBsGCSqGSIb3DQEFAzAOBAi7xKwdryb3lAICCAAEgZBz9ttxivvOc6XJKG5j +Ev55zbGqCRSoUn+deGgy/osENhbn4xTOYKRKXGMbfF16t7qvUtX9hHozrGeVIdYg +4R7hFYxgMFlYTTVcN30fPwAV2ePtmFu4vo1/TSLhLxRhv1F3GPLoOSzZxT8FP9oh +Rd9BeAgPPC5RPBJJVThTCXesCV4JWUpY2Wf0DjpFvo3OV4w= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/data/pems/ecdsa_public.pem b/tests/data/pems/ecdsa_public.pem new file mode 100644 index 00000000..6237339f --- /dev/null +++ b/tests/data/pems/ecdsa_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcLYSZyFGeKdWNt5dWFbnv6N9NyHC +oUNLcG6GZIxLwN8Q8MUdHdOOxGkDnyBRSJpIZ/r/oDECSTwfCYhdogweLA== +-----END PUBLIC KEY----- diff --git a/tests/data/pems/ed25519_private.pem b/tests/data/pems/ed25519_private.pem new file mode 100644 index 00000000..d197b99c --- /dev/null +++ b/tests/data/pems/ed25519_private.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGiI3w9x2HZ9UKGi51USN5JN2wtppaYVCRIBTp8ESaj3 +-----END PRIVATE KEY----- diff --git a/tests/data/pems/ed25519_private_encrypted.pem b/tests/data/pems/ed25519_private_encrypted.pem new file mode 100644 index 00000000..14cfdff4 --- /dev/null +++ b/tests/data/pems/ed25519_private_encrypted.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAi1r8RB+89SSQICCAAw +DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEC+4jGFz/I4eGd/sfUuFVXgEQD6d +idJtTe06bGSHcI66yxwUHolWyiVnnup79tGvv1y6R40P3vvxdA5EThp33HCLEE29 +RAa02JqNkOK8DwzVZw8= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/data/pems/ed25519_public.pem b/tests/data/pems/ed25519_public.pem new file mode 100644 index 00000000..137861a3 --- /dev/null +++ b/tests/data/pems/ed25519_public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAT2bavrzzBiiWN4YAGYTAt1wXXNzzvEhVkzomKPDNCg8= +-----END PUBLIC KEY----- diff --git a/tests/data/pems/rsa_private.pem b/tests/data/pems/rsa_private.pem new file mode 100644 index 00000000..82406424 --- /dev/null +++ b/tests/data/pems/rsa_private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDCFfquKiIv9xfl +izfYicVTnYfy0vXhB5fS1pxl1v4DBwfqciH3uHdFIe8YPtfLq5oxGYaV0CdJVjAk +eqgnmPbKdXNT9EWmv7qfw+zRAWDkaaUSplugWMttLRuxFBxdeeok2xUsQ9PdQu+7 +WsVFtbMAwWM2002Tlb+7QYTNefcPTeMh2EPwgoB2tNF/RUwP1NEJct0yYRTa+R/U +1VSc5Nv91FflADxO0HKVWei5mSdtq9DQn+k+Uct/FSCwrOh3AOUmZs8b0FbBepAK +Y/QOA4Y0hnVfbzUw4FoZuQ1US6e02MJJpDFCN8AtVVTk0B9qXZzAwg22pqhztZr5 +Na+kUQhbAgMBAAECggEAFZPH+NDqWBbKa1Sc8s/uRit/T7mwaEIl2OTPImtSdhe0 +A5aIvDef2um44SMrbpM3YzoJQmKP25FfbM7OHwjcdwmztqOzkqRCJTzs+ReEJCCy +n24rRZpZk1uudnNb6/B/3XUV14P66+BjMpsWz3cx3WWimBfJyhyd4j2YfBeRJfw7 +GLCJ0Jeplj0hKEC4Yo6Dvppnl0DJn8NnsnXLRTwepjwB/EpSxnrpzwBBwzsMTcx/ +2zKC9sZhTE1RDsgbw2IIUiBk1enAhZtmiS/BFT9Y4jWaeXTkkVSnFXPZLYPkdB9W +sHgKGiWOSX/1j90IHaKsSKRFUdn3FHtDTde7o4kGQQKBgQDtdS+WBHfVvBH9iQgw +GWc3KKJPcKHC1m4+GOHhIElb0f5l/y6OTZkvK/bPtKJ8bpufsr9jBVQYubIVfJ2j +ZmO0ukclkzIjzwvY9sSHbnWzFfKbjqNG1zGaZYNe0WM/Lx51pG69hxlVzivLAObf +fqYR0+dt5imD/46FcfHFkTQdCwKBgQDRPchsq4zxxvqMYGxzMfyp7l8y1lLcORHS +j2qkOB0n973DggW2sLIEl3uqf/schpbYO8zFs/1YKrJ5LNnYF14GduugmS5znpnb +YvMJyTXFAqmcbl48ahVUvyOrgxTAOJOfFLRXwZiIVzaAaOop+Ph6A4hEYvXWJ8FW +j6lVr7mz8QKBgFYabArly95At/VLPyDR1U92+IP9v2o6/vadZyqO3org9nJdua/4 +C1fDhVeDlHeyU9PwqN1rDTd5/k00RqT9d6IM+cdyPHgnl5AwysqhDyTFDJfDfQku +9tmZfa1gF7DNkSnvWgh3eIRYoiCWTyEzd1x3ji+Xie5HOJLC4nxVTqRJAoGAZQb6 +rZWLAPX85ShtVJVvFDFW37nh2hjoBQ1gBRhe43xXsH0n+xSHb3YgrKsMeLJ3RMJi +1ZZZHWfIMn+4UwC9Uku66xjq98I9MVMuW6w9/PiTIkeb0nm6AOgk9dvdeg4XILkj +djewSSwq0YdWgJuIhYkNE0/guN0LGZtVvFyTQlECfy3l4m4VwlaPRSSYUxUzULs7 +xc34lL1mf09pWxWebZw4ILQQ90DGWOSD/Zgq6CryRfYgsYqXmGNgDbUFRTWh2DMq +6IoLG3wiqrKSW2oFQL3UOzws0ag7C+6aqKnydpQoEtaP5X+DfAWdAOqnsOP1Ry+W +VTrmtVm4yLiMPBnsw3I= +-----END PRIVATE KEY----- diff --git a/tests/data/pems/rsa_private_encrypted.pem b/tests/data/pems/rsa_private_encrypted.pem new file mode 100644 index 00000000..166891bc --- /dev/null +++ b/tests/data/pems/rsa_private_encrypted.pem @@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE4TAbBgkqhkiG9w0BBQMwDgQI/DHsRq4QSygCAggABIIEwI3XLWQ170mJ3qYl +miu7ULDvtIFfkp0Yjyvoo5PrwOQQYB5kD4Q4XkqPM3GOidmU0c0AiFtcLuxqD0/X +ivxPy/rpE9IW0uEtzw/8bF/eqyTTs+JZryYDYoNEqrLN8SxA5oMI+SCiz8Q43/y4 +s6XOlaMKH9N+YcIZMSSVG0In/aQ7b5B2jehaB/eKTZmsvjk/7XMEHQ4vtjclswHd +yFbNk/Qh4MAIDJ9SCghwWyQp382ledKzqYPJbngkUu70Inbx0T2trh4wHE9c2/0Q +pymbSPLdPEPeDybt7VYenikQZBI9myyE7PfZN+Mj6HB01ziI1BTwegCtOL0XMYQD +J8aOCf5+qIcbouqvroMKgTYhpivacgzPwiuX+zDZv6FxDwn2Y16nIsrCYssSMCK0 +COLynbXxTtijeHYF2GTymTKQD5caZ4eRV68JLpaPSees4UFGbgxUUmhrRtcO7vt4 +FS4axe7AFOIEnBYEAsrmKb0hfq45jaBp7FQQ2abG+xO2wG6pNvM0omaP7fAtUdfm +agIxvRM4srboP7NK1TbmVufTTkOhGUw0gdW7Rom+VB6m/q3jcHdejQe4a4I9HyxP +rtCiBksZ9+J0X+7PZcbuR5AYtNldDg799p9UTjBttvLmKKhyIVbbwXoGXsyp+Bzm +W/1XWpMRT019jLb87adGR+HPPjAWF+kSe9TJA2XICekHvIxCfsZbi0TTq+5t31gH +Jv71Dn52NCTa6JMLtmFHH3h5JrujN9vvhr2lvQChkK1bb8xzAG4yzjAiOr09gRC3 +hFK8in/6ePLnoEdfnTezzg5mK4NJnM65aY9IDCPdLzN+M0DgvZdM3NEzdOnvA8/w +clnWT1N3r09aNPac5VrnlUC9eNS8pzgQwpshkae3vecT+HLe79uoI0KyYD5XSy5j +HbQ6Fvl9AvMcWuZT7zx/V990Q/iYTRhuOTvyJhIe3iWikcO3zVzUl1UE6afmYmev +Iz8WQpNoPdI82aVk+t1DqYQWtY1a/gAYGT1TfjCnQebWeqAYFhCPklv2pttmAVYj +ThHiYLNR80v3KrrH+fFWynnVAmot23RuBIsM8rhTUIOd2Bxyq4YrE0SGTDKvFJ7H +mn5RRrIbICTsqpx/dLNL1mgiazHdZAFSsnZApXGYiixsgc4QQfApsLlGg3LGZD4a +STJW1+Ds9CQTzKZzMWnYDGCFhLjobH1LfnfzhvOpKA399uuv0L1P3ixgfUt30W+p +d7AkYHE494EPx4ogoj40JBztGS2UvRb0l4tJlPv50v9x+kPXK+50dEmsW7NrCATt +m1NbIGw2QzL9++SFbocf5P7RBdQBTPpXHQeClFduny6IXjQhWk+NKGIXdsVhMcLi +gMjL83LYeY4VPpurFIU8nGVfa7bdTMlh/W5KHpQX4ecibIjxUza0ddbTWxdMTwUk +XgpzvjIrz1XBy79J6H1AuSmh/+e+azmFv1fzkI+KCHGgtp8Q7uY/lDBeuc4zvARF +YMnOqFyA93sMEL28GqjOJLDfERMLuscjIeibbUNDPjwkVErtnQDu9QqLppfnKtJv +dOn20wNndpeo348k2RBO2yVzDTeuCRWkUnTmuhqYpCRopLVfqdv2q6+xM+paW7ZZ +4/+i1Zc= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/data/pems/rsa_public.pem b/tests/data/pems/rsa_public.pem new file mode 100644 index 00000000..02e7bb77 --- /dev/null +++ b/tests/data/pems/rsa_public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhX6rioiL/cX5Ys32InF +U52H8tL14QeX0tacZdb+AwcH6nIh97h3RSHvGD7Xy6uaMRmGldAnSVYwJHqoJ5j2 +ynVzU/RFpr+6n8Ps0QFg5GmlEqZboFjLbS0bsRQcXXnqJNsVLEPT3ULvu1rFRbWz +AMFjNtNNk5W/u0GEzXn3D03jIdhD8IKAdrTRf0VMD9TRCXLdMmEU2vkf1NVUnOTb +/dRX5QA8TtBylVnouZknbavQ0J/pPlHLfxUgsKzodwDlJmbPG9BWwXqQCmP0DgOG +NIZ1X281MOBaGbkNVEuntNjCSaQxQjfALVVU5NAfal2cwMINtqaoc7Wa+TWvpFEI +WwIDAQAB +-----END PUBLIC KEY----- From 7dbbb2ed6d4e9da5766c23b043c3f460820dc2a0 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 26 Jun 2023 15:51:05 +0200 Subject: [PATCH 06/16] key: add public key file import method Add method to import public key files from a standard PEM/subjectPublicKeyInfo encoding for all three sslib keytypes. Note that other PEM formats might work but aren't tested. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_key.py | 112 ++++++++++++++++++++++++++++++- tests/check_public_interfaces.py | 5 ++ tests/test_signer.py | 41 +++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index b8632fca..509095d6 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -13,6 +13,7 @@ VerificationError, ) from securesystemslib.signer._signature import Signature +from securesystemslib.signer._utils import compute_default_keyid CRYPTO_IMPORT_ERROR = None try: @@ -41,7 +42,11 @@ SHA512, HashAlgorithm, ) - from cryptography.hazmat.primitives.serialization import load_pem_public_key + from cryptography.hazmat.primitives.serialization import ( + Encoding, + PublicFormat, + load_pem_public_key, + ) except ImportError: CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required" @@ -228,6 +233,111 @@ def _from_pem(self) -> "PublicKeyTypes": public_bytes = self.keyval["public"].encode("utf-8") return load_pem_public_key(public_bytes) + @staticmethod + def _get_keytype_for_crypto_key(public_key: "PublicKeyTypes") -> str: + """Helper to return keytype for pyca/cryptography public key.""" + if isinstance(public_key, RSAPublicKey): + return "rsa" + + if isinstance(public_key, EllipticCurvePublicKey): + return "ecdsa" + + if isinstance(public_key, Ed25519PublicKey): + return "ed25519" + + raise ValueError(f"unsupported 'public_key' type {type(public_key)}") + + @staticmethod + def _get_default_scheme(keytype: str) -> str: + """Helper to return default scheme for keytype.""" + if keytype == "rsa": + return "rsassa-pss-sha256" + + if keytype == "ecdsa": + return "ecdsa-sha2-nistp256" + + if keytype == "ed25519": + return "ed25519" + + raise ValueError(f"unsupported 'keytype' {keytype}") + + @classmethod + def _from_crypto_public_key( + cls, + public_key: "PublicKeyTypes", + keyid: Optional[str], + scheme: Optional[str], + ) -> "SSlibKey": + """Helper to create SSlibKey from pyca/cryptography public key. + + NOTE: keytype (rsa, ecdsa, ed25519) assessed automatically. Defaults + exist for keyid and scheme, if not passed. + + FIXME: also used in CryptoSigner keygen implementations, which requires + protected access. Should we make it public, or refactor and move to + an internal utils method? + """ + keytype = cls._get_keytype_for_crypto_key(public_key) + if not scheme: + scheme = cls._get_default_scheme(keytype) + + if keytype in ["rsa", "ecdsa"]: + pem: bytes = public_key.public_bytes( + encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo + ) + public_key_value = pem.decode() + + else: # ed25519 + raw: bytes = public_key.public_bytes( + encoding=Encoding.Raw, format=PublicFormat.Raw + ) + public_key_value = raw.hex() + + keyval = {"public": public_key_value} + + if not keyid: + keyid = compute_default_keyid(keytype, scheme, keyval) + + return SSlibKey(keyid, keytype, scheme, keyval) + + @classmethod + def from_file( + cls, + path: str, + scheme: Optional[str] = None, + keyid: Optional[str] = None, + ) -> "SSlibKey": + """Load SSlibKey from PEM. + + NOTE: pyca/cryptography is used to decode the PEM payload. The expected + (and tested) format is subjectPublicKeyInfo (RFC 5280). Other formats + may but are not guaranteed to work. + + Args: + path: Path to public key file. + scheme: SSlibKey signing scheme. Defaults are "rsassa-pss-sha256", + "ecdsa-sha2-nistp256", and "ed25519" according to the keytype + keyid: Key identifier. If not passed, a default keyid is computed. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + ValueError: Key type not supported + ValueError, \ + cryptography.exceptions.UnsupportedAlgorithm: + pyca/cryptography deserialization failed + + Returns: + SSlibKey + + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + with open(path, "rb") as f: + pem = f.read() + public_key = load_pem_public_key(pem) + return cls._from_crypto_public_key(public_key, keyid, scheme) + @staticmethod def _get_hash_algorithm(name: str) -> "HashAlgorithm": """Helper to return hash algorithm for name.""" diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index 6dbfc3f8..f92fedeb 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -307,6 +307,11 @@ def test_gpg_functions(self): securesystemslib.gpg.functions.export_pubkey("f00") self.assertEqual(expected_error_msg, str(ctx.exception)) + def test_sslib_key_from_file(self): + """Assert raise UnsupportedLibraryError on SSlibKey.from_file().""" + with self.assertRaises(UnsupportedLibraryError): + SSlibKey.from_file("should/fail/before/file/open") + def test_signer_verify(self): """Assert generic VerificationError from UnsupportedLibraryError.""" keyid = "aa" diff --git a/tests/test_signer.py b/tests/test_signer.py index 24177ad0..4e9b325a 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -5,6 +5,7 @@ import shutil import tempfile import unittest +from pathlib import Path from typing import Any, Dict, Optional import securesystemslib.keys as KEYS @@ -33,6 +34,8 @@ ) from securesystemslib.signer._utils import compute_default_keyid +PEMS_DIR = Path(__file__).parent / "data" / "pems" + class TestKey(unittest.TestCase): """Key tests. See many more tests in python-tuf test suite""" @@ -280,6 +283,44 @@ def to_dict(self) -> Dict[str, Any]: del KEY_FOR_TYPE_AND_SCHEME[("custom", "ed25519")] +class TestSSlibKey(unittest.TestCase): + """SSlibKey tests.""" + + def test_from_file(self): + """Test load PEM/subjectPublicKeyInfo files for each SSlibKey keytype""" + test_data = [ + ( + "rsa", + "rsassa-pss-sha256", + "2f685fa7546f1856b123223ab086b3def14c89d24eef18f49c32508c2f60e241", + ), + ( + "ecdsa", + "ecdsa-sha2-nistp256", + "50d7e110ad65f3b2dba5c3cfc8c5ca259be9774cc26be3410044ffd4be3aa5f3", + ), + ( + "ed25519", + "ed25519", + "c6d8bf2e4f48b41ac2ce8eca21415ca8ef68c133b47fc33df03d4070a7e1e9cc", + ), + ] + + for keytype, default_scheme, default_keyid in test_data: + key = SSlibKey.from_file(PEMS_DIR / f"{keytype}_public.pem") + self.assertEqual(key.keytype, keytype) + self.assertEqual(key.scheme, default_scheme) + self.assertEqual(key.keyid, default_keyid) + + key = SSlibKey.from_file( + PEMS_DIR / "rsa_public.pem", + scheme="rsa-pkcs1v15-sha224", + keyid="abcdef", + ) + self.assertEqual(key.scheme, "rsa-pkcs1v15-sha224") + self.assertEqual(key.keyid, "abcdef") + + class TestSigner(unittest.TestCase): """Test Signer and SSlibSigner functionality""" From 95059c1da466b213c52f99e6c1bf39c7a55cab2a Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 26 Jun 2023 15:57:22 +0200 Subject: [PATCH 07/16] signer: implement CryptoSigner.from_priv_key_uri Port SSlibSigner.from_priv_key_uri to CryptoSigner to load private key files from standard formats. But don't register yet in SIGNER_FOR_URI_SCHEME. DOES NOT support: - legacy proprietary key formats and custom decryption, - envvar reading DOES support: - encrypted and non-encrypted PEM/PKCS8 format for all three sslib keytypes Note that other PEM formats might work but aren't tested. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_signer.py | 72 ++++++++++++++++++++++++++++-- tests/check_public_interfaces.py | 12 +++++ tests/test_signer.py | 58 ++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index 70776342..5a746145 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -251,6 +251,8 @@ def sign(self, payload: bytes) -> Signature: class CryptoSigner(Signer, metaclass=ABCMeta): """Base class for PYCA/cryptography Signer implementations.""" + FILE_URI_SCHEME = "file" + def __init__(self, public_key: SSlibKey): if CRYPTO_IMPORT_ERROR: raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) @@ -288,15 +290,79 @@ def from_securesystemslib_key( raise ValueError(f"unsupported keytype: {public_key.keytype}") + @classmethod + def _from_pem( + cls, private_pem: bytes, secret: Optional[bytes], public_key: SSlibKey + ): + """Helper factory to create CryptoSigner from private PEM.""" + private_key = load_pem_private_key(private_pem, secret) + + if public_key.keytype == "rsa": + return RSASigner(public_key, cast(RSAPrivateKey, private_key)) + + if public_key.keytype == "ecdsa": + return ECDSASigner( + public_key, cast(EllipticCurvePrivateKey, private_key) + ) + + if public_key.keytype == "ed25519": + return Ed25519Signer( + public_key, cast(Ed25519PrivateKey, private_key) + ) + + raise ValueError(f"unsupported keytype: {public_key.keytype}") + @classmethod def from_priv_key_uri( cls, priv_key_uri: str, public_key: Key, secrets_handler: Optional[SecretsHandler] = None, - ) -> "Signer": - # Do not raise NotImplementedError to appease pylint for all subclasses - raise RuntimeError("use SSlibSigner.from_priv_key_uri") + ) -> "SSlibSigner": + """Constructor for Signer to call + + Please refer to Signer.from_priv_key_uri() documentation. + + NOTE: pyca/cryptography is used to deserialize the key data. The + expected (and tested) encoding/format is PEM/PKCS8. Other formats may + but are not guaranteed to work. + + Additionally raises: + UnsupportedLibraryError: pyca/cryptography not installed + OSError: file cannot be read + ValueError: various errors passed arguments + ValueError, TypeError, \ + cryptography.exceptions.UnsupportedAlgorithm: + pyca/cryptography deserialization failed + + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + if not isinstance(public_key, SSlibKey): + raise ValueError(f"Expected SSlibKey for {priv_key_uri}") + + uri = parse.urlparse(priv_key_uri) + + if uri.scheme != cls.FILE_URI_SCHEME: + raise ValueError(f"SSlibSigner does not support {priv_key_uri}") + + params = dict(parse.parse_qsl(uri.query)) + + if "encrypted" not in params: + raise ValueError(f"{uri.scheme} requires 'encrypted' parameter") + + secret = None + if params["encrypted"] != "false": + if not secrets_handler: + raise ValueError("encrypted key requires a secrets handler") + + secret = secrets_handler("passphrase").encode() + + with open(uri.path, "rb") as f: + private_pem = f.read() + + return cls._from_pem(private_pem, secret, public_key) class RSASigner(CryptoSigner): diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index f92fedeb..102b1eaa 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -51,6 +51,7 @@ SpxSigner, SSlibKey, ) +from securesystemslib.signer._signer import CryptoSigner from securesystemslib.signer._sigstore_signer import SigstoreKey @@ -312,6 +313,17 @@ def test_sslib_key_from_file(self): with self.assertRaises(UnsupportedLibraryError): SSlibKey.from_file("should/fail/before/file/open") + def test_crypto_signer_from_priv_key_uri(self): + """Assert raise UnsupportedLibraryError on SSlibKey.from_file().""" + + public_key = SSlibKey( + "aa", "rsa", "rsa-pkcs1v15-sha512", {"public": "val"} + ) + with self.assertRaises(UnsupportedLibraryError): + CryptoSigner.from_priv_key_uri( + "file:should/fail/before/urlparse", public_key, None + ) + def test_signer_verify(self): """Assert generic VerificationError from UnsupportedLibraryError.""" keyid = "aa" diff --git a/tests/test_signer.py b/tests/test_signer.py index 4e9b325a..e421cafd 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -32,6 +32,7 @@ SSlibSigner, generate_spx_key_pair, ) +from securesystemslib.signer._signer import CryptoSigner from securesystemslib.signer._utils import compute_default_keyid PEMS_DIR = Path(__file__).parent / "data" / "pems" @@ -730,6 +731,63 @@ def test_sphincs(self): ) +class TestCryptoSigner(unittest.TestCase): + """CryptoSigner tests""" + + def test_from_priv_key_uri(self): + """Test load and use PEM/PKCS#8 files for each sslib keytype""" + test_data = [ + ( + "rsa", + "rsassa-pss-sha256", + "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhX6rioiL/cX5Ys32InF\nU52H8tL14QeX0tacZdb+AwcH6nIh97h3RSHvGD7Xy6uaMRmGldAnSVYwJHqoJ5j2\nynVzU/RFpr+6n8Ps0QFg5GmlEqZboFjLbS0bsRQcXXnqJNsVLEPT3ULvu1rFRbWz\nAMFjNtNNk5W/u0GEzXn3D03jIdhD8IKAdrTRf0VMD9TRCXLdMmEU2vkf1NVUnOTb\n/dRX5QA8TtBylVnouZknbavQ0J/pPlHLfxUgsKzodwDlJmbPG9BWwXqQCmP0DgOG\nNIZ1X281MOBaGbkNVEuntNjCSaQxQjfALVVU5NAfal2cwMINtqaoc7Wa+TWvpFEI\nWwIDAQAB\n-----END PUBLIC KEY-----\n", + ), + ( + "ecdsa", + "ecdsa-sha2-nistp256", + "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcLYSZyFGeKdWNt5dWFbnv6N9NyHC\noUNLcG6GZIxLwN8Q8MUdHdOOxGkDnyBRSJpIZ/r/oDECSTwfCYhdogweLA==\n-----END PUBLIC KEY-----\n", + ), + ( + "ed25519", + "ed25519", + "4f66dabebcf30628963786001984c0b75c175cdcf3bc4855933a2628f0cd0a0f", + ), + ] + + signer_backup = SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] + SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] = CryptoSigner + + for keytype, scheme, public_key_value in test_data: + for encrypted in [True, False]: + if encrypted: + file_name = f"{keytype}_private_encrypted.pem" + parameter = "true" + + def handler(_): + return "hunter2" + + else: + file_name = f"{keytype}_private.pem" + parameter = "false" + handler = None + + uri = f"file:{PEMS_DIR / file_name}?encrypted={parameter}" + public_key = SSlibKey( + "abcdefg", keytype, scheme, {"public": public_key_value} + ) + signer = Signer.from_priv_key_uri(uri, public_key, handler) + self.assertIsInstance(signer, CryptoSigner) + + sig = signer.sign(b"DATA") + self.assertIsNone( + signer.public_key.verify_signature(sig, b"DATA") + ) + with self.assertRaises(UnverifiedSignatureError): + signer.public_key.verify_signature(sig, b"NOT DATA") + + SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] = signer_backup + + # Run the unit tests. if __name__ == "__main__": unittest.main() From d633af7aa76ca1fa20489922bea108310b117698 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 26 Jun 2023 16:07:25 +0200 Subject: [PATCH 08/16] signer: add sslib key generation methods Add generate methods to RSASigner, ECDSASigner and Ed25519Signer classes, to generate key pairs (or rather Signer with attached public key) for quick in-memory signing and verifying, e.g. for tests or demos. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/__init__.py | 3 + securesystemslib/signer/_signer.py | 89 +++++++++++++++++++++++++++++ tests/check_public_interfaces.py | 9 +++ tests/test_signer.py | 20 +++++++ 4 files changed, 121 insertions(+) diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index f28bd445..c7814a24 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -13,6 +13,9 @@ from securesystemslib.signer._signature import Signature from securesystemslib.signer._signer import ( SIGNER_FOR_URI_SCHEME, + ECDSASigner, + Ed25519Signer, + RSASigner, SecretsHandler, Signer, SSlibSigner, diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index 5a746145..143e7a95 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -15,8 +15,12 @@ try: from cryptography.hazmat.primitives.asymmetric.ec import ( ECDSA, + SECP256R1, EllipticCurvePrivateKey, ) + from cryptography.hazmat.primitives.asymmetric.ec import ( + generate_private_key as generate_ec_private_key, + ) from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, ) @@ -30,6 +34,9 @@ AsymmetricPadding, RSAPrivateKey, ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + generate_private_key as generate_rsa_private_key, + ) from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from cryptography.hazmat.primitives.hashes import ( SHA224, @@ -387,6 +394,38 @@ def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"): self._algorithm = self._get_hash_algorithm(hash_name) self._padding = self._get_rsa_padding(padding_name, self._algorithm) + @classmethod + def generate( + cls, + keyid: Optional[str] = None, + scheme: Optional[str] = "rsassa-pss-sha256", + size: int = 3072, + ) -> "RSASigner": + """Generate new key pair as rsa signer. + + Args: + keyid: Key identifier. If not passed, a default keyid is computed. + scheme: RSA signing scheme. Default is "rsassa-pss-sha256". + size: RSA key size in bits. Default is 3072. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + + Returns: + RSASigner + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + private_key = generate_rsa_private_key( + public_exponent=65537, + key_size=size, + ) + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, scheme + ) + return cls(public_key, private_key) + @staticmethod def _get_hash_algorithm(name: str) -> "HashAlgorithm": """Helper to return hash algorithm for name.""" @@ -440,6 +479,31 @@ def sign(self, payload: bytes) -> Signature: sig = self._private_key.sign(payload, self._signature_algorithm) return Signature(self.public_key.keyid, sig.hex()) + @classmethod + def generate( + cls, + keyid: Optional[str] = None, + ) -> "ECDSASigner": + """Generate new key pair as "ecdsa-sha2-nistp256" signer. + + Args: + keyid: Key identifier. If not passed, a default keyid is computed. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + + Returns: + ECDSASigner + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + private_key = generate_ec_private_key(SECP256R1()) + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, "ecdsa-sha2-nistp256" + ) + return cls(public_key, private_key) + class Ed25519Signer(CryptoSigner): """pyca/cryptography ecdsa signer implementation""" @@ -454,3 +518,28 @@ def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"): def sign(self, payload: bytes) -> Signature: sig = self._private_key.sign(payload) return Signature(self.public_key.keyid, sig.hex()) + + @classmethod + def generate( + cls, + keyid: Optional[str] = None, + ) -> "Ed25519Signer": + """Generate new key pair as "ed25519" signer. + + Args: + keyid: Key identifier. If not passed, a default keyid is computed. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + + Returns: + ED25519Signer + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + private_key = Ed25519PrivateKey.generate() + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, "ed25519" + ) + return cls(public_key, private_key) diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index 102b1eaa..07c91d35 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -44,8 +44,11 @@ VerificationError, ) from securesystemslib.signer import ( + ECDSASigner, + Ed25519Signer, GPGKey, Key, + RSASigner, Signature, SpxKey, SpxSigner, @@ -324,6 +327,12 @@ def test_crypto_signer_from_priv_key_uri(self): "file:should/fail/before/urlparse", public_key, None ) + def test_signer_generate(self): + """Assert raise UnsupportedLibraryError on CryptoSigner.generate().""" + for signer_class in [RSASigner, Ed25519Signer, ECDSASigner]: + with self.assertRaises(UnsupportedLibraryError): + signer_class.generate() + def test_signer_verify(self): """Assert generic VerificationError from UnsupportedLibraryError.""" keyid = "aa" diff --git a/tests/test_signer.py b/tests/test_signer.py index e421cafd..5aad9a10 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -20,9 +20,12 @@ from securesystemslib.signer import ( KEY_FOR_TYPE_AND_SCHEME, SIGNER_FOR_URI_SCHEME, + ECDSASigner, + Ed25519Signer, GPGKey, GPGSigner, Key, + RSASigner, SecretsHandler, Signature, Signer, @@ -787,6 +790,23 @@ def handler(_): SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] = signer_backup + def test_generate(self): + """Test generate and use signer (key pair) for each sslib keytype""" + test_data = [ + (RSASigner, "rsa", "rsassa-pss-sha256"), + (ECDSASigner, "ecdsa", "ecdsa-sha2-nistp256"), + (Ed25519Signer, "ed25519", "ed25519"), + ] + for signer_class, keytype, default_scheme in test_data: + signer = signer_class.generate() + self.assertEqual(signer.public_key.keytype, keytype) + self.assertEqual(signer.public_key.scheme, default_scheme) + + sig = signer.sign(b"DATA") + self.assertIsNone(signer.public_key.verify_signature(sig, b"DATA")) + with self.assertRaises(UnverifiedSignatureError): + signer.public_key.verify_signature(sig, b"NOT DATA") + # Run the unit tests. if __name__ == "__main__": From 16e48dd84da0e066774f7b93d54255ccb77dc86c Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 09:31:03 +0200 Subject: [PATCH 09/16] signer: fix copy/paste mistake in CryptoSigner CryptoSigner.from_priv_key_uri was copied from SSlibSigner. This commit changes to return type and error message from SSlibSigner to CryptoSigner. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_signer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index 143e7a95..7ad0a7db 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -325,7 +325,7 @@ def from_priv_key_uri( priv_key_uri: str, public_key: Key, secrets_handler: Optional[SecretsHandler] = None, - ) -> "SSlibSigner": + ) -> "CryptoSigner": """Constructor for Signer to call Please refer to Signer.from_priv_key_uri() documentation. @@ -352,7 +352,7 @@ def from_priv_key_uri( uri = parse.urlparse(priv_key_uri) if uri.scheme != cls.FILE_URI_SCHEME: - raise ValueError(f"SSlibSigner does not support {priv_key_uri}") + raise ValueError(f"CryptoSigner does not support {priv_key_uri}") params = dict(parse.parse_qsl(uri.query)) From 0f965eb7325325504d18c029d6a6084e196b9024 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 09:40:08 +0200 Subject: [PATCH 10/16] signer: move SSlibSigner and CryptoSigner Move SSlibSigner and CryptoSigner to separate internal modules for a cleaner code separation of signer interface and implementations. This commit does not change functionality. SSlibSigner and CryptoSigner (and its subclasses) remain publicly accessible via `securesystemslib.signer`. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/__init__.py | 11 +- securesystemslib/signer/_azure_signer.py | 9 +- securesystemslib/signer/_crypto_signer.py | 343 +++++++++++++++++ securesystemslib/signer/_gcp_signer.py | 9 +- securesystemslib/signer/_signer.py | 436 +--------------------- securesystemslib/signer/_sslib_signer.py | 110 ++++++ tests/check_public_interfaces.py | 2 +- tests/test_signer.py | 2 +- 8 files changed, 469 insertions(+), 453 deletions(-) create mode 100644 securesystemslib/signer/_crypto_signer.py create mode 100644 securesystemslib/signer/_sslib_signer.py diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index c7814a24..ec3633ad 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -6,6 +6,12 @@ """ from securesystemslib.signer._aws_signer import AWSSigner from securesystemslib.signer._azure_signer import AzureSigner +from securesystemslib.signer._crypto_signer import ( + CryptoSigner, + ECDSASigner, + Ed25519Signer, + RSASigner, +) from securesystemslib.signer._gcp_signer import GCPSigner from securesystemslib.signer._gpg_signer import GPGKey, GPGSigner from securesystemslib.signer._hsm_signer import HSMSigner @@ -13,12 +19,8 @@ from securesystemslib.signer._signature import Signature from securesystemslib.signer._signer import ( SIGNER_FOR_URI_SCHEME, - ECDSASigner, - Ed25519Signer, - RSASigner, SecretsHandler, Signer, - SSlibSigner, ) from securesystemslib.signer._sigstore_signer import SigstoreKey, SigstoreSigner from securesystemslib.signer._spx_signer import ( @@ -26,6 +28,7 @@ SpxSigner, generate_spx_key_pair, ) +from securesystemslib.signer._sslib_signer import SSlibSigner # Register supported private key uri schemes and the Signers implementing them SIGNER_FOR_URI_SCHEME.update( diff --git a/securesystemslib/signer/_azure_signer.py b/securesystemslib/signer/_azure_signer.py index 3ecf6aac..97183fff 100644 --- a/securesystemslib/signer/_azure_signer.py +++ b/securesystemslib/signer/_azure_signer.py @@ -6,13 +6,8 @@ import securesystemslib.hash as sslib_hash from securesystemslib.exceptions import UnsupportedLibraryError -from securesystemslib.signer._key import Key -from securesystemslib.signer._signer import ( - SecretsHandler, - Signature, - Signer, - SSlibKey, -) +from securesystemslib.signer._key import Key, SSlibKey +from securesystemslib.signer._signer import SecretsHandler, Signature, Signer from securesystemslib.signer._utils import compute_default_keyid AZURE_IMPORT_ERROR = None diff --git a/securesystemslib/signer/_crypto_signer.py b/securesystemslib/signer/_crypto_signer.py new file mode 100644 index 00000000..58f87179 --- /dev/null +++ b/securesystemslib/signer/_crypto_signer.py @@ -0,0 +1,343 @@ +"""Signer implementation for pyca/cryptography signing. """ + +import logging +from abc import ABCMeta +from typing import Any, Dict, Optional, cast +from urllib import parse + +from securesystemslib.exceptions import UnsupportedLibraryError +from securesystemslib.signer._key import Key, SSlibKey +from securesystemslib.signer._signature import Signature +from securesystemslib.signer._signer import SecretsHandler, Signer + +CRYPTO_IMPORT_ERROR = None +try: + from cryptography.hazmat.primitives.asymmetric.ec import ( + ECDSA, + SECP256R1, + EllipticCurvePrivateKey, + ) + from cryptography.hazmat.primitives.asymmetric.ec import ( + generate_private_key as generate_ec_private_key, + ) + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + ) + from cryptography.hazmat.primitives.asymmetric.padding import ( + MGF1, + PSS, + AsymmetricPadding, + PKCS1v15, + ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + AsymmetricPadding, + RSAPrivateKey, + ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + generate_private_key as generate_rsa_private_key, + ) + from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes + from cryptography.hazmat.primitives.hashes import ( + SHA224, + SHA256, + SHA384, + SHA512, + HashAlgorithm, + ) + from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, + ) +except ImportError: + CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required" + +logger = logging.getLogger(__name__) + + +class CryptoSigner(Signer, metaclass=ABCMeta): + """Base class for PYCA/cryptography Signer implementations.""" + + FILE_URI_SCHEME = "file" + + def __init__(self, public_key: SSlibKey): + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + self.public_key = public_key + + @classmethod + def from_securesystemslib_key( + cls, key_dict: Dict[str, Any] + ) -> "CryptoSigner": + """Factory to create CryptoSigner from securesystemslib private key dict.""" + private = key_dict["keyval"]["private"] + public_key = SSlibKey.from_securesystemslib_key(key_dict) + + private_key: PrivateKeyTypes + if public_key.keytype == "rsa": + private_key = cast( + RSAPrivateKey, + load_pem_private_key(private.encode(), password=None), + ) + return RSASigner(public_key, private_key) + + if public_key.keytype == "ecdsa": + private_key = cast( + EllipticCurvePrivateKey, + load_pem_private_key(private.encode(), password=None), + ) + return ECDSASigner(public_key, private_key) + + if public_key.keytype == "ed25519": + private_key = Ed25519PrivateKey.from_private_bytes( + bytes.fromhex(private) + ) + return Ed25519Signer(public_key, private_key) + + raise ValueError(f"unsupported keytype: {public_key.keytype}") + + @classmethod + def _from_pem( + cls, private_pem: bytes, secret: Optional[bytes], public_key: SSlibKey + ): + """Helper factory to create CryptoSigner from private PEM.""" + private_key = load_pem_private_key(private_pem, secret) + + if public_key.keytype == "rsa": + return RSASigner(public_key, cast(RSAPrivateKey, private_key)) + + if public_key.keytype == "ecdsa": + return ECDSASigner( + public_key, cast(EllipticCurvePrivateKey, private_key) + ) + + if public_key.keytype == "ed25519": + return Ed25519Signer( + public_key, cast(Ed25519PrivateKey, private_key) + ) + + raise ValueError(f"unsupported keytype: {public_key.keytype}") + + @classmethod + def from_priv_key_uri( + cls, + priv_key_uri: str, + public_key: Key, + secrets_handler: Optional[SecretsHandler] = None, + ) -> "CryptoSigner": + """Constructor for Signer to call + + Please refer to Signer.from_priv_key_uri() documentation. + + NOTE: pyca/cryptography is used to deserialize the key data. The + expected (and tested) encoding/format is PEM/PKCS8. Other formats may + but are not guaranteed to work. + + Additionally raises: + UnsupportedLibraryError: pyca/cryptography not installed + OSError: file cannot be read + ValueError: various errors passed arguments + ValueError, TypeError, \ + cryptography.exceptions.UnsupportedAlgorithm: + pyca/cryptography deserialization failed + + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + if not isinstance(public_key, SSlibKey): + raise ValueError(f"Expected SSlibKey for {priv_key_uri}") + + uri = parse.urlparse(priv_key_uri) + + if uri.scheme != cls.FILE_URI_SCHEME: + raise ValueError(f"CryptoSigner does not support {priv_key_uri}") + + params = dict(parse.parse_qsl(uri.query)) + + if "encrypted" not in params: + raise ValueError(f"{uri.scheme} requires 'encrypted' parameter") + + secret = None + if params["encrypted"] != "false": + if not secrets_handler: + raise ValueError("encrypted key requires a secrets handler") + + secret = secrets_handler("passphrase").encode() + + with open(uri.path, "rb") as f: + private_pem = f.read() + + return cls._from_pem(private_pem, secret, public_key) + + +class RSASigner(CryptoSigner): + """pyca/cryptography rsa signer implementation""" + + def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"): + if public_key.scheme not in [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ]: + raise ValueError(f"unsupported scheme {public_key.scheme}") + + super().__init__(public_key) + self._private_key = private_key + padding_name, hash_name = public_key.scheme.split("-")[1:] + self._algorithm = self._get_hash_algorithm(hash_name) + self._padding = self._get_rsa_padding(padding_name, self._algorithm) + + @classmethod + def generate( + cls, + keyid: Optional[str] = None, + scheme: Optional[str] = "rsassa-pss-sha256", + size: int = 3072, + ) -> "RSASigner": + """Generate new key pair as rsa signer. + + Args: + keyid: Key identifier. If not passed, a default keyid is computed. + scheme: RSA signing scheme. Default is "rsassa-pss-sha256". + size: RSA key size in bits. Default is 3072. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + + Returns: + RSASigner + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + private_key = generate_rsa_private_key( + public_exponent=65537, + key_size=size, + ) + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, scheme + ) + return cls(public_key, private_key) + + @staticmethod + def _get_hash_algorithm(name: str) -> "HashAlgorithm": + """Helper to return hash algorithm for name.""" + algorithm: HashAlgorithm + if name == "sha224": + algorithm = SHA224() + if name == "sha256": + algorithm = SHA256() + if name == "sha384": + algorithm = SHA384() + if name == "sha512": + algorithm = SHA512() + + return algorithm + + @staticmethod + def _get_rsa_padding( + name: str, hash_algorithm: "HashAlgorithm" + ) -> "AsymmetricPadding": + """Helper to return rsa signature padding for name.""" + padding: AsymmetricPadding + if name == "pss": + padding = PSS( + mgf=MGF1(hash_algorithm), salt_length=PSS.DIGEST_LENGTH + ) + + if name == "pkcs1v15": + padding = PKCS1v15() + + return padding + + def sign(self, payload: bytes) -> Signature: + sig = self._private_key.sign(payload, self._padding, self._algorithm) + return Signature(self.public_key.keyid, sig.hex()) + + +class ECDSASigner(CryptoSigner): + """pyca/cryptography ecdsa signer implementation""" + + def __init__( + self, public_key: SSlibKey, private_key: "EllipticCurvePrivateKey" + ): + if public_key.scheme != "ecdsa-sha2-nistp256": + raise ValueError(f"unsupported scheme {public_key.scheme}") + + super().__init__(public_key) + self._private_key = private_key + self._signature_algorithm = ECDSA(SHA256()) + + def sign(self, payload: bytes) -> Signature: + sig = self._private_key.sign(payload, self._signature_algorithm) + return Signature(self.public_key.keyid, sig.hex()) + + @classmethod + def generate( + cls, + keyid: Optional[str] = None, + ) -> "ECDSASigner": + """Generate new key pair as "ecdsa-sha2-nistp256" signer. + + Args: + keyid: Key identifier. If not passed, a default keyid is computed. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + + Returns: + ECDSASigner + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + private_key = generate_ec_private_key(SECP256R1()) + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, "ecdsa-sha2-nistp256" + ) + return cls(public_key, private_key) + + +class Ed25519Signer(CryptoSigner): + """pyca/cryptography ecdsa signer implementation""" + + def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"): + if public_key.scheme != "ed25519": + raise ValueError(f"unsupported scheme {public_key.scheme}") + + super().__init__(public_key) + self._private_key = private_key + + def sign(self, payload: bytes) -> Signature: + sig = self._private_key.sign(payload) + return Signature(self.public_key.keyid, sig.hex()) + + @classmethod + def generate( + cls, + keyid: Optional[str] = None, + ) -> "Ed25519Signer": + """Generate new key pair as "ed25519" signer. + + Args: + keyid: Key identifier. If not passed, a default keyid is computed. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + + Returns: + ED25519Signer + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + private_key = Ed25519PrivateKey.generate() + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, "ed25519" + ) + return cls(public_key, private_key) diff --git a/securesystemslib/signer/_gcp_signer.py b/securesystemslib/signer/_gcp_signer.py index 5f436a1f..d43d26aa 100644 --- a/securesystemslib/signer/_gcp_signer.py +++ b/securesystemslib/signer/_gcp_signer.py @@ -6,13 +6,8 @@ import securesystemslib.hash as sslib_hash from securesystemslib import exceptions -from securesystemslib.signer._key import Key -from securesystemslib.signer._signer import ( - SecretsHandler, - Signature, - Signer, - SSlibKey, -) +from securesystemslib.signer._key import Key, SSlibKey +from securesystemslib.signer._signer import SecretsHandler, Signature, Signer from securesystemslib.signer._utils import compute_default_keyid logger = logging.getLogger(__name__) diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index 7ad0a7db..67fa53f4 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -1,56 +1,12 @@ -"""Signer interface and the default implementations""" +"""Signer interface """ import logging -import os from abc import ABCMeta, abstractmethod -from typing import Any, Callable, Dict, Optional, Type, cast -from urllib import parse +from typing import Callable, Dict, Optional, Type -import securesystemslib.keys as sslib_keys -from securesystemslib.exceptions import UnsupportedLibraryError -from securesystemslib.signer._key import Key, SSlibKey +from securesystemslib.signer._key import Key from securesystemslib.signer._signature import Signature -CRYPTO_IMPORT_ERROR = None -try: - from cryptography.hazmat.primitives.asymmetric.ec import ( - ECDSA, - SECP256R1, - EllipticCurvePrivateKey, - ) - from cryptography.hazmat.primitives.asymmetric.ec import ( - generate_private_key as generate_ec_private_key, - ) - from cryptography.hazmat.primitives.asymmetric.ed25519 import ( - Ed25519PrivateKey, - ) - from cryptography.hazmat.primitives.asymmetric.padding import ( - MGF1, - PSS, - AsymmetricPadding, - PKCS1v15, - ) - from cryptography.hazmat.primitives.asymmetric.rsa import ( - AsymmetricPadding, - RSAPrivateKey, - ) - from cryptography.hazmat.primitives.asymmetric.rsa import ( - generate_private_key as generate_rsa_private_key, - ) - from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes - from cryptography.hazmat.primitives.hashes import ( - SHA224, - SHA256, - SHA384, - SHA512, - HashAlgorithm, - ) - from cryptography.hazmat.primitives.serialization import ( - load_pem_private_key, - ) -except ImportError: - CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required" - logger = logging.getLogger(__name__) # NOTE Signer dispatch table is defined here so it's usable by Signer, @@ -157,389 +113,3 @@ def from_priv_key_uri( return signer.from_priv_key_uri( priv_key_uri, public_key, secrets_handler ) - - -class SSlibSigner(Signer): - """A securesystemslib signer implementation. - - Provides a sign method to generate a cryptographic signature with a - securesystemslib-style rsa, ed25519 or ecdsa key. See keys module - for the supported types, schemes and hash algorithms. - - SSlibSigners should be instantiated with Signer.from_priv_key_uri(). - These private key URI schemes are supported: - * "envvar:": - VAR is an environment variable with unencrypted private key content. - envvar:MYPRIVKEY - * "file:?encrypted=[true|false]": - PATH is a file path to a file with private key content. If - encrypted=true, the file is expected to have been created with - securesystemslib.keys.encrypt_key(). - file:path/to/file?encrypted=true - file:/abs/path/to/file?encrypted=false - - Attributes: - key_dict: - A securesystemslib-style key dictionary. This is an implementation - detail, not part of public API - """ - - ENVVAR_URI_SCHEME = "envvar" - FILE_URI_SCHEME = "file" - - def __init__(self, key_dict: Dict): - self.key_dict = key_dict - self._crypto_signer = CryptoSigner.from_securesystemslib_key(key_dict) - - @classmethod - def from_priv_key_uri( - cls, - priv_key_uri: str, - public_key: Key, - secrets_handler: Optional[SecretsHandler] = None, - ) -> "SSlibSigner": - """Constructor for Signer to call - - Please refer to Signer.from_priv_key_uri() documentation. - - Additionally raises: - OSError: Reading the file failed with "file:" URI - """ - if not isinstance(public_key, SSlibKey): - raise ValueError(f"Expected SSlibKey for {priv_key_uri}") - - uri = parse.urlparse(priv_key_uri) - - if uri.scheme == cls.ENVVAR_URI_SCHEME: - # read private key from environment variable - private = os.getenv(uri.path) - if private is None: - raise ValueError(f"Unset env var for {priv_key_uri}") - - elif uri.scheme == cls.FILE_URI_SCHEME: - params = dict(parse.parse_qsl(uri.query)) - if "encrypted" not in params: - raise ValueError(f"{uri.scheme} requires 'encrypted' parameter") - - # read private key (may be encrypted or not) from file - with open(uri.path, "rb") as f: - private = f.read().decode() - - if params["encrypted"] != "false": - if not secrets_handler: - raise ValueError("encrypted key requires a secrets handler") - - secret = secrets_handler("passphrase") - decrypted = sslib_keys.decrypt_key(private, secret) - private = decrypted["keyval"]["private"] - - else: - raise ValueError(f"SSlibSigner does not support {priv_key_uri}") - - keydict = public_key.to_securesystemslib_key() - keydict["keyval"]["private"] = private - - return cls(keydict) - - def sign(self, payload: bytes) -> Signature: - """Signs a given payload by the key assigned to the SSlibSigner instance. - - Please see Signer.sign() documentation. - - Additionally raises: - securesystemslib.exceptions.FormatError: Key argument is malformed. - securesystemslib.exceptions.CryptoError, \ - securesystemslib.exceptions.UnsupportedAlgorithmError: - Signing errors. - """ - return self._crypto_signer.sign(payload) - - -class CryptoSigner(Signer, metaclass=ABCMeta): - """Base class for PYCA/cryptography Signer implementations.""" - - FILE_URI_SCHEME = "file" - - def __init__(self, public_key: SSlibKey): - if CRYPTO_IMPORT_ERROR: - raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - self.public_key = public_key - - @classmethod - def from_securesystemslib_key( - cls, key_dict: Dict[str, Any] - ) -> "CryptoSigner": - """Factory to create CryptoSigner from securesystemslib private key dict.""" - private = key_dict["keyval"]["private"] - public_key = SSlibKey.from_securesystemslib_key(key_dict) - - private_key: PrivateKeyTypes - if public_key.keytype == "rsa": - private_key = cast( - RSAPrivateKey, - load_pem_private_key(private.encode(), password=None), - ) - return RSASigner(public_key, private_key) - - if public_key.keytype == "ecdsa": - private_key = cast( - EllipticCurvePrivateKey, - load_pem_private_key(private.encode(), password=None), - ) - return ECDSASigner(public_key, private_key) - - if public_key.keytype == "ed25519": - private_key = Ed25519PrivateKey.from_private_bytes( - bytes.fromhex(private) - ) - return Ed25519Signer(public_key, private_key) - - raise ValueError(f"unsupported keytype: {public_key.keytype}") - - @classmethod - def _from_pem( - cls, private_pem: bytes, secret: Optional[bytes], public_key: SSlibKey - ): - """Helper factory to create CryptoSigner from private PEM.""" - private_key = load_pem_private_key(private_pem, secret) - - if public_key.keytype == "rsa": - return RSASigner(public_key, cast(RSAPrivateKey, private_key)) - - if public_key.keytype == "ecdsa": - return ECDSASigner( - public_key, cast(EllipticCurvePrivateKey, private_key) - ) - - if public_key.keytype == "ed25519": - return Ed25519Signer( - public_key, cast(Ed25519PrivateKey, private_key) - ) - - raise ValueError(f"unsupported keytype: {public_key.keytype}") - - @classmethod - def from_priv_key_uri( - cls, - priv_key_uri: str, - public_key: Key, - secrets_handler: Optional[SecretsHandler] = None, - ) -> "CryptoSigner": - """Constructor for Signer to call - - Please refer to Signer.from_priv_key_uri() documentation. - - NOTE: pyca/cryptography is used to deserialize the key data. The - expected (and tested) encoding/format is PEM/PKCS8. Other formats may - but are not guaranteed to work. - - Additionally raises: - UnsupportedLibraryError: pyca/cryptography not installed - OSError: file cannot be read - ValueError: various errors passed arguments - ValueError, TypeError, \ - cryptography.exceptions.UnsupportedAlgorithm: - pyca/cryptography deserialization failed - - """ - if CRYPTO_IMPORT_ERROR: - raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - if not isinstance(public_key, SSlibKey): - raise ValueError(f"Expected SSlibKey for {priv_key_uri}") - - uri = parse.urlparse(priv_key_uri) - - if uri.scheme != cls.FILE_URI_SCHEME: - raise ValueError(f"CryptoSigner does not support {priv_key_uri}") - - params = dict(parse.parse_qsl(uri.query)) - - if "encrypted" not in params: - raise ValueError(f"{uri.scheme} requires 'encrypted' parameter") - - secret = None - if params["encrypted"] != "false": - if not secrets_handler: - raise ValueError("encrypted key requires a secrets handler") - - secret = secrets_handler("passphrase").encode() - - with open(uri.path, "rb") as f: - private_pem = f.read() - - return cls._from_pem(private_pem, secret, public_key) - - -class RSASigner(CryptoSigner): - """pyca/cryptography rsa signer implementation""" - - def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"): - if public_key.scheme not in [ - "rsassa-pss-sha224", - "rsassa-pss-sha256", - "rsassa-pss-sha384", - "rsassa-pss-sha512", - "rsa-pkcs1v15-sha224", - "rsa-pkcs1v15-sha256", - "rsa-pkcs1v15-sha384", - "rsa-pkcs1v15-sha512", - ]: - raise ValueError(f"unsupported scheme {public_key.scheme}") - - super().__init__(public_key) - self._private_key = private_key - padding_name, hash_name = public_key.scheme.split("-")[1:] - self._algorithm = self._get_hash_algorithm(hash_name) - self._padding = self._get_rsa_padding(padding_name, self._algorithm) - - @classmethod - def generate( - cls, - keyid: Optional[str] = None, - scheme: Optional[str] = "rsassa-pss-sha256", - size: int = 3072, - ) -> "RSASigner": - """Generate new key pair as rsa signer. - - Args: - keyid: Key identifier. If not passed, a default keyid is computed. - scheme: RSA signing scheme. Default is "rsassa-pss-sha256". - size: RSA key size in bits. Default is 3072. - - Raises: - UnsupportedLibraryError: pyca/cryptography not installed - - Returns: - RSASigner - """ - if CRYPTO_IMPORT_ERROR: - raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - private_key = generate_rsa_private_key( - public_exponent=65537, - key_size=size, - ) - public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access - private_key.public_key(), keyid, scheme - ) - return cls(public_key, private_key) - - @staticmethod - def _get_hash_algorithm(name: str) -> "HashAlgorithm": - """Helper to return hash algorithm for name.""" - algorithm: HashAlgorithm - if name == "sha224": - algorithm = SHA224() - if name == "sha256": - algorithm = SHA256() - if name == "sha384": - algorithm = SHA384() - if name == "sha512": - algorithm = SHA512() - - return algorithm - - @staticmethod - def _get_rsa_padding( - name: str, hash_algorithm: "HashAlgorithm" - ) -> "AsymmetricPadding": - """Helper to return rsa signature padding for name.""" - padding: AsymmetricPadding - if name == "pss": - padding = PSS( - mgf=MGF1(hash_algorithm), salt_length=PSS.DIGEST_LENGTH - ) - - if name == "pkcs1v15": - padding = PKCS1v15() - - return padding - - def sign(self, payload: bytes) -> Signature: - sig = self._private_key.sign(payload, self._padding, self._algorithm) - return Signature(self.public_key.keyid, sig.hex()) - - -class ECDSASigner(CryptoSigner): - """pyca/cryptography ecdsa signer implementation""" - - def __init__( - self, public_key: SSlibKey, private_key: "EllipticCurvePrivateKey" - ): - if public_key.scheme != "ecdsa-sha2-nistp256": - raise ValueError(f"unsupported scheme {public_key.scheme}") - - super().__init__(public_key) - self._private_key = private_key - self._signature_algorithm = ECDSA(SHA256()) - - def sign(self, payload: bytes) -> Signature: - sig = self._private_key.sign(payload, self._signature_algorithm) - return Signature(self.public_key.keyid, sig.hex()) - - @classmethod - def generate( - cls, - keyid: Optional[str] = None, - ) -> "ECDSASigner": - """Generate new key pair as "ecdsa-sha2-nistp256" signer. - - Args: - keyid: Key identifier. If not passed, a default keyid is computed. - - Raises: - UnsupportedLibraryError: pyca/cryptography not installed - - Returns: - ECDSASigner - """ - if CRYPTO_IMPORT_ERROR: - raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - private_key = generate_ec_private_key(SECP256R1()) - public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access - private_key.public_key(), keyid, "ecdsa-sha2-nistp256" - ) - return cls(public_key, private_key) - - -class Ed25519Signer(CryptoSigner): - """pyca/cryptography ecdsa signer implementation""" - - def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"): - if public_key.scheme != "ed25519": - raise ValueError(f"unsupported scheme {public_key.scheme}") - - super().__init__(public_key) - self._private_key = private_key - - def sign(self, payload: bytes) -> Signature: - sig = self._private_key.sign(payload) - return Signature(self.public_key.keyid, sig.hex()) - - @classmethod - def generate( - cls, - keyid: Optional[str] = None, - ) -> "Ed25519Signer": - """Generate new key pair as "ed25519" signer. - - Args: - keyid: Key identifier. If not passed, a default keyid is computed. - - Raises: - UnsupportedLibraryError: pyca/cryptography not installed - - Returns: - ED25519Signer - """ - if CRYPTO_IMPORT_ERROR: - raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - private_key = Ed25519PrivateKey.generate() - public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access - private_key.public_key(), keyid, "ed25519" - ) - return cls(public_key, private_key) diff --git a/securesystemslib/signer/_sslib_signer.py b/securesystemslib/signer/_sslib_signer.py new file mode 100644 index 00000000..f54c595a --- /dev/null +++ b/securesystemslib/signer/_sslib_signer.py @@ -0,0 +1,110 @@ +"""Legacy signer default implementations""" + +import logging +import os +from typing import Dict, Optional +from urllib import parse + +from securesystemslib import keys as sslib_keys +from securesystemslib.signer._crypto_signer import CryptoSigner +from securesystemslib.signer._key import Key, SSlibKey +from securesystemslib.signer._signature import Signature +from securesystemslib.signer._signer import SecretsHandler, Signer + +logger = logging.getLogger(__name__) + + +class SSlibSigner(Signer): + """A securesystemslib signer implementation. + + Provides a sign method to generate a cryptographic signature with a + securesystemslib-style rsa, ed25519 or ecdsa key. See keys module + for the supported types, schemes and hash algorithms. + + SSlibSigners should be instantiated with Signer.from_priv_key_uri(). + These private key URI schemes are supported: + * "envvar:": + VAR is an environment variable with unencrypted private key content. + envvar:MYPRIVKEY + * "file:?encrypted=[true|false]": + PATH is a file path to a file with private key content. If + encrypted=true, the file is expected to have been created with + securesystemslib.keys.encrypt_key(). + file:path/to/file?encrypted=true + file:/abs/path/to/file?encrypted=false + + Attributes: + key_dict: + A securesystemslib-style key dictionary. This is an implementation + detail, not part of public API + """ + + ENVVAR_URI_SCHEME = "envvar" + FILE_URI_SCHEME = "file" + + def __init__(self, key_dict: Dict): + self.key_dict = key_dict + self._crypto_signer = CryptoSigner.from_securesystemslib_key(key_dict) + + @classmethod + def from_priv_key_uri( + cls, + priv_key_uri: str, + public_key: Key, + secrets_handler: Optional[SecretsHandler] = None, + ) -> "SSlibSigner": + """Constructor for Signer to call + + Please refer to Signer.from_priv_key_uri() documentation. + + Additionally raises: + OSError: Reading the file failed with "file:" URI + """ + if not isinstance(public_key, SSlibKey): + raise ValueError(f"Expected SSlibKey for {priv_key_uri}") + + uri = parse.urlparse(priv_key_uri) + + if uri.scheme == cls.ENVVAR_URI_SCHEME: + # read private key from environment variable + private = os.getenv(uri.path) + if private is None: + raise ValueError(f"Unset env var for {priv_key_uri}") + + elif uri.scheme == cls.FILE_URI_SCHEME: + params = dict(parse.parse_qsl(uri.query)) + if "encrypted" not in params: + raise ValueError(f"{uri.scheme} requires 'encrypted' parameter") + + # read private key (may be encrypted or not) from file + with open(uri.path, "rb") as f: + private = f.read().decode() + + if params["encrypted"] != "false": + if not secrets_handler: + raise ValueError("encrypted key requires a secrets handler") + + secret = secrets_handler("passphrase") + decrypted = sslib_keys.decrypt_key(private, secret) + private = decrypted["keyval"]["private"] + + else: + raise ValueError(f"SSlibSigner does not support {priv_key_uri}") + + keydict = public_key.to_securesystemslib_key() + keydict["keyval"]["private"] = private + + return cls(keydict) + + def sign(self, payload: bytes) -> Signature: + """Signs a given payload by the key assigned to the SSlibSigner instance. + + Please see Signer.sign() documentation. + + Additionally raises: + securesystemslib.exceptions.FormatError: Key argument is malformed. + securesystemslib.exceptions.CryptoError, \ + securesystemslib.exceptions.UnsupportedAlgorithmError: + Signing errors. + """ + return self._crypto_signer.sign(payload) diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index 07c91d35..811d6d47 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -44,6 +44,7 @@ VerificationError, ) from securesystemslib.signer import ( + CryptoSigner, ECDSASigner, Ed25519Signer, GPGKey, @@ -54,7 +55,6 @@ SpxSigner, SSlibKey, ) -from securesystemslib.signer._signer import CryptoSigner from securesystemslib.signer._sigstore_signer import SigstoreKey diff --git a/tests/test_signer.py b/tests/test_signer.py index 5aad9a10..f785161f 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -20,6 +20,7 @@ from securesystemslib.signer import ( KEY_FOR_TYPE_AND_SCHEME, SIGNER_FOR_URI_SCHEME, + CryptoSigner, ECDSASigner, Ed25519Signer, GPGKey, @@ -35,7 +36,6 @@ SSlibSigner, generate_spx_key_pair, ) -from securesystemslib.signer._signer import CryptoSigner from securesystemslib.signer._utils import compute_default_keyid PEMS_DIR = Path(__file__).parent / "data" / "pems" From d14a21c42a0dc2fb3e262a5f57bd6b56ab3701f0 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 09:55:00 +0200 Subject: [PATCH 11/16] signer: move CryptoSigner generate functions Move functions from subclasses to superclass: Ed25519Signer.generate() -> CryptoSigner.generate_ed25519() RSASigner.generate() -> CryptoSigner.generate_rsa() ECDSASigner.generate() -> CryptoSigner.generate_ecdsa() This allows us to make the subclasses non-public. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_crypto_signer.py | 145 +++++++++++----------- tests/check_public_interfaces.py | 11 +- tests/test_signer.py | 13 +- 3 files changed, 82 insertions(+), 87 deletions(-) diff --git a/securesystemslib/signer/_crypto_signer.py b/securesystemslib/signer/_crypto_signer.py index 58f87179..367b8932 100644 --- a/securesystemslib/signer/_crypto_signer.py +++ b/securesystemslib/signer/_crypto_signer.py @@ -169,36 +169,36 @@ def from_priv_key_uri( return cls._from_pem(private_pem, secret, public_key) + @staticmethod + def generate_ed25519( + keyid: Optional[str] = None, + ) -> "CryptoSigner": + """Generate new key pair as "ed25519" signer. -class RSASigner(CryptoSigner): - """pyca/cryptography rsa signer implementation""" + Args: + keyid: Key identifier. If not passed, a default keyid is computed. - def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"): - if public_key.scheme not in [ - "rsassa-pss-sha224", - "rsassa-pss-sha256", - "rsassa-pss-sha384", - "rsassa-pss-sha512", - "rsa-pkcs1v15-sha224", - "rsa-pkcs1v15-sha256", - "rsa-pkcs1v15-sha384", - "rsa-pkcs1v15-sha512", - ]: - raise ValueError(f"unsupported scheme {public_key.scheme}") + Raises: + UnsupportedLibraryError: pyca/cryptography not installed - super().__init__(public_key) - self._private_key = private_key - padding_name, hash_name = public_key.scheme.split("-")[1:] - self._algorithm = self._get_hash_algorithm(hash_name) - self._padding = self._get_rsa_padding(padding_name, self._algorithm) + Returns: + ED25519Signer + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - @classmethod - def generate( - cls, + private_key = Ed25519PrivateKey.generate() + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, "ed25519" + ) + return Ed25519Signer(public_key, private_key) + + @staticmethod + def generate_rsa( keyid: Optional[str] = None, scheme: Optional[str] = "rsassa-pss-sha256", size: int = 3072, - ) -> "RSASigner": + ) -> "CryptoSigner": """Generate new key pair as rsa signer. Args: @@ -222,7 +222,54 @@ def generate( public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access private_key.public_key(), keyid, scheme ) - return cls(public_key, private_key) + return RSASigner(public_key, private_key) + + @staticmethod + def generate_ecdsa( + keyid: Optional[str] = None, + ) -> "CryptoSigner": + """Generate new key pair as "ecdsa-sha2-nistp256" signer. + + Args: + keyid: Key identifier. If not passed, a default keyid is computed. + + Raises: + UnsupportedLibraryError: pyca/cryptography not installed + + Returns: + ECDSASigner + """ + if CRYPTO_IMPORT_ERROR: + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + private_key = generate_ec_private_key(SECP256R1()) + public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access + private_key.public_key(), keyid, "ecdsa-sha2-nistp256" + ) + return ECDSASigner(public_key, private_key) + + +class RSASigner(CryptoSigner): + """pyca/cryptography rsa signer implementation""" + + def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"): + if public_key.scheme not in [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ]: + raise ValueError(f"unsupported scheme {public_key.scheme}") + + super().__init__(public_key) + self._private_key = private_key + padding_name, hash_name = public_key.scheme.split("-")[1:] + self._algorithm = self._get_hash_algorithm(hash_name) + self._padding = self._get_rsa_padding(padding_name, self._algorithm) @staticmethod def _get_hash_algorithm(name: str) -> "HashAlgorithm": @@ -277,31 +324,6 @@ def sign(self, payload: bytes) -> Signature: sig = self._private_key.sign(payload, self._signature_algorithm) return Signature(self.public_key.keyid, sig.hex()) - @classmethod - def generate( - cls, - keyid: Optional[str] = None, - ) -> "ECDSASigner": - """Generate new key pair as "ecdsa-sha2-nistp256" signer. - - Args: - keyid: Key identifier. If not passed, a default keyid is computed. - - Raises: - UnsupportedLibraryError: pyca/cryptography not installed - - Returns: - ECDSASigner - """ - if CRYPTO_IMPORT_ERROR: - raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - private_key = generate_ec_private_key(SECP256R1()) - public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access - private_key.public_key(), keyid, "ecdsa-sha2-nistp256" - ) - return cls(public_key, private_key) - class Ed25519Signer(CryptoSigner): """pyca/cryptography ecdsa signer implementation""" @@ -316,28 +338,3 @@ def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"): def sign(self, payload: bytes) -> Signature: sig = self._private_key.sign(payload) return Signature(self.public_key.keyid, sig.hex()) - - @classmethod - def generate( - cls, - keyid: Optional[str] = None, - ) -> "Ed25519Signer": - """Generate new key pair as "ed25519" signer. - - Args: - keyid: Key identifier. If not passed, a default keyid is computed. - - Raises: - UnsupportedLibraryError: pyca/cryptography not installed - - Returns: - ED25519Signer - """ - if CRYPTO_IMPORT_ERROR: - raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - private_key = Ed25519PrivateKey.generate() - public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access - private_key.public_key(), keyid, "ed25519" - ) - return cls(public_key, private_key) diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index 811d6d47..fc1c7546 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -45,11 +45,8 @@ ) from securesystemslib.signer import ( CryptoSigner, - ECDSASigner, - Ed25519Signer, GPGKey, Key, - RSASigner, Signature, SpxKey, SpxSigner, @@ -329,9 +326,13 @@ def test_crypto_signer_from_priv_key_uri(self): def test_signer_generate(self): """Assert raise UnsupportedLibraryError on CryptoSigner.generate().""" - for signer_class in [RSASigner, Ed25519Signer, ECDSASigner]: + for generate in [ + CryptoSigner.generate_rsa, + CryptoSigner.generate_ecdsa, + CryptoSigner.generate_ed25519, + ]: with self.assertRaises(UnsupportedLibraryError): - signer_class.generate() + generate() def test_signer_verify(self): """Assert generic VerificationError from UnsupportedLibraryError.""" diff --git a/tests/test_signer.py b/tests/test_signer.py index f785161f..174a07fb 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -21,12 +21,9 @@ KEY_FOR_TYPE_AND_SCHEME, SIGNER_FOR_URI_SCHEME, CryptoSigner, - ECDSASigner, - Ed25519Signer, GPGKey, GPGSigner, Key, - RSASigner, SecretsHandler, Signature, Signer, @@ -793,12 +790,12 @@ def handler(_): def test_generate(self): """Test generate and use signer (key pair) for each sslib keytype""" test_data = [ - (RSASigner, "rsa", "rsassa-pss-sha256"), - (ECDSASigner, "ecdsa", "ecdsa-sha2-nistp256"), - (Ed25519Signer, "ed25519", "ed25519"), + (CryptoSigner.generate_rsa, "rsa", "rsassa-pss-sha256"), + (CryptoSigner.generate_ecdsa, "ecdsa", "ecdsa-sha2-nistp256"), + (CryptoSigner.generate_ed25519, "ed25519", "ed25519"), ] - for signer_class, keytype, default_scheme in test_data: - signer = signer_class.generate() + for generate, keytype, default_scheme in test_data: + signer = generate() self.assertEqual(signer.public_key.keytype, keytype) self.assertEqual(signer.public_key.scheme, default_scheme) From de28eb74d121af5679dc627cd53cf5f28daa6955 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 10:01:11 +0200 Subject: [PATCH 12/16] signer: make CryptoSigner subclasses non-public To decrease the API surface, RSASigner, Ed25519Signer and ECDSASigner are made non-public and should be loaded via Signer.from_priv_key_uri, or generated via CryptoSigner.generate_* methods. NOTE: Removing them from securesystemslib.signer.__init__ would be enough (they are already in an internal module), but prefixing the classes with an underscore makes it more explicit to us securesystemslib developers. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/__init__.py | 7 +----- securesystemslib/signer/_crypto_signer.py | 30 +++++++++++------------ 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index ec3633ad..2ededf4c 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -6,12 +6,7 @@ """ from securesystemslib.signer._aws_signer import AWSSigner from securesystemslib.signer._azure_signer import AzureSigner -from securesystemslib.signer._crypto_signer import ( - CryptoSigner, - ECDSASigner, - Ed25519Signer, - RSASigner, -) +from securesystemslib.signer._crypto_signer import CryptoSigner from securesystemslib.signer._gcp_signer import GCPSigner from securesystemslib.signer._gpg_signer import GPGKey, GPGSigner from securesystemslib.signer._hsm_signer import HSMSigner diff --git a/securesystemslib/signer/_crypto_signer.py b/securesystemslib/signer/_crypto_signer.py index 367b8932..90eeab3e 100644 --- a/securesystemslib/signer/_crypto_signer.py +++ b/securesystemslib/signer/_crypto_signer.py @@ -78,20 +78,20 @@ def from_securesystemslib_key( RSAPrivateKey, load_pem_private_key(private.encode(), password=None), ) - return RSASigner(public_key, private_key) + return _RSASigner(public_key, private_key) if public_key.keytype == "ecdsa": private_key = cast( EllipticCurvePrivateKey, load_pem_private_key(private.encode(), password=None), ) - return ECDSASigner(public_key, private_key) + return _ECDSASigner(public_key, private_key) if public_key.keytype == "ed25519": private_key = Ed25519PrivateKey.from_private_bytes( bytes.fromhex(private) ) - return Ed25519Signer(public_key, private_key) + return _Ed25519Signer(public_key, private_key) raise ValueError(f"unsupported keytype: {public_key.keytype}") @@ -103,15 +103,15 @@ def _from_pem( private_key = load_pem_private_key(private_pem, secret) if public_key.keytype == "rsa": - return RSASigner(public_key, cast(RSAPrivateKey, private_key)) + return _RSASigner(public_key, cast(RSAPrivateKey, private_key)) if public_key.keytype == "ecdsa": - return ECDSASigner( + return _ECDSASigner( public_key, cast(EllipticCurvePrivateKey, private_key) ) if public_key.keytype == "ed25519": - return Ed25519Signer( + return _Ed25519Signer( public_key, cast(Ed25519PrivateKey, private_key) ) @@ -191,7 +191,7 @@ def generate_ed25519( public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access private_key.public_key(), keyid, "ed25519" ) - return Ed25519Signer(public_key, private_key) + return _Ed25519Signer(public_key, private_key) @staticmethod def generate_rsa( @@ -222,7 +222,7 @@ def generate_rsa( public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access private_key.public_key(), keyid, scheme ) - return RSASigner(public_key, private_key) + return _RSASigner(public_key, private_key) @staticmethod def generate_ecdsa( @@ -246,11 +246,11 @@ def generate_ecdsa( public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access private_key.public_key(), keyid, "ecdsa-sha2-nistp256" ) - return ECDSASigner(public_key, private_key) + return _ECDSASigner(public_key, private_key) -class RSASigner(CryptoSigner): - """pyca/cryptography rsa signer implementation""" +class _RSASigner(CryptoSigner): + """Internal pyca/cryptography rsa signer implementation""" def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"): if public_key.scheme not in [ @@ -307,8 +307,8 @@ def sign(self, payload: bytes) -> Signature: return Signature(self.public_key.keyid, sig.hex()) -class ECDSASigner(CryptoSigner): - """pyca/cryptography ecdsa signer implementation""" +class _ECDSASigner(CryptoSigner): + """Internal pyca/cryptography ecdsa signer implementation""" def __init__( self, public_key: SSlibKey, private_key: "EllipticCurvePrivateKey" @@ -325,8 +325,8 @@ def sign(self, payload: bytes) -> Signature: return Signature(self.public_key.keyid, sig.hex()) -class Ed25519Signer(CryptoSigner): - """pyca/cryptography ecdsa signer implementation""" +class _Ed25519Signer(CryptoSigner): + """Internal pyca/cryptography ecdsa signer implementation""" def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"): if public_key.scheme != "ed25519": From 414460fb6712512aca0e6c9648c987f510f90e95 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 15:13:42 +0200 Subject: [PATCH 13/16] key: SSlibKey.from_file -> SSlibKey.from_pem If needed we can add a from_file wrapper on top of from_pem (bytes). Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_key.py | 8 +++----- tests/check_public_interfaces.py | 8 ++++---- tests/test_signer.py | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index 509095d6..74132bfc 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -301,9 +301,9 @@ def _from_crypto_public_key( return SSlibKey(keyid, keytype, scheme, keyval) @classmethod - def from_file( + def from_pem( cls, - path: str, + pem: bytes, scheme: Optional[str] = None, keyid: Optional[str] = None, ) -> "SSlibKey": @@ -314,7 +314,7 @@ def from_file( may but are not guaranteed to work. Args: - path: Path to public key file. + pem: Public key PEM data. scheme: SSlibKey signing scheme. Defaults are "rsassa-pss-sha256", "ecdsa-sha2-nistp256", and "ed25519" according to the keytype keyid: Key identifier. If not passed, a default keyid is computed. @@ -333,8 +333,6 @@ def from_file( if CRYPTO_IMPORT_ERROR: raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - with open(path, "rb") as f: - pem = f.read() public_key = load_pem_public_key(pem) return cls._from_crypto_public_key(public_key, keyid, scheme) diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index fc1c7546..33b81388 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -308,13 +308,13 @@ def test_gpg_functions(self): securesystemslib.gpg.functions.export_pubkey("f00") self.assertEqual(expected_error_msg, str(ctx.exception)) - def test_sslib_key_from_file(self): - """Assert raise UnsupportedLibraryError on SSlibKey.from_file().""" + def test_sslib_key_from_pem(self): + """Assert raise UnsupportedLibraryError on SSlibKey.from_pem().""" with self.assertRaises(UnsupportedLibraryError): - SSlibKey.from_file("should/fail/before/file/open") + SSlibKey.from_pem(b"fail") def test_crypto_signer_from_priv_key_uri(self): - """Assert raise UnsupportedLibraryError on SSlibKey.from_file().""" + """Assert raise UnsupportedLibraryError on 'from_priv_key_uri'.""" public_key = SSlibKey( "aa", "rsa", "rsa-pkcs1v15-sha512", {"public": "val"} diff --git a/tests/test_signer.py b/tests/test_signer.py index 174a07fb..4fe5828f 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -287,8 +287,8 @@ def to_dict(self) -> Dict[str, Any]: class TestSSlibKey(unittest.TestCase): """SSlibKey tests.""" - def test_from_file(self): - """Test load PEM/subjectPublicKeyInfo files for each SSlibKey keytype""" + def test_from_pem(self): + """Test load PEM/subjectPublicKeyInfo for each SSlibKey keytype""" test_data = [ ( "rsa", @@ -307,14 +307,22 @@ def test_from_file(self): ), ] + def _from_file(path): + with open(path, "rb") as f: + pem = f.read() + return pem + for keytype, default_scheme, default_keyid in test_data: - key = SSlibKey.from_file(PEMS_DIR / f"{keytype}_public.pem") + pem = _from_file(PEMS_DIR / f"{keytype}_public.pem") + key = SSlibKey.from_pem(pem) self.assertEqual(key.keytype, keytype) self.assertEqual(key.scheme, default_scheme) self.assertEqual(key.keyid, default_keyid) - key = SSlibKey.from_file( - PEMS_DIR / "rsa_public.pem", + # Test with non-default scheme/keyid + pem = _from_file(PEMS_DIR / "rsa_public.pem") + key = SSlibKey.from_pem( + pem, scheme="rsa-pkcs1v15-sha224", keyid="abcdef", ) From f021d8bda882b027eb0070dba53e657a95b59a37 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 15:18:30 +0200 Subject: [PATCH 14/16] docs: add CryptoSigner usage documentation Signed-off-by: Lukas Puehringer --- docs/CRYPTO_SIGNER.md | 97 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/CRYPTO_SIGNER.md diff --git a/docs/CRYPTO_SIGNER.md b/docs/CRYPTO_SIGNER.md new file mode 100644 index 00000000..a98669c8 --- /dev/null +++ b/docs/CRYPTO_SIGNER.md @@ -0,0 +1,97 @@ + +# CryptoSigner + +`CryptoSigner` is a modern replacement for the legacy `securesystemslib.keys` +module. It can be used via the `Signer.from_priv_key_uri` API to load private +*rsa*, *ecdsa* and *ed25519* keys from file. It also provides API to generate +in-memory signers for ad-hoc signing. + +## Code examples + +### Example 1: Ad-hoc signing + +`CryptoSigner` provides `generate_{rsa, ed25519, ecdsa}` methods for ad-hoc +signing and signature verification, e.g. in tests or demos. + +```python +from securesystemslib.signer import CryptoSigner + +signer = CryptoSigner.generate_ed25519() +signature = signer.sign(b"data") +signer.public_key.verify_signature(signature, b"data") +``` + +### Example 2: Asynchronous key management and signing + +The typical Signer API usage is described in +[this blog post](https://theupdateframework.github.io/python-tuf/2023/01/24/securesystemslib-signer-api.html) +and outlined below for a file-based signer. + +#### 1. Generate key files +*`CryptoSigner` does not provide API to generate key files. Compatible +keys can be generated with standard tools like `openssl genpkey` (CLI) or +`pyca/cryptography` (Python).* + +```python +from cryptography.hazmat.primitives import asymmetric, serialization + +# Generate key pair +private_key = asymmetric.ed25519.Ed25519PrivateKey.generate() + +# Serialize private key as encrypted PEM/PKCS8 +private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(b"hunter2"), +) + +# Serialize public key as encrypted PEM/subjectPublicKeyInfo +public_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, +) + +# Write key files +with open("private.pem", "wb") as f: + f.write(private_pem) +with open("public.pem", "wb") as f: + f.write(public_pem) +``` + +#### 2. Prepare signing environment + +```python +import os +from securesystemslib.signer import SSlibKey + +with open("public.pem", "rb") as f: + public_bytes = f.read() + +# Make public key, signer URI, and key decryption password available to the +# signer, e.g. via environment variables. The private key file must also be +# available to the signer at the specified path. +os.environ.update({ + "SIGNER_URI": "file:private.pem?encrypted=true", + "SIGNER_PUBLIC": public_bytes.decode(), + "SIGNER_SECRET": "hunter2" +}) +``` + +#### 3. Load and use signer + +```python +import os +from securesystemslib.signer import SSlibKey, Signer, CryptoSigner, SIGNER_FOR_URI_SCHEME + +# NOTE: Registration becomes obsolete once CryptoSigner is the default file signer +SIGNER_FOR_URI_SCHEME.update({CryptoSigner.FILE_URI_SCHEME: CryptoSigner}) + +# Read signer details +uri = os.environ["SIGNER_URI"] +public_key = SSlibKey.from_pem(os.environ["SIGNER_PUBLIC"].encode()) +secrets_handler = lambda sec: os.environ["SIGNER_SECRET"] + +# Load and sign +signer = Signer.from_priv_key_uri(uri, public_key, secrets_handler) +signer.sign(b"data") +``` From 9ddde6d1f7f20f525ec1f412c85707e8ce6dc5e5 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 15:32:35 +0200 Subject: [PATCH 15/16] signer: deprecate SSlibSigner and sslib keydict Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_key.py | 12 ++++++++++-- securesystemslib/signer/_sslib_signer.py | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index 74132bfc..9d6ddcb7 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -192,7 +192,11 @@ class SSlibKey(Key): """Key implementation for RSA, Ed25519, ECDSA keys""" def to_securesystemslib_key(self) -> Dict[str, Any]: - """Internal helper, returns a classic securesystemslib keydict""" + """Internal helper, returns a classic securesystemslib keydict. + + .. deprecated:: 0.28.0 + Please use ``CryptoSigner`` instead of securesystemslib keydicts. + """ return { "keyid": self.keyid, "keytype": self.keytype, @@ -202,7 +206,11 @@ def to_securesystemslib_key(self) -> Dict[str, Any]: @classmethod def from_securesystemslib_key(cls, key_dict: Dict[str, Any]) -> "SSlibKey": - """Constructor from classic securesystemslib keydict""" + """Constructor from classic securesystemslib keydict + + .. deprecated:: 0.28.0 + Please use ``CryptoSigner`` instead of securesystemslib keydicts. + """ # ensure possible private keys are not included in keyval return SSlibKey( key_dict["keyid"], diff --git a/securesystemslib/signer/_sslib_signer.py b/securesystemslib/signer/_sslib_signer.py index f54c595a..bc1aef75 100644 --- a/securesystemslib/signer/_sslib_signer.py +++ b/securesystemslib/signer/_sslib_signer.py @@ -37,6 +37,9 @@ class SSlibSigner(Signer): key_dict: A securesystemslib-style key dictionary. This is an implementation detail, not part of public API + + .. deprecated:: 0.28.0 + Please use ``CryptoSigner`` instead. """ ENVVAR_URI_SCHEME = "envvar" From 3951fed3069a81a2df1f56e83ab461c0cd0b60f2 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 10 Aug 2023 15:47:58 +0200 Subject: [PATCH 16/16] signer: fix rebase issues in aws_signer - update SSlibKey import - use new compute_default_keyid Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_aws_signer.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/securesystemslib/signer/_aws_signer.py b/securesystemslib/signer/_aws_signer.py index e13ebd74..73fae09f 100644 --- a/securesystemslib/signer/_aws_signer.py +++ b/securesystemslib/signer/_aws_signer.py @@ -7,13 +7,9 @@ import securesystemslib.hash as sslib_hash from securesystemslib import exceptions from securesystemslib.exceptions import UnsupportedLibraryError -from securesystemslib.signer._key import Key -from securesystemslib.signer._signer import ( - SecretsHandler, - Signature, - Signer, - SSlibKey, -) +from securesystemslib.signer._key import Key, SSlibKey +from securesystemslib.signer._signer import SecretsHandler, Signature, Signer +from securesystemslib.signer._utils import compute_default_keyid logger = logging.getLogger(__name__) @@ -127,7 +123,7 @@ def import_(cls, aws_key_id: str, local_scheme: str) -> Tuple[str, Key]: ) from e keyval = {"public": public_key_pem} - keyid = cls._get_keyid(keytype, local_scheme, keyval) + keyid = compute_default_keyid(keytype, local_scheme, keyval) public_key = SSlibKey(keyid, keytype, local_scheme, keyval) return f"{cls.SCHEME}:{aws_key_id}", public_key