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") +``` diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index f28bd445..2ededf4c 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -6,6 +6,7 @@ """ from securesystemslib.signer._aws_signer import AWSSigner from securesystemslib.signer._azure_signer import AzureSigner +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 @@ -15,7 +16,6 @@ SIGNER_FOR_URI_SCHEME, SecretsHandler, Signer, - SSlibSigner, ) from securesystemslib.signer._sigstore_signer import SigstoreKey, SigstoreSigner from securesystemslib.signer._spx_signer import ( @@ -23,6 +23,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/_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 diff --git a/securesystemslib/signer/_azure_signer.py b/securesystemslib/signer/_azure_signer.py index dbe5d6b5..97183fff 100644 --- a/securesystemslib/signer/_azure_signer.py +++ b/securesystemslib/signer/_azure_signer.py @@ -6,13 +6,9 @@ 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 try: @@ -231,7 +227,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/_crypto_signer.py b/securesystemslib/signer/_crypto_signer.py new file mode 100644 index 00000000..90eeab3e --- /dev/null +++ b/securesystemslib/signer/_crypto_signer.py @@ -0,0 +1,340 @@ +"""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) + + @staticmethod + def generate_ed25519( + keyid: Optional[str] = None, + ) -> "CryptoSigner": + """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 _Ed25519Signer(public_key, private_key) + + @staticmethod + def generate_rsa( + keyid: Optional[str] = None, + scheme: Optional[str] = "rsassa-pss-sha256", + size: int = 3072, + ) -> "CryptoSigner": + """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 _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): + """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 + + def sign(self, payload: bytes) -> Signature: + sig = self._private_key.sign(payload) + return Signature(self.public_key.keyid, sig.hex()) diff --git a/securesystemslib/signer/_gcp_signer.py b/securesystemslib/signer/_gcp_signer.py index f0755e73..d43d26aa 100644 --- a/securesystemslib/signer/_gcp_signer.py +++ b/securesystemslib/signer/_gcp_signer.py @@ -6,13 +6,9 @@ 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__) @@ -103,7 +99,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/_key.py b/securesystemslib/signer/_key.py index 7863b886..9d6ddcb7 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" @@ -187,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, @@ -197,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"], @@ -220,10 +233,117 @@ 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) + @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_pem( + cls, + pem: bytes, + 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: + 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. + + 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) + + 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/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index a61d3ad9..67fa53f4 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -1,15 +1,10 @@ -"""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 -from urllib import parse +from typing import Callable, Dict, Optional, Type -import securesystemslib.keys as sslib_keys -from securesystemslib.formats import encode_canonical -from securesystemslib.hash import digest -from securesystemslib.signer._key import Key, SSlibKey +from securesystemslib.signer._key import Key from securesystemslib.signer._signature import Signature logger = logging.getLogger(__name__) @@ -118,112 +113,3 @@ def from_priv_key_uri( return signer.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. - - 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 - - @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. - """ - sig_dict = sslib_keys.create_signature(self.key_dict, payload) - return Signature(**sig_dict) 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/_sslib_signer.py b/securesystemslib/signer/_sslib_signer.py new file mode 100644 index 00000000..bc1aef75 --- /dev/null +++ b/securesystemslib/signer/_sslib_signer.py @@ -0,0 +1,113 @@ +"""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 + + .. deprecated:: 0.28.0 + Please use ``CryptoSigner`` instead. + """ + + 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/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/check_public_interfaces.py b/tests/check_public_interfaces.py index 6dbfc3f8..33b81388 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -44,6 +44,7 @@ VerificationError, ) from securesystemslib.signer import ( + CryptoSigner, GPGKey, Key, Signature, @@ -307,6 +308,32 @@ def test_gpg_functions(self): securesystemslib.gpg.functions.export_pubkey("f00") self.assertEqual(expected_error_msg, str(ctx.exception)) + def test_sslib_key_from_pem(self): + """Assert raise UnsupportedLibraryError on SSlibKey.from_pem().""" + with self.assertRaises(UnsupportedLibraryError): + SSlibKey.from_pem(b"fail") + + def test_crypto_signer_from_priv_key_uri(self): + """Assert raise UnsupportedLibraryError on 'from_priv_key_uri'.""" + + 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_generate(self): + """Assert raise UnsupportedLibraryError on CryptoSigner.generate().""" + for generate in [ + CryptoSigner.generate_rsa, + CryptoSigner.generate_ecdsa, + CryptoSigner.generate_ed25519, + ]: + with self.assertRaises(UnsupportedLibraryError): + generate() + def test_signer_verify(self): """Assert generic VerificationError from UnsupportedLibraryError.""" keyid = "aa" 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----- 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..4fe5828f 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -5,13 +5,13 @@ import shutil import tempfile import unittest +from pathlib import Path from typing import Any, Dict, Optional import securesystemslib.keys as KEYS from securesystemslib.exceptions import ( CryptoError, FormatError, - UnsupportedAlgorithmError, UnverifiedSignatureError, VerificationError, ) @@ -20,6 +20,7 @@ from securesystemslib.signer import ( KEY_FOR_TYPE_AND_SCHEME, SIGNER_FOR_URI_SCHEME, + CryptoSigner, GPGKey, GPGSigner, Key, @@ -32,6 +33,9 @@ SSlibSigner, generate_spx_key_pair, ) +from securesystemslib.signer._utils import compute_default_keyid + +PEMS_DIR = Path(__file__).parent / "data" / "pems" class TestKey(unittest.TestCase): @@ -280,6 +284,52 @@ def to_dict(self) -> Dict[str, Any]: del KEY_FOR_TYPE_AND_SCHEME[("custom", "ed25519")] +class TestSSlibKey(unittest.TestCase): + """SSlibKey tests.""" + + def test_from_pem(self): + """Test load PEM/subjectPublicKeyInfo for each SSlibKey keytype""" + test_data = [ + ( + "rsa", + "rsassa-pss-sha256", + "2f685fa7546f1856b123223ab086b3def14c89d24eef18f49c32508c2f60e241", + ), + ( + "ecdsa", + "ecdsa-sha2-nistp256", + "50d7e110ad65f3b2dba5c3cfc8c5ca259be9774cc26be3410044ffd4be3aa5f3", + ), + ( + "ed25519", + "ed25519", + "c6d8bf2e4f48b41ac2ce8eca21415ca8ef68c133b47fc33df03d4070a7e1e9cc", + ), + ] + + def _from_file(path): + with open(path, "rb") as f: + pem = f.read() + return pem + + for keytype, default_scheme, default_keyid in test_data: + 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) + + # 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", + ) + self.assertEqual(key.scheme, "rsa-pkcs1v15-sha224") + self.assertEqual(key.keyid, "abcdef") + + class TestSigner(unittest.TestCase): """Test Signer and SSlibSigner functionality""" @@ -385,8 +435,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) @@ -397,25 +468,20 @@ 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 + 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"] = "" + 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 @@ -627,24 +693,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") @@ -672,6 +739,80 @@ 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 + + def test_generate(self): + """Test generate and use signer (key pair) for each sslib keytype""" + test_data = [ + (CryptoSigner.generate_rsa, "rsa", "rsassa-pss-sha256"), + (CryptoSigner.generate_ecdsa, "ecdsa", "ecdsa-sha2-nistp256"), + (CryptoSigner.generate_ed25519, "ed25519", "ed25519"), + ] + 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) + + 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__": unittest.main()