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 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 public_bytes = self.keyval["public"].encode("utf-8")
326 return load_pem_public_key(public_bytes)
327
328 @staticmethod
329 def _from_crypto(public_key: PublicKeyTypes) -> tuple[str, str, str]:
330 """Return tuple of keytype, default scheme and serialized public key
331 value for the passed public key.
332
333 Raise ValueError if public key is not supported.
334 """
335
336 def _raw() -> str:
337 return public_key.public_bytes(
338 encoding=Encoding.Raw, format=PublicFormat.Raw
339 ).hex()
340
341 def _pem() -> str:
342 return public_key.public_bytes(
343 encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
344 ).decode()
345
346 if isinstance(public_key, RSAPublicKey):
347 ret = (KEY_TYPE_RSA, RSASSA_PSS_SHA256, _pem())
348 elif isinstance(public_key, EllipticCurvePublicKey):
349 if isinstance(public_key.curve, SECP256R1):
350 ret = (KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP256, _pem())
351 elif isinstance(public_key.curve, SECP384R1):
352 ret = (KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP384, _pem())
353 elif isinstance(public_key.curve, SECP521R1):
354 ret = (KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP521, _pem())
355 else:
356 raise ValueError(f"unsupported curve '{public_key.curve.name}'")
357 elif isinstance(public_key, Ed25519PublicKey):
358 ret = (KEY_TYPE_ED25519, ED25519, _raw())
359 # ML-DSA key types may be None as fallback for cryptography < 48
360 elif MLDSA44PublicKey is not None and isinstance(public_key, MLDSA44PublicKey):
361 ret = (KEY_TYPE_MLDSA, MLDSA_44_1, _pem())
362 elif MLDSA65PublicKey is not None and isinstance(public_key, MLDSA65PublicKey):
363 ret = (KEY_TYPE_MLDSA, MLDSA_65_1, _pem())
364 elif MLDSA87PublicKey is not None and isinstance(public_key, MLDSA87PublicKey):
365 ret = (KEY_TYPE_MLDSA, MLDSA_87_1, _pem())
366 else:
367 raise ValueError(f"unsupported key '{type(public_key)}'")
368
369 return ret
370
371 @classmethod
372 def from_crypto(
373 cls,
374 public_key: PublicKeyTypes,
375 keyid: str | None = None,
376 scheme: str | None = None,
377 ) -> SSlibKey:
378 """Create SSlibKey from pyca/cryptography public key.
379
380 Args:
381 public_key: pyca/cryptography public key object.
382 keyid: Key identifier. If not passed, a default keyid is computed.
383 scheme: SSlibKey signing scheme. Defaults are ``RSASSA_PSS_SHA256``,
384 ``ECDSA_SHA2_NISTP256``, ``ECDSA_SHA2_NISTP384`` and ``ED25519``
385 according to the keytype.
386
387 Raises:
388 UnsupportedLibraryError: pyca/cryptography not installed
389 ValueError: Key type not supported
390
391 Returns:
392 SSlibKey
393
394 """
395 if CRYPTO_IMPORT_ERROR:
396 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR)
397
398 keytype, default_scheme, public_key_value = cls._from_crypto(public_key)
399
400 if not scheme:
401 scheme = default_scheme
402
403 keyval = {"public": public_key_value}
404
405 if not keyid:
406 keyid = compute_default_keyid(keytype, scheme, keyval)
407
408 return SSlibKey(keyid, keytype, scheme, keyval)
409
410 @staticmethod
411 def _get_rsa_padding(name: str, hash_algorithm: HashAlgorithm) -> AsymmetricPadding:
412 """Helper to return rsa signature padding for name."""
413 padding: AsymmetricPadding
414 if name == "pss":
415 padding = PSS(mgf=MGF1(hash_algorithm), salt_length=PSS.AUTO)
416
417 if name == "pkcs1v15":
418 padding = PKCS1v15()
419
420 return padding
421
422 def _verify_ed25519_fallback(self, signature: bytes, data: bytes) -> None:
423 """Helper to verify ed25519 sig if pyca/cryptography is unavailable."""
424 try:
425 public_bytes = bytes.fromhex(self.keyval["public"])
426 checkvalid(signature, data, public_bytes)
427
428 except SignatureMismatch as e:
429 raise UnverifiedSignatureError from e
430
431 def _verify(self, signature: bytes, data: bytes) -> None: # noqa: PLR0912, PLR0915
432 """Helper to verify signature using pyca/cryptography (default)."""
433
434 def _validate_type(key: object, type_: type) -> None:
435 if not isinstance(key, type_):
436 raise ValueError(f"bad key {key} for {self.scheme}")
437
438 def _validate_curve(
439 key: EllipticCurvePublicKey, curve: type[EllipticCurve]
440 ) -> None:
441 if not isinstance(key.curve, curve):
442 raise ValueError(f"bad curve {key.curve} for {self.scheme}")
443
444 if self.keytype == KEY_TYPE_MLDSA and MLDSA_IMPORT_ERROR:
445 raise UnsupportedLibraryError(MLDSA_IMPORT_ERROR)
446
447 try:
448 key: PublicKeyTypes
449 if self.keytype == KEY_TYPE_RSA and self.scheme in [
450 RSASSA_PSS_SHA224,
451 RSASSA_PSS_SHA256,
452 RSASSA_PSS_SHA384,
453 RSASSA_PSS_SHA512,
454 RSA_PKCS1V15_SHA224,
455 RSA_PKCS1V15_SHA256,
456 RSA_PKCS1V15_SHA384,
457 RSA_PKCS1V15_SHA512,
458 ]:
459 key = cast(RSAPublicKey, self._crypto_key())
460 _validate_type(key, RSAPublicKey)
461 hash_name = self.get_hash_algorithm_name()
462 hash_algorithm = get_hash_algorithm(hash_name)
463 padding_name = self.get_padding_name()
464 padding = self._get_rsa_padding(padding_name, hash_algorithm)
465 key.verify(signature, data, padding, hash_algorithm)
466
467 elif (
468 self.keytype in [KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP256]
469 and self.scheme == ECDSA_SHA2_NISTP256
470 ):
471 if self.keytype == ECDSA_SHA2_NISTP256:
472 warnings.warn(
473 f"keytype '{ECDSA_SHA2_NISTP256}' is deprecated, "
474 f"use '{KEY_TYPE_ECDSA}' instead",
475 DeprecationWarning,
476 stacklevel=2,
477 )
478 key = cast(EllipticCurvePublicKey, self._crypto_key())
479 _validate_type(key, EllipticCurvePublicKey)
480 _validate_curve(key, SECP256R1)
481 key.verify(signature, data, ECDSA(SHA256()))
482
483 elif (
484 self.keytype in [KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP384]
485 and self.scheme == ECDSA_SHA2_NISTP384
486 ):
487 if self.keytype == ECDSA_SHA2_NISTP384:
488 warnings.warn(
489 f"keytype '{ECDSA_SHA2_NISTP384}' is deprecated, "
490 f"use '{KEY_TYPE_ECDSA}' instead",
491 DeprecationWarning,
492 stacklevel=2,
493 )
494 key = cast(EllipticCurvePublicKey, self._crypto_key())
495 _validate_type(key, EllipticCurvePublicKey)
496 _validate_curve(key, SECP384R1)
497 key.verify(signature, data, ECDSA(SHA384()))
498
499 elif (
500 self.keytype in [KEY_TYPE_ECDSA, ECDSA_SHA2_NISTP521]
501 and self.scheme == ECDSA_SHA2_NISTP521
502 ):
503 if self.keytype == ECDSA_SHA2_NISTP521:
504 warnings.warn(
505 f"keytype '{ECDSA_SHA2_NISTP521}' is deprecated, "
506 f"use '{KEY_TYPE_ECDSA}' instead",
507 DeprecationWarning,
508 stacklevel=2,
509 )
510 key = cast(EllipticCurvePublicKey, self._crypto_key())
511 _validate_type(key, EllipticCurvePublicKey)
512 _validate_curve(key, SECP521R1)
513 key.verify(signature, data, ECDSA(SHA512()))
514
515 elif self.keytype == KEY_TYPE_ED25519 and self.scheme == ED25519:
516 public_bytes = bytes.fromhex(self.keyval["public"])
517 key = Ed25519PublicKey.from_public_bytes(public_bytes)
518 key.verify(signature, data)
519
520 elif self.keytype == KEY_TYPE_MLDSA and self.scheme == MLDSA_44_1:
521 key = cast(MLDSA44PublicKey, self._crypto_key())
522 _validate_type(key, MLDSA44PublicKey)
523 key.verify(signature, get_mldsa_payload(data, 1))
524
525 elif self.keytype == KEY_TYPE_MLDSA and self.scheme == MLDSA_65_1:
526 key = cast(MLDSA65PublicKey, self._crypto_key())
527 _validate_type(key, MLDSA65PublicKey)
528 key.verify(signature, get_mldsa_payload(data, 1))
529
530 elif self.keytype == KEY_TYPE_MLDSA and self.scheme == MLDSA_87_1:
531 key = cast(MLDSA87PublicKey, self._crypto_key())
532 _validate_type(key, MLDSA87PublicKey)
533 key.verify(signature, get_mldsa_payload(data, 1))
534
535 else:
536 raise ValueError(f"Unsupported public key {self.keytype}/{self.scheme}")
537
538 except InvalidSignature as e:
539 raise UnverifiedSignatureError from e
540
541 def verify_signature(self, signature: Signature, data: bytes) -> None:
542 try:
543 if signature.keyid != self.keyid:
544 raise ValueError(
545 f"keyid mismatch: 'key id: {self.keyid}"
546 f" != signature keyid: {signature.keyid}'"
547 )
548
549 signature_bytes = bytes.fromhex(signature.signature)
550
551 if CRYPTO_IMPORT_ERROR:
552 if self.scheme != ED25519:
553 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR)
554
555 return self._verify_ed25519_fallback(signature_bytes, data)
556
557 return self._verify(signature_bytes, data)
558
559 except UnverifiedSignatureError as e:
560 raise UnverifiedSignatureError(
561 f"Failed to verify signature by {self.keyid}"
562 ) from e
563
564 except Exception as e:
565 logger.info("Key %s failed to verify sig: %s", self.keyid, e)
566 raise VerificationError(
567 f"Unknown failure to verify signature by {self.keyid}"
568 ) from e