Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion requirements-pinned.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions securesystemslib/signer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
67 changes: 52 additions & 15 deletions securesystemslib/signer/_crypto_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand Down Expand Up @@ -71,6 +78,14 @@ class _NoSignArgs:
pass


@dataclass
class _MLDSASignArgs:
prefix: bytes

def __init__(self, version: int) -> None:
self.prefix = b"tuf" + bytes([version])
Comment thread
jku marked this conversation as resolved.
Outdated


# for backwards compat: use when spec-deprecated keytype ecdsa-sha2-nistp256
# should be accepted in addition to "ecdsa"
_ECDSA_KEYTYPES = ["ecdsa", "ecdsa-sha2-nistp256"]
Expand Down Expand Up @@ -117,14 +132,20 @@ 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)

if public_key is None:
public_key = SSlibKey.from_crypto(private_key.public_key())

self._private_key: PrivateKeyTypes
self._sign_args: _RSASignArgs | _ECDSASignArgs | _NoSignArgs
self._sign_args: _RSASignArgs | _ECDSASignArgs | _NoSignArgs | _MLDSASignArgs

if public_key.keytype == "rsa" and public_key.scheme in [
"rsassa-pss-sha224",
Expand All @@ -136,8 +157,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)
Copy link
Copy Markdown
Collaborator Author

@jku jku May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all these assert tweaks are just to keep linter happy ("too many branches"): the functionality should not change


hash_name = public_key.get_hash_algorithm_name()
hash_algo = get_hash_algorithm(hash_name)
Expand All @@ -146,31 +166,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()
self._private_key = private_key

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 = _MLDSASignArgs(1)

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 = _MLDSASignArgs(1)

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 = _MLDSASignArgs(1)

else:
raise ValueError(
f"unsupported public key {public_key.keytype}/{public_key.scheme}"
)

self._private_key = private_key
self._public_key = public_key

@property
Expand Down Expand Up @@ -321,5 +346,17 @@ 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)
):
if not isinstance(self._sign_args, _MLDSASignArgs):
raise AssertionError("Unexpected MLDSA signer state")

digest = Hash(SHA512())
digest.update(payload)

sig = self._private_key.sign(self._sign_args.prefix + digest.finalize())
else:
sig = self._private_key.sign(payload, *astuple(self._sign_args)) # type: ignore

return Signature(self.public_key.keyid, sig.hex())
26 changes: 22 additions & 4 deletions securesystemslib/signer/_gcp_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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 = (
Expand Down Expand Up @@ -176,10 +188,16 @@ def sign(self, payload: bytes) -> Signature:
# NOTE: request and response can contain CRC32C of the digest/sig:
# Verifying could be useful but would require another dependency...

hasher = hashlib.new(self.hash_algorithm)
hasher.update(payload)
digest = {self.hash_algorithm: hasher.digest()}
request = {"name": self.gcp_keyid, "digest": digest}
if self.public_key.keytype == "ml-dsa":
hasher = hashlib.new("sha512")
hasher.update(payload)
pre_signing_string = b"tuf" + bytes([1]) + hasher.digest()
request = {"name": self.gcp_keyid, "data": pre_signing_string}
else:
hasher = hashlib.new(self.hash_algorithm)
hasher.update(payload)
digest = {self.hash_algorithm: hasher.digest()}
request = {"name": self.gcp_keyid, "digest": digest}

logger.debug("signing request %s", request)
response = self.client.asymmetric_sign(request)
Expand Down
76 changes: 58 additions & 18 deletions securesystemslib/signer/_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -46,6 +51,7 @@
SHA256,
SHA384,
SHA512,
Hash,
HashAlgorithm,
)
from cryptography.hazmat.primitives.serialization import (
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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_):
Expand Down Expand Up @@ -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}")

Expand Down
Loading
Loading