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