Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/securesystemslib/signer/_key.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

223 statements  

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