1"""Key interface and the default implementations"""
2
3from __future__ import annotations
4
5import logging
6import warnings
7from abc import ABCMeta, abstractmethod
8from typing import Any, cast
9
10from securesystemslib._internal.utils import make_hashable
11from securesystemslib._vendor.ed25519.ed25519 import (
12 SignatureMismatch,
13 checkvalid,
14)
15from securesystemslib.exceptions import (
16 UnsupportedLibraryError,
17 UnverifiedSignatureError,
18 VerificationError,
19)
20from securesystemslib.signer._constants import (
21 ECDSA_SHA2_NISTP256,
22 ECDSA_SHA2_NISTP384,
23 ECDSA_SHA2_NISTP521,
24 ED25519,
25 KEY_TYPE_ECDSA,
26 KEY_TYPE_ED25519,
27 KEY_TYPE_MLDSA,
28 KEY_TYPE_RSA,
29 MLDSA_44_1,
30 MLDSA_65_1,
31 MLDSA_87_1,
32 RSA_PKCS1V15_SHA224,
33 RSA_PKCS1V15_SHA256,
34 RSA_PKCS1V15_SHA384,
35 RSA_PKCS1V15_SHA512,
36 RSASSA_PSS_SHA224,
37 RSASSA_PSS_SHA256,
38 RSASSA_PSS_SHA384,
39 RSASSA_PSS_SHA512,
40)
41from securesystemslib.signer._signature import Signature
42from securesystemslib.signer._utils import compute_default_keyid, get_mldsa_payload
43
44CRYPTO_IMPORT_ERROR = None
45MLDSA_IMPORT_ERROR = None
46try:
47 from cryptography.exceptions import InvalidSignature
48 from cryptography.hazmat.primitives.asymmetric.ec import (
49 ECDSA,
50 SECP256R1,
51 SECP384R1,
52 SECP521R1,
53 EllipticCurve,
54 EllipticCurvePublicKey,
55 )
56 from cryptography.hazmat.primitives.asymmetric.ed25519 import (
57 Ed25519PublicKey,
58 )
59 from cryptography.hazmat.primitives.asymmetric.padding import (
60 MGF1,
61 PSS,
62 PKCS1v15,
63 )
64 from cryptography.hazmat.primitives.asymmetric.rsa import (
65 AsymmetricPadding,
66 RSAPublicKey,
67 )
68 from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes
69 from cryptography.hazmat.primitives.hashes import (
70 SHA256,
71 SHA384,
72 SHA512,
73 HashAlgorithm,
74 )
75 from cryptography.hazmat.primitives.serialization import (
76 Encoding,
77 PublicFormat,
78 load_pem_public_key,
79 )
80
81 from securesystemslib.signer._crypto_utils import get_hash_algorithm
82
83except ImportError:
84 CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required"
85
86try:
87 from cryptography.hazmat.primitives.asymmetric.mldsa import (
88 MLDSA44PublicKey,
89 MLDSA65PublicKey,
90 MLDSA87PublicKey,
91 )
92except ImportError:
93 MLDSA_IMPORT_ERROR = "'cryptography>=48.0.0' required for ML-DSA support"
94 MLDSA44PublicKey = None # type: ignore[assignment, misc]
95 MLDSA65PublicKey = None # type: ignore[assignment, misc]
96 MLDSA87PublicKey = None # type: ignore[assignment, misc]
97
98
99logger = logging.getLogger(__name__)
100
101# NOTE Key dispatch table is defined here so it's usable by Key,
102# but is populated in __init__.py (and can be appended by users).
103KEY_FOR_TYPE_AND_SCHEME: dict[tuple[str, str], type] = {}
104"""Key dispatch table for ``Key.from_dict()``
105
106See ``securesystemslib.signer.KEY_FOR_TYPE_AND_SCHEME`` for default key types
107and schemes, and how to register custom implementations.
108"""
109
110
111class Key(metaclass=ABCMeta):
112 """Abstract class representing the public portion of a key.
113
114 *All parameters named below are not just constructor arguments but also
115 instance attributes.*
116
117 Args:
118 keyid: Key identifier that is unique within the metadata it is used in.
119 Keyid is not verified to be the hash of a specific representation
120 of the key.
121 keytype: Key type, e.g. ``KEY_TYPE_RSA``, ``KEY_TYPE_ED25519`` or
122 ``KEY_TYPE_ECDSA``.
123 scheme: Signature scheme. For example:
124 ``RSASSA_PSS_SHA256``, ``ED25519``, and ``ECDSA_SHA2_NISTP256``.
125 keyval: Opaque key content
126 unrecognized_fields: Dictionary of all attributes that are not managed
127 by Securesystemslib
128
129 Raises:
130 TypeError: Invalid type for an argument.
131 """
132
133 def __init__(
134 self,
135 keyid: str,
136 keytype: str,
137 scheme: str,
138 keyval: dict[str, Any],
139 unrecognized_fields: dict[str, Any] | None = None,
140 ):
141 if not all(
142 isinstance(at, str) for at in [keyid, keytype, scheme]
143 ) or not isinstance(keyval, dict):
144 raise TypeError("Unexpected Key attributes types!")
145 self.keyid = keyid
146 self.keytype = keytype
147 self.scheme = scheme
148 self.keyval = keyval
149
150 if unrecognized_fields is None:
151 unrecognized_fields = {}
152
153 self.unrecognized_fields = unrecognized_fields
154
155 def __eq__(self, other: Any) -> bool:
156 if not isinstance(other, Key):
157 return False
158
159 return (
160 self.keyid == other.keyid
161 and self.keytype == other.keytype
162 and self.scheme == other.scheme
163 and self.keyval == other.keyval
164 and self.unrecognized_fields == other.unrecognized_fields
165 )
166
167 def __hash__(self) -> int:
168 return hash(
169 (
170 self.keyid,
171 self.keytype,
172 self.scheme,
173 make_hashable(self.keyval),
174 make_hashable(self.unrecognized_fields),
175 )
176 )
177
178 @classmethod
179 @abstractmethod
180 def from_dict(cls, keyid: str, key_dict: dict[str, Any]) -> Key:
181 """Creates ``Key`` object from a serialization dict
182
183 Key implementations must override this factory constructor that is used
184 as a deserialization helper.
185
186 Users should call ``Key.from_dict()``: it dispatches to the actual
187 subclass implementation based on supported keys in
188 ``KEY_FOR_TYPE_AND_SCHEME``.
189
190 Raises:
191 KeyError, TypeError: Invalid arguments.
192 """
193 keytype = key_dict.get("keytype")
194 scheme = key_dict.get("scheme")
195 if (keytype, scheme) not in KEY_FOR_TYPE_AND_SCHEME:
196 raise ValueError(f"Unsupported public key {keytype}/{scheme}")
197
198 # NOTE: Explicitly not checking the keytype and scheme types to allow
199 # intoto to use (None,None) to lookup GPGKey, see issue #450
200 key_impl = KEY_FOR_TYPE_AND_SCHEME[(keytype, scheme)] # type: ignore
201 return key_impl.from_dict(keyid, key_dict) # type: ignore
202
203 @abstractmethod
204 def to_dict(self) -> dict[str, Any]:
205 """Returns a serialization dict.
206
207 Key implementations must override this serialization helper.
208 """
209 raise NotImplementedError
210
211 def _to_dict(self) -> dict[str, Any]:
212 """Serialization helper to add base Key fields to a dict.
213
214 Key implementations may call this in their to_dict, which they must
215 still provide, in order to avoid unnoticed serialization accidents.
216 """
217 return {
218 "keytype": self.keytype,
219 "scheme": self.scheme,
220 "keyval": self.keyval,
221 **self.unrecognized_fields,
222 }
223
224 @staticmethod
225 def _from_dict(key_dict: dict[str, Any]) -> tuple[str, str, dict[str, Any]]:
226 """Deserialization helper to pop base Key fields off the dict.
227
228 Key implementations may call this in their from_dict, in order to parse
229 out common fields. But they have to create the Key instance themselves.
230 """
231 keytype = key_dict.pop("keytype")
232 scheme = key_dict.pop("scheme")
233 keyval = key_dict.pop("keyval")
234
235 return keytype, scheme, keyval
236
237 @abstractmethod
238 def verify_signature(self, signature: Signature, data: bytes) -> None:
239 """Raises if verification of signature over data fails.
240
241 Args:
242 signature: Signature object.
243 data: Payload bytes.
244
245 Raises:
246 UnverifiedSignatureError: Failed to verify signature.
247 VerificationError: Signature verification process error. If you
248 are only interested in the verify result, just handle
249 UnverifiedSignatureError: it contains VerificationError as well
250 """
251 raise NotImplementedError
252
253
254class SSlibKey(Key):
255 """Key implementation for RSA, Ed25519, ECDSA and ML-DSA keys"""
256
257 def __init__(
258 self,
259 keyid: str,
260 keytype: str,
261 scheme: str,
262 keyval: dict[str, Any],
263 unrecognized_fields: dict[str, Any] | None = None,
264 ):
265 if "public" not in keyval or not isinstance(keyval["public"], str):
266 raise ValueError(f"public key string required for scheme {scheme}")
267 super().__init__(keyid, keytype, scheme, keyval, unrecognized_fields)
268
269 def get_hash_algorithm_name(self) -> str:
270 """Get hash algorithm name for scheme. Raise
271 ValueError if the scheme is not a supported pre-hash scheme."""
272 if self.scheme in [
273 RSASSA_PSS_SHA224,
274 RSASSA_PSS_SHA256,
275 RSASSA_PSS_SHA384,
276 RSASSA_PSS_SHA512,
277 RSA_PKCS1V15_SHA224,
278 RSA_PKCS1V15_SHA256,
279 RSA_PKCS1V15_SHA384,
280 RSA_PKCS1V15_SHA512,
281 ECDSA_SHA2_NISTP256,
282 ECDSA_SHA2_NISTP384,
283 ]:
284 return f"sha{self.scheme[-3:]}"
285
286 elif self.scheme in [
287 ECDSA_SHA2_NISTP521,
288 MLDSA_44_1,
289 MLDSA_65_1,
290 MLDSA_87_1,
291 ]:
292 return "sha512"
293
294 raise ValueError(f"method not supported for scheme {self.scheme}")
295
296 def get_padding_name(self) -> str:
297 """Get padding name for scheme. Raise
298 ValueError if the scheme is not a supported padded rsa scheme."""
299 if self.scheme in [
300 RSASSA_PSS_SHA224,
301 RSASSA_PSS_SHA256,
302 RSASSA_PSS_SHA384,
303 RSASSA_PSS_SHA512,
304 RSA_PKCS1V15_SHA224,
305 RSA_PKCS1V15_SHA256,
306 RSA_PKCS1V15_SHA384,
307 RSA_PKCS1V15_SHA512,
308 ]:
309 return self.scheme.split("-")[1]
310
311 raise ValueError(f"method not supported for scheme {self.scheme}")
312
313 @classmethod
314 def from_dict(cls, keyid: str, key_dict: dict[str, Any]) -> SSlibKey:
315 keytype, scheme, keyval = cls._from_dict(key_dict)
316
317 # All fields left in the key_dict are unrecognized.
318 return cls(keyid, keytype, scheme, keyval, key_dict)
319
320 def to_dict(self) -> dict[str, Any]:
321 return self._to_dict()
322
323 def _crypto_key(self) -> PublicKeyTypes:
324 """Helper to get a `cryptography` public key for this SSlibKey."""
325 if self.keytype == KEY_TYPE_ED25519:
326 return Ed25519PublicKey.from_public_bytes(
327 bytes.fromhex(self.keyval["public"])
328 )
329 public_bytes = self.keyval["public"].encode("utf-8")
330 return load_pem_public_key(public_bytes)
331
332 @staticmethod
333 def _from_crypto(public_key: PublicKeyTypes) -> tuple[str, str, str]:
334 """Return tuple of keytype, default scheme and serialized public key
335 value for the passed public key.
336
337 Raise ValueError if public key is not supported.
338 """
339
340 def _raw() -> str:
341 return public_key.public_bytes(
342 encoding=Encoding.Raw, format=PublicFormat.Raw
343 ).hex()
344
345 def _pem() -> str:
346 return public_key.public_bytes(
347 encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
348 ).decode()
349
350 if isinstance(public_key, RSAPublicKey):
351 ret = (KEY_TYPE_RSA, RSASSA_PSS_SHA256, _pem())
352 elif isinstance(public_key, EllipticCurvePublicKey):
353 if isinstance(public_key.curve, SECP256R1):
354 ret = (KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP256, _pem())
355 elif isinstance(public_key.curve, SECP384R1):
356 ret = (KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP384, _pem())
357 elif isinstance(public_key.curve, SECP521R1):
358 ret = (KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP521, _pem())
359 else:
360 raise ValueError(f"unsupported curve '{public_key.curve.name}'")
361 elif isinstance(public_key, Ed25519PublicKey):
362 ret = (KEY_TYPE_ED25519, ED25519, _raw())
363 # ML-DSA key types may be None as fallback for cryptography < 48
364 elif MLDSA44PublicKey is not None and isinstance(public_key, MLDSA44PublicKey):
365 ret = (KEY_TYPE_MLDSA, MLDSA_44_1, _pem())
366 elif MLDSA65PublicKey is not None and isinstance(public_key, MLDSA65PublicKey):
367 ret = (KEY_TYPE_MLDSA, MLDSA_65_1, _pem())
368 elif MLDSA87PublicKey is not None and isinstance(public_key, MLDSA87PublicKey):
369 ret = (KEY_TYPE_MLDSA, MLDSA_87_1, _pem())
370 else:
371 raise ValueError(f"unsupported key '{type(public_key)}'")
372
373 return ret
374
375 @classmethod
376 def from_crypto(
377 cls,
378 public_key: PublicKeyTypes,
379 keyid: str | None = None,
380 scheme: str | None = None,
381 ) -> SSlibKey:
382 """Create SSlibKey from pyca/cryptography public key.
383
384 Args:
385 public_key: pyca/cryptography public key object.
386 keyid: Key identifier. If not passed, a default keyid is computed.
387 scheme: SSlibKey signing scheme. If not passed, default value is decided
388 based on the public key content.
389
390 Raises:
391 UnsupportedLibraryError: pyca/cryptography not installed
392 ValueError: Key type not supported
393
394 Returns:
395 SSlibKey
396
397 """
398 if CRYPTO_IMPORT_ERROR:
399 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR)
400
401 keytype, default_scheme, public_key_value = cls._from_crypto(public_key)
402
403 if not scheme:
404 scheme = default_scheme
405
406 keyval = {"public": public_key_value}
407
408 if not keyid:
409 keyid = compute_default_keyid(keytype, scheme, keyval)
410
411 return SSlibKey(keyid, keytype, scheme, keyval)
412
413 @staticmethod
414 def _get_rsa_padding(name: str, hash_algorithm: HashAlgorithm) -> AsymmetricPadding:
415 """Helper to return rsa signature padding for name."""
416 padding: AsymmetricPadding
417 if name == "pss":
418 padding = PSS(mgf=MGF1(hash_algorithm), salt_length=PSS.AUTO)
419
420 if name == "pkcs1v15":
421 padding = PKCS1v15()
422
423 return padding
424
425 def _verify_ed25519_fallback(self, signature: bytes, data: bytes) -> None:
426 """Helper to verify ed25519 sig if pyca/cryptography is unavailable."""
427 try:
428 public_bytes = bytes.fromhex(self.keyval["public"])
429 checkvalid(signature, data, public_bytes)
430
431 except SignatureMismatch as e:
432 raise UnverifiedSignatureError from e
433
434 def _verify(self, signature: bytes, data: bytes) -> None: # noqa: PLR0912, PLR0915
435 """Helper to verify signature using pyca/cryptography (default)."""
436
437 def _validate_type(key: object, type_: type) -> None:
438 if not isinstance(key, type_):
439 raise ValueError(f"bad key {key} for {self.scheme}")
440
441 def _validate_curve(
442 key: EllipticCurvePublicKey, curve: type[EllipticCurve]
443 ) -> None:
444 if not isinstance(key.curve, curve):
445 raise ValueError(f"bad curve {key.curve} for {self.scheme}")
446
447 if self.keytype == KEY_TYPE_MLDSA and MLDSA_IMPORT_ERROR:
448 raise UnsupportedLibraryError(MLDSA_IMPORT_ERROR)
449
450 try:
451 key: PublicKeyTypes
452 if self.keytype == KEY_TYPE_RSA and self.scheme in [
453 RSASSA_PSS_SHA224,
454 RSASSA_PSS_SHA256,
455 RSASSA_PSS_SHA384,
456 RSASSA_PSS_SHA512,
457 RSA_PKCS1V15_SHA224,
458 RSA_PKCS1V15_SHA256,
459 RSA_PKCS1V15_SHA384,
460 RSA_PKCS1V15_SHA512,
461 ]:
462 key = cast(RSAPublicKey, self._crypto_key())
463 _validate_type(key, RSAPublicKey)
464 hash_name = self.get_hash_algorithm_name()
465 hash_algorithm = get_hash_algorithm(hash_name)
466 padding_name = self.get_padding_name()
467 padding = self._get_rsa_padding(padding_name, hash_algorithm)
468 key.verify(signature, data, padding, hash_algorithm)
469
470 elif (
471 self.keytype in [KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP256]
472 and self.scheme == ECDSA_SHA2_NISTP256
473 ):
474 if self.keytype == ECDSA_SHA2_NISTP256:
475 warnings.warn(
476 f"keytype '{ECDSA_SHA2_NISTP256}' is deprecated, "
477 f"use '{KEY_TYPE_ECDSA}' instead",
478 DeprecationWarning,
479 stacklevel=2,
480 )
481 key = cast(EllipticCurvePublicKey, self._crypto_key())
482 _validate_type(key, EllipticCurvePublicKey)
483 _validate_curve(key, SECP256R1)
484 key.verify(signature, data, ECDSA(SHA256()))
485
486 elif (
487 self.keytype in [KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP384]
488 and self.scheme == ECDSA_SHA2_NISTP384
489 ):
490 if self.keytype == ECDSA_SHA2_NISTP384:
491 warnings.warn(
492 f"keytype '{ECDSA_SHA2_NISTP384}' is deprecated, "
493 f"use '{KEY_TYPE_ECDSA}' instead",
494 DeprecationWarning,
495 stacklevel=2,
496 )
497 key = cast(EllipticCurvePublicKey, self._crypto_key())
498 _validate_type(key, EllipticCurvePublicKey)
499 _validate_curve(key, SECP384R1)
500 key.verify(signature, data, ECDSA(SHA384()))
501
502 elif (
503 self.keytype in [KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP521]
504 and self.scheme == ECDSA_SHA2_NISTP521
505 ):
506 if self.keytype == ECDSA_SHA2_NISTP521:
507 warnings.warn(
508 f"keytype '{ECDSA_SHA2_NISTP521}' is deprecated, "
509 f"use '{KEY_TYPE_ECDSA}' instead",
510 DeprecationWarning,
511 stacklevel=2,
512 )
513 key = cast(EllipticCurvePublicKey, self._crypto_key())
514 _validate_type(key, EllipticCurvePublicKey)
515 _validate_curve(key, SECP521R1)
516 key.verify(signature, data, ECDSA(SHA512()))
517
518 elif self.keytype == KEY_TYPE_ED25519 and self.scheme == ED25519:
519 key = cast(Ed25519PublicKey, self._crypto_key())
520 _validate_type(key, Ed25519PublicKey)
521 key.verify(signature, data)
522
523 elif self.keytype == KEY_TYPE_MLDSA and self.scheme == MLDSA_44_1:
524 key = cast(MLDSA44PublicKey, self._crypto_key())
525 _validate_type(key, MLDSA44PublicKey)
526 key.verify(signature, get_mldsa_payload(data, 1))
527
528 elif self.keytype == KEY_TYPE_MLDSA and self.scheme == MLDSA_65_1:
529 key = cast(MLDSA65PublicKey, self._crypto_key())
530 _validate_type(key, MLDSA65PublicKey)
531 key.verify(signature, get_mldsa_payload(data, 1))
532
533 elif self.keytype == KEY_TYPE_MLDSA and self.scheme == MLDSA_87_1:
534 key = cast(MLDSA87PublicKey, self._crypto_key())
535 _validate_type(key, MLDSA87PublicKey)
536 key.verify(signature, get_mldsa_payload(data, 1))
537
538 else:
539 raise ValueError(f"Unsupported public key {self.keytype}/{self.scheme}")
540
541 except InvalidSignature as e:
542 raise UnverifiedSignatureError from e
543
544 def verify_signature(self, signature: Signature, data: bytes) -> None:
545 try:
546 if signature.keyid != self.keyid:
547 raise ValueError(
548 f"keyid mismatch: 'key id: {self.keyid}"
549 f" != signature keyid: {signature.keyid}'"
550 )
551
552 signature_bytes = bytes.fromhex(signature.signature)
553
554 if CRYPTO_IMPORT_ERROR:
555 if self.scheme != ED25519:
556 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR)
557
558 return self._verify_ed25519_fallback(signature_bytes, data)
559
560 return self._verify(signature_bytes, data)
561
562 except UnverifiedSignatureError as e:
563 raise UnverifiedSignatureError(
564 f"Failed to verify signature by {self.keyid}"
565 ) from e
566
567 except Exception as e:
568 logger.info("Key %s failed to verify sig: %s", self.keyid, e)
569 raise VerificationError(
570 f"Unknown failure to verify signature by {self.keyid}"
571 ) from e