From 28b6457aef7634056726b8d4f8105628a8319510 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Sat, 28 Oct 2023 11:14:22 +0200 Subject: [PATCH] CryptoSigner: support init from PrivateKeyTypes Previously CryptoSigner instances could only be created via `from_priv_key_uri` factory method, because: - CryptoSigner was abstract - Subclass implmentations were protected This change makes the CryptoSigner constructor usable directly, so that signers can be created directly from pyca/cryptography private key objects. If a public key is not passed along, it is created from the private key, with default keyid and signing scheme for key type. *Details* Internally, this now (1) uses composition instead of inheritance, and (2) eliminates one level of abstraction: (1) CryptoSigner is no longer an abstract base class for protected rsa, ecdsa and ed25519 "signer" subclasses. It only made creation more difficult, without advantage. Instead CryptoSigner holds references to the "signers" (2) There is no more abstraction over individual pyca/cryptography rsa, ecdsa and ed25519 private key types. cryptography provides a common "PrivateKeyTypes" interface with sign method, which we can use directly. The different additional sign args for the different key types are managed in new internal data classes. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_crypto_signer.py | 250 ++++++++++------------ tests/test_signer.py | 20 ++ 2 files changed, 136 insertions(+), 134 deletions(-) diff --git a/securesystemslib/signer/_crypto_signer.py b/securesystemslib/signer/_crypto_signer.py index 90eeab3e..eb81fc25 100644 --- a/securesystemslib/signer/_crypto_signer.py +++ b/securesystemslib/signer/_crypto_signer.py @@ -1,8 +1,8 @@ """Signer implementation for pyca/cryptography signing. """ import logging -from abc import ABCMeta -from typing import Any, Dict, Optional, cast +from dataclasses import astuple, dataclass +from typing import Any, Dict, Optional, Union from urllib import parse from securesystemslib.exceptions import UnsupportedLibraryError @@ -53,15 +53,114 @@ logger = logging.getLogger(__name__) -class CryptoSigner(Signer, metaclass=ABCMeta): - """Base class for PYCA/cryptography Signer implementations.""" +@dataclass +class _RSASignArgs: + padding: "AsymmetricPadding" + hash_algo: "HashAlgorithm" + + +@dataclass +class _ECDSASignArgs: + sig_algo: "ECDSA" + + +@dataclass +class _NoSignArgs: + pass + + +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 + + +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 + + +class CryptoSigner(Signer): + """PYCA/cryptography Signer implementations.""" FILE_URI_SCHEME = "file" - def __init__(self, public_key: SSlibKey): + def __init__( + self, + private_key: "PrivateKeyTypes", + public_key: Optional[SSlibKey] = None, + ): if CRYPTO_IMPORT_ERROR: raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + if public_key is None: + public_key = SSlibKey._from_crypto_public_key( + private_key.public_key(), None, None + ) + + self._private_key: PrivateKeyTypes + self._sign_args: Union[_RSASignArgs, _ECDSASignArgs, _NoSignArgs] + + if public_key.keytype == "rsa" and public_key.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", + ]: + if not isinstance(private_key, RSAPrivateKey): + raise ValueError(f"invalid rsa key: {type(private_key)}") + + padding_name, hash_name = public_key.scheme.split("-")[1:] + hash_algo = _get_hash_algorithm(hash_name) + padding = _get_rsa_padding(padding_name, hash_algo) + self._sign_args = _RSASignArgs(padding, hash_algo) + self._private_key = private_key + + elif ( + public_key.keytype == "ecdsa" + and public_key.scheme == "ecdsa-sha2-nistp256" + ): + if not isinstance(private_key, EllipticCurvePrivateKey): + raise ValueError(f"invalid ecdsa key: {type(private_key)}") + + signature_algorithm = ECDSA(SHA256()) + self._sign_args = _ECDSASignArgs(signature_algorithm) + self._private_key = private_key + + elif public_key.keytype == "ed25519" and public_key.scheme == "ed25519": + if not isinstance(private_key, Ed25519PrivateKey): + raise ValueError(f"invalid ed25519 key: {type(private_key)}") + + self._sign_args = _NoSignArgs() + self._private_key = private_key + + else: + raise ValueError( + f"unsupported public key {public_key.keytype}/{public_key.scheme}" + ) + self.public_key = public_key @classmethod @@ -73,49 +172,18 @@ def from_securesystemslib_key( 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 in ["rsa", "ecdsa"]: + private_key = load_pem_private_key(private.encode(), password=None) - 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": + elif 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) - ) + else: + raise ValueError(f"unsupported keytype: {public_key.keytype}") - raise ValueError(f"unsupported keytype: {public_key.keytype}") + return CryptoSigner(private_key, public_key) @classmethod def from_priv_key_uri( @@ -167,7 +235,8 @@ def from_priv_key_uri( with open(uri.path, "rb") as f: private_pem = f.read() - return cls._from_pem(private_pem, secret, public_key) + private_key = load_pem_private_key(private_pem, secret) + return CryptoSigner(private_key, public_key) @staticmethod def generate_ed25519( @@ -191,7 +260,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 CryptoSigner(private_key, public_key) @staticmethod def generate_rsa( @@ -222,7 +291,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 CryptoSigner(private_key, public_key) @staticmethod def generate_ecdsa( @@ -246,95 +315,8 @@ 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) - - -class _RSASigner(CryptoSigner): - """Internal 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): - """Internal 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): - """Internal 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 + return CryptoSigner(private_key, public_key) def sign(self, payload: bytes) -> Signature: - sig = self._private_key.sign(payload) + sig = self._private_key.sign(payload, *astuple(self._sign_args)) # type: ignore return Signature(self.public_key.keyid, sig.hex()) diff --git a/tests/test_signer.py b/tests/test_signer.py index 4fe5828f..2c4704a4 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import Any, Dict, Optional +from cryptography.hazmat.primitives.serialization import load_pem_private_key + import securesystemslib.keys as KEYS from securesystemslib.exceptions import ( CryptoError, @@ -742,6 +744,24 @@ def test_sphincs(self): class TestCryptoSigner(unittest.TestCase): """CryptoSigner tests""" + def test_init(self): + """Test CryptoSigner constructor.""" + for keytype in ["rsa", "ecdsa", "ed25519"]: + path = PEMS_DIR / f"{keytype}_private.pem" + + with open(path, "rb") as f: + data = f.read() + + private_key = load_pem_private_key(data, None) + + # Init w/o public key (public key is created from private key) + signer = CryptoSigner(private_key) + self.assertEqual(keytype, signer.public_key.keytype) + + # Re-init with passed public key + signer2 = CryptoSigner(private_key, signer.public_key) + self.assertEqual(keytype, signer2.public_key.keytype) + def test_from_priv_key_uri(self): """Test load and use PEM/PKCS#8 files for each sslib keytype""" test_data = [