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

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

170 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 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 self._private_key = private_key 

261 self._public_key = public_key 

262 

263 @property 

264 def public_key(self) -> SSlibKey: 

265 return self._public_key 

266 

267 @property 

268 def private_bytes(self) -> bytes: 

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

270 

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

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

273 return self._private_key.private_bytes( 

274 encoding=Encoding.PEM, 

275 format=PrivateFormat.PKCS8, 

276 encryption_algorithm=NoEncryption(), 

277 ) 

278 

279 @classmethod 

280 def from_priv_key_uri( 

281 cls, 

282 priv_key_uri: str, 

283 public_key: Key, 

284 secrets_handler: SecretsHandler | None = None, 

285 ) -> CryptoSigner: 

286 """Constructor for Signer to call 

287 

288 Please refer to Signer.from_priv_key_uri() documentation. 

289 

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

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

292 but are not guaranteed to work. 

293 

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

295 private key file. If CRYPTO_SIGNER_PATH_PREFIX environment variable 

296 is set, the private key will be read from 

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

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

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

300 

301 Raises: 

302 UnsupportedLibraryError: pyca/cryptography not installed. 

303 OSError: File cannot be read. 

304 ValueError: Invalid passed arguments. 

305 cryptography.exceptions.UnsupportedAlgorithm: pyca/cryptography 

306 deserialization failed. 

307 """ 

308 if CRYPTO_IMPORT_ERROR: 

309 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

310 

311 if not isinstance(public_key, SSlibKey): 

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

313 

314 uri = parse.urlparse(priv_key_uri) 

315 

316 if uri.scheme != cls.SCHEME: 

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

318 

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

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

321 try: 

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

323 private_pem = f.read() 

324 except FileNotFoundError as e: 

325 raise FileNotFoundError( 

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

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

328 ) from e 

329 

330 private_key = load_pem_private_key(private_pem, None) 

331 return CryptoSigner(private_key, public_key) 

332 

333 @staticmethod 

334 def generate_ed25519( 

335 keyid: str | None = None, 

336 ) -> CryptoSigner: 

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

338 

339 Args: 

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

341 

342 Raises: 

343 UnsupportedLibraryError: pyca/cryptography not installed 

344 """ 

345 if CRYPTO_IMPORT_ERROR: 

346 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

347 

348 private_key = Ed25519PrivateKey.generate() 

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

350 return CryptoSigner(private_key, public_key) 

351 

352 @staticmethod 

353 def generate_rsa( 

354 keyid: str | None = None, 

355 scheme: str | None = RSASSA_PSS_SHA256, 

356 size: int = 3072, 

357 ) -> CryptoSigner: 

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

359 

360 Args: 

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

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

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

364 

365 Raises: 

366 UnsupportedLibraryError: pyca/cryptography not installed 

367 """ 

368 if CRYPTO_IMPORT_ERROR: 

369 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

370 

371 private_key = generate_rsa_private_key( 

372 public_exponent=65537, 

373 key_size=size, 

374 ) 

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

376 return CryptoSigner(private_key, public_key) 

377 

378 @staticmethod 

379 def generate_ecdsa( 

380 keyid: str | None = None, 

381 scheme: str | None = None, 

382 ) -> CryptoSigner: 

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

384 

385 Args: 

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

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

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

389 

390 Raises: 

391 UnsupportedLibraryError: pyca/cryptography not installed 

392 ValueError: Invalid scheme 

393 """ 

394 if CRYPTO_IMPORT_ERROR: 

395 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

396 

397 scheme = ECDSA_SHA2_NISTP256 if scheme is None else scheme 

398 if scheme not in _ECDSA_SCHEMES: 

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

400 

401 curve, _ = _get_ecdsa_curve_and_hash(scheme) 

402 private_key = generate_ec_private_key(curve()) 

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

404 return CryptoSigner(private_key, public_key) 

405 

406 @staticmethod 

407 def generate_mldsa( 

408 keyid: str | None = None, 

409 scheme: str | None = None, 

410 ) -> CryptoSigner: 

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

412 

413 Args: 

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

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

416 

417 Raises: 

418 UnsupportedLibraryError: pyca/cryptography not installed 

419 """ 

420 if CRYPTO_IMPORT_ERROR: 

421 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

422 if MLDSA_IMPORT_ERROR: 

423 raise UnsupportedLibraryError(MLDSA_IMPORT_ERROR) 

424 

425 scheme = MLDSA_65_1 if scheme is None else scheme 

426 if scheme == MLDSA_44_1: 

427 private_key: PrivateKeyTypes = MLDSA44PrivateKey.generate() 

428 elif scheme == MLDSA_65_1: 

429 private_key = MLDSA65PrivateKey.generate() 

430 elif scheme == MLDSA_87_1: 

431 private_key = MLDSA87PrivateKey.generate() 

432 else: 

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

434 

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

436 return CryptoSigner(private_key, public_key) 

437 

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

439 if self.public_key.keytype == KEY_TYPE_MLDSA: 

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

441 payload = get_mldsa_payload(payload, 1) 

442 

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

444 

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