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

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

183 statements  

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