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

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

172 statements  

1"""Signer implementation for pyca/cryptography signing.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6import os 

7from dataclasses import astuple, dataclass 

8from typing import cast 

9from urllib import parse 

10 

11from securesystemslib.exceptions import KeyMismatchError, UnsupportedLibraryError 

12from securesystemslib.signer._constants import ( 

13 ECDSA_SHA2_NISTP256, 

14 ECDSA_SHA2_NISTP384, 

15 ECDSA_SHA2_NISTP521, 

16 ED25519, 

17 KEY_TYPE_ECDSA, 

18 KEY_TYPE_ED25519, 

19 KEY_TYPE_MLDSA, 

20 KEY_TYPE_RSA, 

21 MLDSA_44_1, 

22 MLDSA_65_1, 

23 MLDSA_87_1, 

24 RSA_PKCS1V15_SHA224, 

25 RSA_PKCS1V15_SHA256, 

26 RSA_PKCS1V15_SHA384, 

27 RSA_PKCS1V15_SHA512, 

28 RSASSA_PSS_SHA224, 

29 RSASSA_PSS_SHA256, 

30 RSASSA_PSS_SHA384, 

31 RSASSA_PSS_SHA512, 

32) 

33from securesystemslib.signer._key import Key, SSlibKey 

34from securesystemslib.signer._signature import Signature 

35from securesystemslib.signer._signer import SecretsHandler, Signer 

36from securesystemslib.signer._utils import get_mldsa_payload 

37 

38CRYPTO_IMPORT_ERROR = None 

39MLDSA_IMPORT_ERROR = None 

40try: 

41 from cryptography.hazmat.primitives.asymmetric.ec import ( 

42 ECDSA, 

43 SECP256R1, 

44 SECP384R1, 

45 SECP521R1, 

46 EllipticCurve, 

47 EllipticCurvePrivateKey, 

48 ) 

49 from cryptography.hazmat.primitives.asymmetric.ec import ( 

50 generate_private_key as generate_ec_private_key, 

51 ) 

52 from cryptography.hazmat.primitives.asymmetric.ed25519 import ( 

53 Ed25519PrivateKey, 

54 ) 

55 from cryptography.hazmat.primitives.asymmetric.padding import ( 

56 MGF1, 

57 PSS, 

58 PKCS1v15, 

59 ) 

60 from cryptography.hazmat.primitives.asymmetric.rsa import ( 

61 AsymmetricPadding, 

62 RSAPrivateKey, 

63 ) 

64 from cryptography.hazmat.primitives.asymmetric.rsa import ( 

65 generate_private_key as generate_rsa_private_key, 

66 ) 

67 from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes 

68 from cryptography.hazmat.primitives.hashes import ( 

69 SHA256, 

70 SHA384, 

71 SHA512, 

72 HashAlgorithm, 

73 ) 

74 from cryptography.hazmat.primitives.serialization import ( 

75 Encoding, 

76 NoEncryption, 

77 PrivateFormat, 

78 load_pem_private_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 

86# Handle ML-DSA support separately for cryptography 48 dependency: 

87# This should not be needed but https://github.com/secure-systems-lab/securesystemslib/issues/1203 

88try: 

89 from cryptography.hazmat.primitives.asymmetric.mldsa import ( 

90 MLDSA44PrivateKey, 

91 MLDSA65PrivateKey, 

92 MLDSA87PrivateKey, 

93 ) 

94except ImportError: 

95 MLDSA_IMPORT_ERROR = "'cryptography>=48.0.0' required for ML-DSA support" 

96 MLDSA44PrivateKey = None # type: ignore[assignment, misc] 

97 MLDSA65PrivateKey = None # type: ignore[assignment, misc] 

98 MLDSA87PrivateKey = None # type: ignore[assignment, misc] 

99 

100logger = logging.getLogger(__name__) 

101 

102 

103@dataclass 

104class _RSASignArgs: 

105 padding: AsymmetricPadding 

106 hash_algo: HashAlgorithm 

107 

108 

109@dataclass 

110class _ECDSASignArgs: 

111 sig_algo: ECDSA 

112 

113 

114@dataclass 

115class _NoSignArgs: 

116 pass 

117 

118 

119# keep in sync with _get_ecdsa_curve_and_hash() below 

120_ECDSA_SCHEMES = [ 

121 ECDSA_SHA2_NISTP256, 

122 ECDSA_SHA2_NISTP384, 

123 ECDSA_SHA2_NISTP521, 

124] 

125 

126 

127def _get_ecdsa_curve_and_hash( 

128 scheme: str, 

129) -> tuple[type[EllipticCurve], HashAlgorithm]: 

130 """Helper to return curve and hash algorithm for an ecdsa scheme. 

131 

132 An ecdsa scheme fixes both, and the pairs must agree with the ones 

133 SSlibKey._verify() uses, or signatures will not verify. 

134 """ 

135 # built here and not at module scope, so that importing this module 

136 # still works when pyca/cryptography is not installed 

137 curves_and_hashes: dict[str, tuple[type[EllipticCurve], HashAlgorithm]] = { 

138 ECDSA_SHA2_NISTP256: (SECP256R1, SHA256()), 

139 ECDSA_SHA2_NISTP384: (SECP384R1, SHA384()), 

140 ECDSA_SHA2_NISTP521: (SECP521R1, SHA512()), 

141 } 

142 

143 return curves_and_hashes[scheme] 

144 

145 

146def _get_rsa_padding(name: str, hash_algorithm: HashAlgorithm) -> AsymmetricPadding: 

147 """Helper to return rsa signature padding for name.""" 

148 padding: AsymmetricPadding 

149 if name == "pss": 

150 padding = PSS(mgf=MGF1(hash_algorithm), salt_length=PSS.DIGEST_LENGTH) 

151 

152 if name == "pkcs1v15": 

153 padding = PKCS1v15() 

154 

155 return padding 

156 

157 

158class CryptoSigner(Signer): 

159 """File-based signer using the cryptography (pyca/cryptography) library. 

160 

161 Supports signing with RSA, ECDSA, Ed25519, and ML-DSA keys. 

162 

163 The private key URI scheme is: ``file2:<PATH>``, where ``<PATH>`` is the filesystem 

164 path to a PEM-encoded PKCS#8 private key file. If the ``CRYPTO_SIGNER_PATH_PREFIX`` 

165 environment variable is set, the path will be resolved relative to that prefix. 

166 

167 A CryptoSigner can be instantiated with: 

168 

169 * ``Signer.from_priv_key_uri("file2:<PATH>", public_key)``: 

170 Generic way to load from an existing private key file. 

171 * ``CryptoSigner.generate_*()`` factory methods generate new key pairs 

172 * ``CryptoSigner(privkey, pubkey)``: Direct instantiation using existing 

173 pyca/cryptography private key objects. 

174 """ 

175 

176 SCHEME = "file2" 

177 PREFIX_ENV_VAR = "CRYPTO_SIGNER_PATH_PREFIX" 

178 

179 def __init__( 

180 self, 

181 private_key: PrivateKeyTypes, 

182 public_key: SSlibKey | None = None, 

183 ): 

184 def assert_type( 

185 name: str, key: PrivateKeyTypes, typ: type[PrivateKeyTypes] 

186 ) -> None: 

187 if not isinstance(key, typ): 

188 raise ValueError(f"invalid {name} key: {type(key)}") 

189 

190 if CRYPTO_IMPORT_ERROR: 

191 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

192 

193 if public_key is None: 

194 public_key = SSlibKey.from_crypto(private_key.public_key()) 

195 

196 if public_key.keytype == KEY_TYPE_MLDSA and MLDSA_IMPORT_ERROR: 

197 raise UnsupportedLibraryError(MLDSA_IMPORT_ERROR) 

198 

199 self._private_key: PrivateKeyTypes 

200 self._sign_args: _RSASignArgs | _ECDSASignArgs | _NoSignArgs 

201 

202 if public_key.keytype == KEY_TYPE_RSA and public_key.scheme in [ 

203 RSASSA_PSS_SHA224, 

204 RSASSA_PSS_SHA256, 

205 RSASSA_PSS_SHA384, 

206 RSASSA_PSS_SHA512, 

207 RSA_PKCS1V15_SHA224, 

208 RSA_PKCS1V15_SHA256, 

209 RSA_PKCS1V15_SHA384, 

210 RSA_PKCS1V15_SHA512, 

211 ]: 

212 assert_type(KEY_TYPE_RSA, private_key, RSAPrivateKey) 

213 

214 hash_name = public_key.get_hash_algorithm_name() 

215 hash_algo = get_hash_algorithm(hash_name) 

216 

217 padding_name = public_key.get_padding_name() 

218 padding = _get_rsa_padding(padding_name, hash_algo) 

219 

220 self._sign_args = _RSASignArgs(padding, hash_algo) 

221 

222 # for backwards compat the spec-deprecated ecdsa keytypes (which are 

223 # named after the scheme) are accepted in addition to "ecdsa" 

224 elif ( 

225 public_key.keytype in [KEY_TYPE_ECDSA, public_key.scheme] 

226 and public_key.scheme in _ECDSA_SCHEMES 

227 ): 

228 assert_type(KEY_TYPE_ECDSA, private_key, EllipticCurvePrivateKey) 

229 ec_key = cast(EllipticCurvePrivateKey, private_key) 

230 

231 curve, hash_algo = _get_ecdsa_curve_and_hash(public_key.scheme) 

232 if not isinstance(ec_key.curve, curve): 

233 raise ValueError( 

234 f"bad curve {ec_key.curve.name} for {public_key.scheme}" 

235 ) 

236 

237 self._sign_args = _ECDSASignArgs(ECDSA(hash_algo)) 

238 

239 elif public_key.keytype == KEY_TYPE_ED25519 and public_key.scheme == ED25519: 

240 assert_type(KEY_TYPE_ED25519, private_key, Ed25519PrivateKey) 

241 self._sign_args = _NoSignArgs() 

242 

243 elif public_key.keytype == KEY_TYPE_MLDSA and public_key.scheme == MLDSA_44_1: 

244 assert_type(KEY_TYPE_MLDSA, private_key, MLDSA44PrivateKey) 

245 self._sign_args = _NoSignArgs() 

246 

247 elif public_key.keytype == KEY_TYPE_MLDSA and public_key.scheme == MLDSA_65_1: 

248 assert_type(KEY_TYPE_MLDSA, private_key, MLDSA65PrivateKey) 

249 self._sign_args = _NoSignArgs() 

250 

251 elif public_key.keytype == KEY_TYPE_MLDSA and public_key.scheme == MLDSA_87_1: 

252 assert_type(KEY_TYPE_MLDSA, private_key, MLDSA87PrivateKey) 

253 self._sign_args = _NoSignArgs() 

254 

255 else: 

256 raise ValueError( 

257 f"unsupported public key {public_key.keytype}/{public_key.scheme}" 

258 ) 

259 

260 if public_key._crypto_key() != private_key.public_key(): 

261 raise KeyMismatchError("CryptoSigner private key does not match public key") 

262 

263 self._private_key = private_key 

264 self._public_key = public_key 

265 

266 @property 

267 def public_key(self) -> SSlibKey: 

268 return self._public_key 

269 

270 @property 

271 def private_bytes(self) -> bytes: 

272 """Return the PEM encoded PKCS8 format private key as bytes 

273 

274 The return value can be used as file content when a Signer is loaded with 

275 `Signer.from_priv_key_uri('file2:<FILEPATH>')`.""" 

276 return self._private_key.private_bytes( 

277 encoding=Encoding.PEM, 

278 format=PrivateFormat.PKCS8, 

279 encryption_algorithm=NoEncryption(), 

280 ) 

281 

282 @classmethod 

283 def from_priv_key_uri( 

284 cls, 

285 priv_key_uri: str, 

286 public_key: Key, 

287 secrets_handler: SecretsHandler | None = None, 

288 ) -> CryptoSigner: 

289 """Constructor for Signer to call 

290 

291 Please refer to Signer.from_priv_key_uri() documentation. 

292 

293 NOTE: pyca/cryptography is used to deserialize the key data. The 

294 expected (and tested) encoding/format is PEM/PKCS8. Other formats may 

295 but are not guaranteed to work. 

296 

297 URI has the format "file2:<PATH>", where PATH is a filesystem path to the 

298 private key file. If CRYPTO_SIGNER_PATH_PREFIX environment variable 

299 is set, the private key will be read from 

300 ``CRYPTO_SIGNER_PATH_PREFIX + <SEPARATOR> + PATH``. The purpose of this 

301 is to allow PATH to only encode an identifier (e.g. filename) while allowing 

302 the signing system to store the private keys whereever it wants at runtime. 

303 

304 Raises: 

305 UnsupportedLibraryError: pyca/cryptography not installed. 

306 KeyMismatchError: Private key does not match public key. 

307 OSError: File cannot be read. 

308 ValueError: Invalid passed arguments. 

309 cryptography.exceptions.UnsupportedAlgorithm: pyca/cryptography 

310 deserialization failed. 

311 """ 

312 if CRYPTO_IMPORT_ERROR: 

313 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

314 

315 if not isinstance(public_key, SSlibKey): 

316 raise ValueError(f"Expected SSlibKey for {priv_key_uri}") 

317 

318 uri = parse.urlparse(priv_key_uri) 

319 

320 if uri.scheme != cls.SCHEME: 

321 raise ValueError(f"CryptoSigner does not support {priv_key_uri}") 

322 

323 prefix = os.environ.get(cls.PREFIX_ENV_VAR) 

324 path = os.path.join(prefix, uri.path) if prefix else uri.path 

325 try: 

326 with open(path, "rb") as f: 

327 private_pem = f.read() 

328 except FileNotFoundError as e: 

329 raise FileNotFoundError( 

330 f"Private key not found in '{path}' (with ", 

331 f"{cls.PREFIX_ENV_VAR}: {prefix}, path: {uri.path})", 

332 ) from e 

333 

334 private_key = load_pem_private_key(private_pem, None) 

335 return CryptoSigner(private_key, public_key) 

336 

337 @staticmethod 

338 def generate_ed25519( 

339 keyid: str | None = None, 

340 ) -> CryptoSigner: 

341 """Generate new key pair as "ed25519" signer. 

342 

343 Args: 

344 keyid: Key identifier. If not passed, a default keyid is computed. 

345 

346 Raises: 

347 UnsupportedLibraryError: pyca/cryptography not installed 

348 """ 

349 if CRYPTO_IMPORT_ERROR: 

350 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

351 

352 private_key = Ed25519PrivateKey.generate() 

353 public_key = SSlibKey.from_crypto(private_key.public_key(), keyid, ED25519) 

354 return CryptoSigner(private_key, public_key) 

355 

356 @staticmethod 

357 def generate_rsa( 

358 keyid: str | None = None, 

359 scheme: str | None = RSASSA_PSS_SHA256, 

360 size: int = 3072, 

361 ) -> CryptoSigner: 

362 """Generate new key pair as rsa signer. 

363 

364 Args: 

365 keyid: Key identifier. If not passed, a default keyid is computed. 

366 scheme: RSA signing scheme. Default is "rsassa-pss-sha256". 

367 size: RSA key size in bits. Default is 3072. 

368 

369 Raises: 

370 UnsupportedLibraryError: pyca/cryptography not installed 

371 """ 

372 if CRYPTO_IMPORT_ERROR: 

373 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

374 

375 private_key = generate_rsa_private_key( 

376 public_exponent=65537, 

377 key_size=size, 

378 ) 

379 public_key = SSlibKey.from_crypto(private_key.public_key(), keyid, scheme) 

380 return CryptoSigner(private_key, public_key) 

381 

382 @staticmethod 

383 def generate_ecdsa( 

384 keyid: str | None = None, 

385 scheme: str | None = None, 

386 ) -> CryptoSigner: 

387 """Generate new key pair for an ecdsa signer. 

388 

389 Args: 

390 keyid: Key identifier. If not passed, a default keyid is computed. 

391 scheme: A valid ecdsa scheme, which also selects the curve. If not 

392 passed, "ecdsa-sha2-nistp256" is used. 

393 

394 Raises: 

395 UnsupportedLibraryError: pyca/cryptography not installed 

396 ValueError: Invalid scheme 

397 """ 

398 if CRYPTO_IMPORT_ERROR: 

399 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

400 

401 scheme = ECDSA_SHA2_NISTP256 if scheme is None else scheme 

402 if scheme not in _ECDSA_SCHEMES: 

403 raise ValueError(f"Invalid scheme for ecdsa: {scheme}") 

404 

405 curve, _ = _get_ecdsa_curve_and_hash(scheme) 

406 private_key = generate_ec_private_key(curve()) 

407 public_key = SSlibKey.from_crypto(private_key.public_key(), keyid, scheme) 

408 return CryptoSigner(private_key, public_key) 

409 

410 @staticmethod 

411 def generate_mldsa( 

412 keyid: str | None = None, 

413 scheme: str | None = None, 

414 ) -> CryptoSigner: 

415 """Generate new key pair for a ML-DSA signer. 

416 

417 Args: 

418 keyid: Key identifier. If not passed, a default keyid is computed. 

419 scheme: A valid key scheme for ml-dsa. If not passed, "ml-dsa-65/1" is used 

420 

421 Raises: 

422 UnsupportedLibraryError: pyca/cryptography not installed 

423 """ 

424 if CRYPTO_IMPORT_ERROR: 

425 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

426 if MLDSA_IMPORT_ERROR: 

427 raise UnsupportedLibraryError(MLDSA_IMPORT_ERROR) 

428 

429 scheme = MLDSA_65_1 if scheme is None else scheme 

430 if scheme == MLDSA_44_1: 

431 private_key: PrivateKeyTypes = MLDSA44PrivateKey.generate() 

432 elif scheme == MLDSA_65_1: 

433 private_key = MLDSA65PrivateKey.generate() 

434 elif scheme == MLDSA_87_1: 

435 private_key = MLDSA87PrivateKey.generate() 

436 else: 

437 raise ValueError(f"Invalid scheme for ML-DSA: {scheme}") 

438 

439 public_key = SSlibKey.from_crypto(private_key.public_key(), keyid, scheme) 

440 return CryptoSigner(private_key, public_key) 

441 

442 def sign(self, payload: bytes) -> Signature: 

443 if self.public_key.keytype == KEY_TYPE_MLDSA: 

444 # ml-dsa keytype specifies a domain-specific hash prefixing scheme 

445 payload = get_mldsa_payload(payload, 1) 

446 

447 sig = self._private_key.sign(payload, *astuple(self._sign_args)) # type: ignore 

448 

449 return Signature(self.public_key.keyid, sig.hex())