diff --git a/pyproject.toml b/pyproject.toml index 397f54f1..45844daf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,14 +44,14 @@ Source = "https://github.com/secure-systems-lab/securesystemslib" Issues = "https://github.com/secure-systems-lab/securesystemslib/issues" [project.optional-dependencies] -crypto = ["cryptography>=40.0.0"] -gcpkms = ["google-cloud-kms", "cryptography>=40.0.0"] +crypto = ["cryptography>=48.0.0"] +gcpkms = ["google-cloud-kms", "cryptography>=48.0.0"] azurekms = ["azure-identity", "azure-keyvault-keys", "cryptography>=40.0.0"] -awskms = ["boto3", "botocore", "cryptography>=40.0.0"] -hsm = ["asn1crypto", "cryptography>=40.0.0", "PyKCS11"] +awskms = ["boto3", "botocore", "cryptography>=48.0.0"] +hsm = ["asn1crypto", "cryptography>=48.0.0", "PyKCS11"] PySPX = ["PySPX>=0.5.0"] sigstore = ["sigstore>=4,<5"] -vault = ["hvac", "cryptography>=40.0.0"] +vault = ["hvac", "cryptography>=48.0.0"] [tool.hatch.version] path = "securesystemslib/__init__.py" diff --git a/requirements-pinned.txt b/requirements-pinned.txt index 91145256..23766162 100644 --- a/requirements-pinned.txt +++ b/requirements-pinned.txt @@ -10,7 +10,7 @@ cffi==2.0.0 # via # cryptography # pyspx -cryptography==46.0.7 +cryptography==48.0.0 # via -r requirements.txt pycparser==3.0 # via cffi diff --git a/requirements-sigstore.txt b/requirements-sigstore.txt index dad4d731..5b1971a2 100644 --- a/requirements-sigstore.txt +++ b/requirements-sigstore.txt @@ -1 +1 @@ -sigstore==4.2.0 +sigstore==4.3.0 diff --git a/requirements.txt b/requirements.txt index b9e800e1..ad60ac4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # # 'requirements-pinned.txt' is updated on GitHub with Dependabot, which # triggers CI/CD builds to automatically test against updated dependencies. -cryptography >= 37.0.0 +cryptography >= 48.0.0 PySPX; platform_system != 'Windows' PyKCS11 asn1crypto diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index d8fbe8d1..7ef6f613 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -62,6 +62,9 @@ ("rsa", "rsa-pkcs1v15-sha256"): SSlibKey, ("rsa", "rsa-pkcs1v15-sha384"): SSlibKey, ("rsa", "rsa-pkcs1v15-sha512"): SSlibKey, + ("ml-dsa", "ml-dsa-44/1"): SSlibKey, + ("ml-dsa", "ml-dsa-65/1"): SSlibKey, + ("ml-dsa", "ml-dsa-87/1"): SSlibKey, ("rsa", "pgp+rsa-pkcsv1.5"): GPGKey, ("dsa", "pgp+dsa-fips-180-2"): GPGKey, ("eddsa", "pgp+eddsa-ed25519"): GPGKey, diff --git a/securesystemslib/signer/_crypto_signer.py b/securesystemslib/signer/_crypto_signer.py index 326c6762..8df6b8dd 100644 --- a/securesystemslib/signer/_crypto_signer.py +++ b/securesystemslib/signer/_crypto_signer.py @@ -23,6 +23,11 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, ) + from cryptography.hazmat.primitives.asymmetric.mldsa import ( + MLDSA44PrivateKey, + MLDSA65PrivateKey, + MLDSA87PrivateKey, + ) from cryptography.hazmat.primitives.asymmetric.padding import ( MGF1, PSS, @@ -38,6 +43,8 @@ from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from cryptography.hazmat.primitives.hashes import ( SHA256, + SHA512, + Hash, HashAlgorithm, ) from cryptography.hazmat.primitives.serialization import ( @@ -117,6 +124,12 @@ def __init__( private_key: "PrivateKeyTypes", public_key: SSlibKey | None = None, ): + def assert_type( + name: str, key: PrivateKeyTypes, typ: type[PrivateKeyTypes] + ) -> None: + if not isinstance(key, typ): + raise ValueError(f"invalid {name} key: {type(key)}") + if CRYPTO_IMPORT_ERROR: raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) @@ -136,8 +149,7 @@ def __init__( "rsa-pkcs1v15-sha384", "rsa-pkcs1v15-sha512", ]: - if not isinstance(private_key, RSAPrivateKey): - raise ValueError(f"invalid rsa key: {type(private_key)}") + assert_type("rsa", private_key, RSAPrivateKey) hash_name = public_key.get_hash_algorithm_name() hash_algo = get_hash_algorithm(hash_name) @@ -146,31 +158,36 @@ def __init__( padding = _get_rsa_padding(padding_name, hash_algo) self._sign_args = _RSASignArgs(padding, hash_algo) - self._private_key = private_key elif ( public_key.keytype in _ECDSA_KEYTYPES 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 + assert_type("ecdsa", private_key, EllipticCurvePrivateKey) + self._sign_args = _ECDSASignArgs(ECDSA(SHA256())) 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)}") + assert_type("ed25519", private_key, Ed25519PrivateKey) + self._sign_args = _NoSignArgs() + + elif public_key.keytype == "ml-dsa" and public_key.scheme == "ml-dsa-44/1": + assert_type("ml-dsa-44", private_key, MLDSA44PrivateKey) + self._sign_args = _NoSignArgs() + elif public_key.keytype == "ml-dsa" and public_key.scheme == "ml-dsa-65/1": + assert_type("ml-dsa-65", private_key, MLDSA65PrivateKey) + self._sign_args = _NoSignArgs() + + elif public_key.keytype == "ml-dsa" and public_key.scheme == "ml-dsa-87/1": + assert_type("ml-dsa-87", private_key, MLDSA87PrivateKey) self._sign_args = _NoSignArgs() - self._private_key = private_key else: raise ValueError( f"unsupported public key {public_key.keytype}/{public_key.scheme}" ) + self._private_key = private_key self._public_key = public_key @property @@ -321,5 +338,14 @@ def generate_ecdsa( return CryptoSigner(private_key, public_key) def sign(self, payload: bytes) -> Signature: - sig = self._private_key.sign(payload, *astuple(self._sign_args)) # type: ignore + if isinstance( + self._private_key, (MLDSA44PrivateKey, MLDSA65PrivateKey, MLDSA87PrivateKey) + ): + digest = Hash(SHA512()) + digest.update(payload) + + sig = self._private_key.sign(b"tuf" + bytes([1]) + digest.finalize()) + else: + sig = self._private_key.sign(payload, *astuple(self._sign_args)) # type: ignore + return Signature(self.public_key.keyid, sig.hex()) diff --git a/securesystemslib/signer/_gcp_signer.py b/securesystemslib/signer/_gcp_signer.py index d8fb4e56..b229cb14 100644 --- a/securesystemslib/signer/_gcp_signer.py +++ b/securesystemslib/signer/_gcp_signer.py @@ -4,6 +4,7 @@ import hashlib import logging +from typing import Any from urllib import parse from securesystemslib import exceptions @@ -59,6 +60,18 @@ "rsa", "rsa-pkcs1v15-sha512", ), + CryptoKeyVersion.CryptoKeyVersionAlgorithm.PQ_SIGN_ML_DSA_44: ( + "ml-dsa", + "ml-dsa-44/1", + ), + CryptoKeyVersion.CryptoKeyVersionAlgorithm.PQ_SIGN_ML_DSA_65: ( + "ml-dsa", + "ml-dsa-65/1", + ), + CryptoKeyVersion.CryptoKeyVersionAlgorithm.PQ_SIGN_ML_DSA_87: ( + "ml-dsa", + "ml-dsa-87/1", + ), } except ImportError: GCP_IMPORT_ERROR = ( @@ -178,8 +191,11 @@ def sign(self, payload: bytes) -> Signature: hasher = hashlib.new(self.hash_algorithm) hasher.update(payload) - digest = {self.hash_algorithm: hasher.digest()} - request = {"name": self.gcp_keyid, "digest": digest} + request: dict[str, Any] = {"name": self.gcp_keyid} + if self.public_key.keytype == "ml-dsa": + request["data"] = b"tuf" + bytes([1]) + hasher.digest() + else: + request["digest"] = {self.hash_algorithm: hasher.digest()} logger.debug("signing request %s", request) response = self.client.asymmetric_sign(request) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index a45ed4ef..219afd2f 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -32,6 +32,11 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PublicKey, ) + from cryptography.hazmat.primitives.asymmetric.mldsa import ( + MLDSA44PublicKey, + MLDSA65PublicKey, + MLDSA87PublicKey, + ) from cryptography.hazmat.primitives.asymmetric.padding import ( MGF1, PSS, @@ -46,6 +51,7 @@ SHA256, SHA384, SHA512, + Hash, HashAlgorithm, ) from cryptography.hazmat.primitives.serialization import ( @@ -246,7 +252,12 @@ def get_hash_algorithm_name(self) -> str: ]: return f"sha{self.scheme[-3:]}" - elif self.scheme == "ecdsa-sha2-nistp521": + elif self.scheme in [ + "ecdsa-sha2-nistp521", + "ml-dsa-44/1", + "ml-dsa-65/1", + "ml-dsa-87/1", + ]: return "sha512" raise ValueError(f"method not supported for scheme {self.scheme}") @@ -302,24 +313,28 @@ def _pem() -> str: ).decode() if isinstance(public_key, RSAPublicKey): - return "rsa", "rsassa-pss-sha256", _pem() - - if isinstance(public_key, EllipticCurvePublicKey): + ret = ("rsa", "rsassa-pss-sha256", _pem()) + elif isinstance(public_key, EllipticCurvePublicKey): if isinstance(public_key.curve, SECP256R1): - return "ecdsa", "ecdsa-sha2-nistp256", _pem() - - if isinstance(public_key.curve, SECP384R1): - return "ecdsa", "ecdsa-sha2-nistp384", _pem() - - if isinstance(public_key.curve, SECP521R1): - return "ecdsa", "ecdsa-sha2-nistp521", _pem() - - raise ValueError(f"unsupported curve '{public_key.curve.name}'") - - if isinstance(public_key, Ed25519PublicKey): - return "ed25519", "ed25519", _raw() - - raise ValueError(f"unsupported key '{type(public_key)}'") + ret = ("ecdsa", "ecdsa-sha2-nistp256", _pem()) + elif isinstance(public_key.curve, SECP384R1): + ret = ("ecdsa", "ecdsa-sha2-nistp384", _pem()) + elif isinstance(public_key.curve, SECP521R1): + ret = ("ecdsa", "ecdsa-sha2-nistp521", _pem()) + else: + raise ValueError(f"unsupported curve '{public_key.curve.name}'") + elif isinstance(public_key, Ed25519PublicKey): + ret = ("ed25519", "ed25519", _raw()) + elif isinstance(public_key, MLDSA44PublicKey): + ret = ("ml-dsa", "ml-dsa-44/1", _pem()) + elif isinstance(public_key, MLDSA65PublicKey): + ret = ("ml-dsa", "ml-dsa-65/1", _pem()) + elif isinstance(public_key, MLDSA87PublicKey): + ret = ("ml-dsa", "ml-dsa-87/1", _pem()) + else: + raise ValueError(f"unsupported key '{type(public_key)}'") + + return ret @classmethod def from_crypto( @@ -383,6 +398,7 @@ def _verify_ed25519_fallback(self, signature: bytes, data: bytes) -> None: def _verify(self, signature: bytes, data: bytes) -> None: """Helper to verify signature using pyca/cryptography (default).""" + # ruff: noqa: PLR0915 def _validate_type(key: object, type_: type) -> None: if not isinstance(key, type_): @@ -446,6 +462,30 @@ def _validate_curve( key = Ed25519PublicKey.from_public_bytes(public_bytes) key.verify(signature, data) + elif self.keytype == "ml-dsa" and self.scheme == "ml-dsa-44/1": + key = cast(MLDSA44PublicKey, self._crypto_key()) + _validate_type(key, MLDSA44PublicKey) + + digest = Hash(SHA512()) + digest.update(data) + key.verify(signature, b"tuf" + bytes([1]) + digest.finalize()) + + elif self.keytype == "ml-dsa" and self.scheme == "ml-dsa-65/1": + key = cast(MLDSA65PublicKey, self._crypto_key()) + _validate_type(key, MLDSA65PublicKey) + + digest = Hash(SHA512()) + digest.update(data) + key.verify(signature, b"tuf" + bytes([1]) + digest.finalize()) + + elif self.keytype == "ml-dsa" and self.scheme == "ml-dsa-87/1": + key = cast(MLDSA87PublicKey, self._crypto_key()) + _validate_type(key, MLDSA87PublicKey) + + digest = Hash(SHA512()) + digest.update(data) + key.verify(signature, b"tuf" + bytes([1]) + digest.finalize()) + else: raise ValueError(f"Unsupported public key {self.keytype}/{self.scheme}") diff --git a/tests/check_kms_signers.py b/tests/check_kms_signers.py index 9de3d5f2..5b18984c 100644 --- a/tests/check_kms_signers.py +++ b/tests/check_kms_signers.py @@ -22,50 +22,106 @@ class TestKMSKeys(unittest.TestCase): """Test that KMS keys can be used to sign.""" - pubkey = Key.from_dict( - "ab45d8d98992a4128efaea284c7ef0459557db199aeadf237ae41b915b9b5a1c", - { - "keytype": "ecdsa", - "scheme": "ecdsa-sha2-nistp256", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/ptvrXYuUc2ZaKssHhtg/IKNbO1X\ncDWlbKqLNpaK62MKdOwDz1qlp5AGHZkTY9tO09iq1F16SvVot1BQ9FJ2dw==\n-----END PUBLIC KEY-----\n" - }, - }, - ) - gcp_id = "projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ecdsa-sha2-nistp256/cryptoKeyVersions/1" + keys_to_test = [ + # ECDSA key + ( + "projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ecdsa-sha2-nistp256/cryptoKeyVersions/1", + Key.from_dict( + "ab45d8d98992a4128efaea284c7ef0459557db199aeadf237ae41b915b9b5a1c", + { + "keytype": "ecdsa", + "scheme": "ecdsa-sha2-nistp256", + "keyval": { + "public": ( + "-----BEGIN PUBLIC KEY-----\n" + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/ptvrXYuUc2ZaKssHhtg/IKNbO1X\n" + "cDWlbKqLNpaK62MKdOwDz1qlp5AGHZkTY9tO09iq1F16SvVot1BQ9FJ2dw==\n" + "-----END PUBLIC KEY-----\n" + ) + }, + }, + ), + ), + # ML-DSA-65 key + ( + "projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ml-dsa-65/cryptoKeyVersions/1", + Key.from_dict( + "64b2c1923d4f6cf74910b560e1f14c21d057446e86f16d8c3588e10c4b7f01ce", + { + "keytype": "ml-dsa", + "scheme": "ml-dsa-65/1", + "keyval": { + "public": ( + "-----BEGIN PUBLIC KEY-----\n" + "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\n" + "BQ2MpA4q\n" + "-----END PUBLIC KEY-----\n" + ) + }, + }, + ), + ), + ] def test_gcp_sign(self): - """Test that GCP KMS key works for signing - - NOTE: The KMS account is setup to only accept requests from the - Securesystemslib GitHub Action environment: test cannot pass elsewhere. - - In case of problems with KMS account, please file an issue and - assign @jku. - """ - + """Test that GCP KMS key works for signing""" data = b"data" + for gcp_id, pubkey in self.keys_to_test: + with self.subTest(scheme=pubkey.scheme): + signer = Signer.from_priv_key_uri(f"gcpkms:{gcp_id}", pubkey) + sig = signer.sign(data) - signer = Signer.from_priv_key_uri(f"gcpkms:{self.gcp_id}", self.pubkey) - sig = signer.sign(data) - - self.pubkey.verify_signature(sig, data) - with self.assertRaises(UnverifiedSignatureError): - self.pubkey.verify_signature(sig, b"NOT DATA") + pubkey.verify_signature(sig, data) + with self.assertRaises(UnverifiedSignatureError): + pubkey.verify_signature(sig, b"NOT DATA") def test_gcp_import(self): - """Test that GCP KMS key can be imported - - NOTE: The KMS account is setup to only accept requests from the - Securesystemslib GitHub Action environment: test cannot pass elsewhere. - - In case of problems with KMS account, please file an issue and - assign @jku. - """ - - uri, key = GCPSigner.import_(self.gcp_id) - self.assertEqual(key, self.pubkey) - self.assertEqual(uri, f"gcpkms:{self.gcp_id}") + """Test that GCP KMS key can be imported""" + for gcp_id, pubkey in self.keys_to_test: + with self.subTest(scheme=pubkey.scheme): + uri, key = GCPSigner.import_(gcp_id) + self.assertEqual(key, pubkey) + self.assertEqual(uri, f"gcpkms:{gcp_id}") if __name__ == "__main__": diff --git a/tests/data/pems/mldsa44_private.pem b/tests/data/pems/mldsa44_private.pem new file mode 100644 index 00000000..9078f2ed --- /dev/null +++ b/tests/data/pems/mldsa44_private.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMRBCKAILUIf4ZObgpXxhzI2OZbF9+GQfka1oc54fxm +RthXpJm1 +-----END PRIVATE KEY----- diff --git a/tests/data/pems/mldsa65_private.pem b/tests/data/pems/mldsa65_private.pem new file mode 100644 index 00000000..cdffae2a --- /dev/null +++ b/tests/data/pems/mldsa65_private.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMSBCKAICw6KtFzMPlpAXN76WPayM9aVwQoJxJx9BdK +gMlVDsR3 +-----END PRIVATE KEY----- diff --git a/tests/data/pems/mldsa87_private.pem b/tests/data/pems/mldsa87_private.pem new file mode 100644 index 00000000..385c0bad --- /dev/null +++ b/tests/data/pems/mldsa87_private.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMTBCKAIE0TGb7givf1aCnE6kWBWwTTjnXEijDI0ZOW +8Pw+g3jg +-----END PRIVATE KEY----- diff --git a/tests/test_signer.py b/tests/test_signer.py index 641d78ea..cbd30948 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -9,7 +9,6 @@ from pathlib import Path from typing import Any -from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from cryptography.hazmat.primitives.serialization import ( load_pem_private_key, load_pem_public_key, @@ -608,20 +607,48 @@ class TestCryptoSigner(unittest.TestCase): @classmethod def setUpClass(cls): - cls.keys: list[PrivateKeyTypes] = [] - for keytype in ["rsa", "ecdsa", "ed25519"]: - path = PEMS_DIR / f"{keytype}_private.pem" + cls.keys = {} + cls.schemes = {} + cls.expected_keytypes = {} + + key_info = [ + ( + "rsa", + "rsa", + [ + "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", "ecdsa", ["ecdsa-sha2-nistp256"]), + ("ed25519", "ed25519", ["ed25519"]), + ("mldsa44", "ml-dsa", ["ml-dsa-44/1"]), + ("mldsa65", "ml-dsa", ["ml-dsa-65/1"]), + ("mldsa87", "ml-dsa", ["ml-dsa-87/1"]), + ] + + for name, keytype, schemes in key_info: + path = PEMS_DIR / f"{name}_private.pem" with open(path, "rb") as f: data = f.read() private_key = load_pem_private_key(data, None) - cls.keys.append(private_key) + cls.keys[name] = private_key + cls.expected_keytypes[name] = keytype + cls.schemes[name] = schemes def test_init(self): """Test CryptoSigner constructor.""" - for keytype, private_key in zip(["rsa", "ecdsa", "ed25519"], self.keys): + for name, private_key in self.keys.items(): + keytype = self.expected_keytypes[name] # Init w/o public key (public key is created from private key) signer = CryptoSigner(private_key) self.assertEqual(keytype, signer.public_key.keytype) @@ -631,23 +658,9 @@ def test_init(self): 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): + for name, private_key in self.keys.items(): public_key = SSlibKey.from_crypto(private_key.public_key()) - for scheme in key_schemes: + for scheme in self.schemes[name]: public_key.scheme = scheme signer = CryptoSigner(private_key, public_key) sig = signer.sign(b"DATA") @@ -747,7 +760,7 @@ def test_private_bytes(self): def test_custom_crypto_signer(self): # setup - key = self.keys[0] + key = self.keys["rsa"] pubkey = SSlibKey.from_crypto(key.public_key()) class CustomSigner(CryptoSigner):