diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index 642597bf..7991ba98 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -24,13 +24,10 @@ 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( { - SSlibSigner.ENVVAR_URI_SCHEME: SSlibSigner, - SSlibSigner.FILE_URI_SCHEME: SSlibSigner, GCPSigner.SCHEME: GCPSigner, HSMSigner.SCHEME: HSMSigner, GPGSigner.SCHEME: GPGSigner, diff --git a/securesystemslib/signer/_crypto_signer.py b/securesystemslib/signer/_crypto_signer.py index 4ddef2b4..4bba130c 100644 --- a/securesystemslib/signer/_crypto_signer.py +++ b/securesystemslib/signer/_crypto_signer.py @@ -2,7 +2,7 @@ import logging from dataclasses import astuple, dataclass -from typing import Any, Dict, Optional, Union +from typing import Optional, Union from urllib import parse from securesystemslib.exceptions import UnsupportedLibraryError @@ -189,28 +189,6 @@ def __init__( def public_key(self) -> Key: return self._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 in ["rsa"] + _ECDSA_KEYTYPES: - private_key = load_pem_private_key(private.encode(), password=None) - - elif public_key.keytype == "ed25519": - private_key = Ed25519PrivateKey.from_private_bytes( - bytes.fromhex(private) - ) - - else: - raise ValueError(f"unsupported keytype: {public_key.keytype}") - - return CryptoSigner(private_key, public_key) - @classmethod def from_priv_key_uri( cls, diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index bd631925..e370f3c6 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -200,34 +200,6 @@ def verify_signature(self, signature: Signature, data: bytes) -> None: 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. - - .. deprecated:: 0.28.0 - Please use ``CryptoSigner`` instead of securesystemslib keydicts. - """ - return { - "keyid": self.keyid, - "keytype": self.keytype, - "scheme": self.scheme, - "keyval": self.keyval, - } - - @classmethod - def from_securesystemslib_key(cls, key_dict: Dict[str, Any]) -> "SSlibKey": - """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"], - key_dict["keytype"], - key_dict["scheme"], - {"public": key_dict["keyval"]["public"]}, - ) - @classmethod def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "SSlibKey": keytype, scheme, keyval = cls._from_dict(key_dict) diff --git a/securesystemslib/signer/_signer.py b/securesystemslib/signer/_signer.py index dc836957..a31433e9 100644 --- a/securesystemslib/signer/_signer.py +++ b/securesystemslib/signer/_signer.py @@ -30,7 +30,7 @@ class Signer(metaclass=ABCMeta): Usage example:: - signer = Signer.from_priv_key_uri("envvar:MYPRIVKEY", pub_key) + signer = Signer.from_priv_key_uri(uri, pub_key) sig = signer.sign(b"data") Note that signer implementations may raise errors (during both @@ -39,11 +39,7 @@ class Signer(metaclass=ABCMeta): Applications should use generic try-except here if unexpected raises are not an option. - See ``SIGNER_FOR_URI_SCHEME`` for supported private key URI schemes. The - currently supported default schemes are: - - * envvar: see ``SSlibSigner`` for details - * file: see ``SSlibSigner`` for details + See ``SIGNER_FOR_URI_SCHEME`` for supported private key URI schemes. Interactive applications may also define a secrets handler that allows asking for user secrets if they are needed:: @@ -53,14 +49,8 @@ class Signer(metaclass=ABCMeta): def sec_handler(secret_name:str) -> str: return getpass(f"Enter {secret_name}: ") - # user will not be asked for a passphrase for unencrypted key - uri = "file:keys/mykey?encrypted=false" signer = Signer.from_priv_key_uri(uri, pub_key, sec_handler) - # user will be asked for a passphrase for encrypted key - uri2 = "file:keys/myenckey?encrypted=true" - signer2 = Signer.from_priv_key_uri(uri2, pub_key2, sec_handler) - Applications can provide their own Signer and Key implementations:: from securesystemslib.signer import Signer, SIGNER_FOR_URI_SCHEME diff --git a/securesystemslib/signer/_sslib_signer.py b/securesystemslib/signer/_sslib_signer.py deleted file mode 100644 index beb9f95a..00000000 --- a/securesystemslib/signer/_sslib_signer.py +++ /dev/null @@ -1,118 +0,0 @@ -"""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) - self._public_key = SSlibKey.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) - - @property - def public_key(self) -> Key: - return self._public_key - - 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/test_dsse.py b/tests/test_dsse.py index 52a64ad1..02aab7d0 100644 --- a/tests/test_dsse.py +++ b/tests/test_dsse.py @@ -2,11 +2,15 @@ import copy import unittest +from pathlib import Path + +from cryptography.hazmat.primitives.serialization import load_pem_private_key -import securesystemslib.keys as KEYS from securesystemslib.dsse import Envelope from securesystemslib.exceptions import VerificationError -from securesystemslib.signer import Signature, SSlibKey, SSlibSigner +from securesystemslib.signer import CryptoSigner, Signature + +PEMS_DIR = Path(__file__).parent / "data" / "pems" class TestEnvelope(unittest.TestCase): @@ -14,11 +18,17 @@ class TestEnvelope(unittest.TestCase): @classmethod def setUpClass(cls): - cls.key_dicts = [ - KEYS.generate_rsa_key(), - KEYS.generate_ed25519_key(), - KEYS.generate_ecdsa_key(), - ] + cls.signers: list[CryptoSigner] = [] + 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) + signer = CryptoSigner(private_key) + + cls.signers.append(signer) cls.signature_dict = { "keyid": "11fa391a0ed7a447", @@ -102,23 +112,14 @@ def test_sign_and_verify(self): envelope_obj = Envelope.from_dict(envelope_dict) key_list = [] - for key_dict in self.key_dicts: - # Test for invalid scheme. - valid_scheme = key_dict["scheme"] - key_dict["scheme"] = "invalid_scheme" - with self.assertRaises(ValueError): - signer = SSlibSigner(key_dict) - - # Sign the payload. - key_dict["scheme"] = valid_scheme - signer = SSlibSigner(key_dict) + for signer in self.signers: envelope_obj.sign(signer) # Create a List of "Key" from key_dict. - key_list.append(SSlibKey.from_securesystemslib_key(key_dict)) + key_list.append(signer.public_key) # Check for signatures of Envelope. - self.assertEqual(len(self.key_dicts), len(envelope_obj.signatures)) + self.assertEqual(len(self.signers), len(envelope_obj.signatures)) for signature in envelope_obj.signatures.values(): self.assertIsInstance(signature, Signature) @@ -136,14 +137,12 @@ def test_sign_and_verify(self): self.assertEqual(len(verified_keys), len(key_list)) # Test for unknown keys and threshold of 1. - new_key_dicts = [ - KEYS.generate_rsa_key(), - KEYS.generate_ed25519_key(), - KEYS.generate_ecdsa_key(), - ] new_key_list = [] - for key_dict in new_key_dicts: - new_key_list.append(SSlibKey.from_securesystemslib_key(key_dict)) + for key in key_list: + new_key = copy.deepcopy(key) + # if it has a different keyid, it is a different key in sslib + new_key.keyid = reversed(key.keyid) + new_key_list.append(new_key) with self.assertRaises(VerificationError): envelope_obj.verify(new_key_list, 1) diff --git a/tests/test_signer.py b/tests/test_signer.py index 73cdb99e..a2dba832 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -8,14 +8,13 @@ from pathlib import Path from typing import Any, Dict, Optional +from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from cryptography.hazmat.primitives.serialization import ( load_pem_private_key, load_pem_public_key, ) -import securesystemslib.keys as KEYS from securesystemslib.exceptions import ( - CryptoError, FormatError, UnverifiedSignatureError, VerificationError, @@ -35,7 +34,6 @@ SpxKey, SpxSigner, SSlibKey, - SSlibSigner, generate_spx_key_pair, ) from securesystemslib.signer._utils import compute_default_keyid @@ -349,186 +347,14 @@ def _from_file(path): class TestSigner(unittest.TestCase): """Test Signer and SSlibSigner functionality""" - @classmethod - def setUpClass(cls): - cls.keys = [ - KEYS.generate_rsa_key(), - KEYS.generate_ed25519_key(), - KEYS.generate_ecdsa_key(), - ] - - cls.DATA = b"DATA" - - # pylint: disable=consider-using-with - cls.testdir = tempfile.TemporaryDirectory() - - @classmethod - def tearDownClass(cls): - cls.testdir.cleanup() - def test_signer_sign_with_incorrect_uri(self): - pubkey = SSlibKey.from_securesystemslib_key(self.keys[0]) - with self.assertRaises(ValueError): - # unknown uri + pubkey = "fake key" + with self.assertRaises(ValueError) as ctx: Signer.from_priv_key_uri("unknownscheme:x", pubkey) - with self.assertRaises(ValueError): - # env variable not defined - Signer.from_priv_key_uri("envvar:NONEXISTENTVAR", pubkey) - - with self.assertRaises(ValueError): - # no "encrypted" param - Signer.from_priv_key_uri("file:path/to/privkey", pubkey) - - with self.assertRaises(OSError): - # file not found - uri = "file:nonexistentfile?encrypted=false" - Signer.from_priv_key_uri(uri, pubkey) - - def test_signer_sign_with_envvar_uri(self): - for key in self.keys: - # setup - pubkey = SSlibKey.from_securesystemslib_key(key) - os.environ["PRIVKEY"] = key["keyval"]["private"] - - # test signing - signer = Signer.from_priv_key_uri("envvar:PRIVKEY", pubkey) - sig = signer.sign(self.DATA) - - pubkey.verify_signature(sig, self.DATA) - with self.assertRaises(UnverifiedSignatureError): - pubkey.verify_signature(sig, b"NOT DATA") - - def test_signer_sign_with_file_uri(self): - for key in self.keys: - # setup - pubkey = SSlibKey.from_securesystemslib_key(key) - # let teardownclass handle the file removal - with tempfile.NamedTemporaryFile( - dir=self.testdir.name, delete=False - ) as f: - f.write(key["keyval"]["private"].encode()) - - # test signing with unencrypted key - uri = f"file:{f.name}?encrypted=false" - signer = Signer.from_priv_key_uri(uri, pubkey) - sig = signer.sign(self.DATA) - - pubkey.verify_signature(sig, self.DATA) - with self.assertRaises(UnverifiedSignatureError): - pubkey.verify_signature(sig, b"NOT DATA") - - def test_signer_sign_with_enc_file_uri(self): - for key in self.keys: - # setup - pubkey = SSlibKey.from_securesystemslib_key(key) - privkey = KEYS.encrypt_key(key, "hunter2") - # let teardownclass handle the file removal - with tempfile.NamedTemporaryFile( - dir=self.testdir.name, delete=False - ) as f: - f.write(privkey.encode()) - - # test signing with encrypted key - def secrets_handler(secret: str) -> str: - if secret != "passphrase": - raise ValueError("Only prepared to return a passphrase") - return "hunter2" - - uri = f"file:{f.name}?encrypted=true" - - signer = Signer.from_priv_key_uri(uri, pubkey, secrets_handler) - sig = signer.sign(self.DATA) - - pubkey.verify_signature(sig, self.DATA) - with self.assertRaises(UnverifiedSignatureError): - pubkey.verify_signature(sig, b"NOT DATA") - - # test wrong passphrase - def fake_handler(_) -> str: - return "12345" - - with self.assertRaises(CryptoError): - signer = Signer.from_priv_key_uri(uri, pubkey, fake_handler) - - 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) - - # Verify signature - verified = KEYS.verify_signature( - scheme_dict, sig_obj.to_dict(), self.DATA - ) - 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"] = "" - 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 - key = self.keys[0] - pubkey = SSlibKey.from_securesystemslib_key(key) - - class CustomSigner(SSlibSigner): - """Custom signer with a hard coded key""" - - CUSTOM_SCHEME = "custom" - - @classmethod - def from_priv_key_uri( - cls, - priv_key_uri: str, - public_key: Key, - secrets_handler: Optional[SecretsHandler] = None, - ) -> "CustomSigner": - return cls(key) - - # register custom signer - SIGNER_FOR_URI_SCHEME[CustomSigner.CUSTOM_SCHEME] = CustomSigner - - # test signing - signer = Signer.from_priv_key_uri("custom:foo", pubkey) - self.assertIsInstance(signer, CustomSigner) - sig = signer.sign(self.DATA) - - pubkey.verify_signature(sig, self.DATA) - with self.assertRaises(UnverifiedSignatureError): - pubkey.verify_signature(sig, b"NOT DATA") + self.assertEqual( + "Unsupported private key scheme unknownscheme", str(ctx.exception) + ) def test_signature_from_to_dict(self): signature_dict = { @@ -758,8 +584,9 @@ def test_sphincs(self): class TestCryptoSigner(unittest.TestCase): """CryptoSigner tests""" - def test_init(self): - """Test CryptoSigner constructor.""" + @classmethod + def setUpClass(cls): + cls.keys: list[PrivateKeyTypes] = [] for keytype in ["rsa", "ecdsa", "ed25519"]: path = PEMS_DIR / f"{keytype}_private.pem" @@ -768,6 +595,12 @@ def test_init(self): private_key = load_pem_private_key(data, None) + cls.keys.append(private_key) + + def test_init(self): + """Test CryptoSigner constructor.""" + for keytype, private_key in zip(["rsa", "ecdsa", "ed25519"], self.keys): + # Init w/o public key (public key is created from private key) signer = CryptoSigner(private_key) self.assertEqual(keytype, signer.public_key.keytype) @@ -776,6 +609,33 @@ def test_init(self): signer2 = CryptoSigner(private_key, signer.public_key) self.assertEqual(keytype, signer2.public_key.keytype) + def test_sign(self): + rsa_schemes = [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ] + ecdsa_schemes = ["ecdsa-sha2-nistp256"] + ed25519_schemes = ["ed25519"] + schemes = [rsa_schemes, ecdsa_schemes, ed25519_schemes] + + for private_key, key_schemes in zip(self.keys, schemes): + public_key = SSlibKey.from_crypto(private_key.public_key()) + for scheme in key_schemes: + public_key.scheme = scheme + signer = CryptoSigner(private_key, public_key) + 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") + def test_from_priv_key_uri(self): """Test load and use PEM/PKCS#8 files for each sslib keytype""" test_data = [ @@ -805,7 +665,8 @@ def test_from_priv_key_uri(self): ), ] - signer_backup = SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] + # FIXME: remove, if CryptoSigner.FILE_URI_SCHEME becomes default (#617) + signer_backup = SIGNER_FOR_URI_SCHEME.get(CryptoSigner.FILE_URI_SCHEME) SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] = CryptoSigner for keytype, scheme, public_key_value, fname in test_data: @@ -834,7 +695,17 @@ def handler(_): with self.assertRaises(UnverifiedSignatureError): signer.public_key.verify_signature(sig, b"NOT DATA") - SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] = signer_backup + if signer_backup: + SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] = signer_backup + + with self.assertRaises(ValueError): + # no "encrypted" param + Signer.from_priv_key_uri("file:path/to/privkey", signer.public_key) + + with self.assertRaises(OSError): + # file not found + uri = "file:nonexistentfile?encrypted=false" + Signer.from_priv_key_uri(uri, signer.public_key) def test_generate(self): """Test generate and use signer (key pair) for each sslib keytype""" @@ -853,6 +724,37 @@ def test_generate(self): with self.assertRaises(UnverifiedSignatureError): signer.public_key.verify_signature(sig, b"NOT DATA") + def test_custom_crypto_signer(self): + # setup + key = self.keys[0] + pubkey = SSlibKey.from_crypto(key.public_key()) + + class CustomSigner(CryptoSigner): + """Custom signer with a hard coded key""" + + CUSTOM_SCHEME = "custom" + + @classmethod + def from_priv_key_uri( + cls, + priv_key_uri: str, + public_key: Key, + secrets_handler: Optional[SecretsHandler] = None, + ) -> "CustomSigner": + return cls(key) + + # register custom signer + SIGNER_FOR_URI_SCHEME[CustomSigner.CUSTOM_SCHEME] = CustomSigner + + # test signing + signer = Signer.from_priv_key_uri("custom:foo", pubkey) + self.assertIsInstance(signer, CustomSigner) + sig = signer.sign(b"DATA") + + pubkey.verify_signature(sig, b"DATA") + with self.assertRaises(UnverifiedSignatureError): + pubkey.verify_signature(sig, b"NOT DATA") + # Run the unit tests. if __name__ == "__main__":