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

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

121 statements  

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

2 

3import logging 

4import os 

5from dataclasses import astuple, dataclass 

6from urllib import parse 

7 

8from securesystemslib.exceptions import UnsupportedLibraryError 

9from securesystemslib.signer._key import Key, SSlibKey 

10from securesystemslib.signer._signature import Signature 

11from securesystemslib.signer._signer import SecretsHandler, Signer 

12 

13CRYPTO_IMPORT_ERROR = None 

14try: 

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

16 ECDSA, 

17 SECP256R1, 

18 EllipticCurvePrivateKey, 

19 ) 

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

21 generate_private_key as generate_ec_private_key, 

22 ) 

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

24 Ed25519PrivateKey, 

25 ) 

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

27 MGF1, 

28 PSS, 

29 PKCS1v15, 

30 ) 

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

32 AsymmetricPadding, 

33 RSAPrivateKey, 

34 ) 

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

36 generate_private_key as generate_rsa_private_key, 

37 ) 

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

39 from cryptography.hazmat.primitives.hashes import ( 

40 SHA256, 

41 HashAlgorithm, 

42 ) 

43 from cryptography.hazmat.primitives.serialization import ( 

44 Encoding, 

45 NoEncryption, 

46 PrivateFormat, 

47 load_pem_private_key, 

48 ) 

49 

50 from securesystemslib.signer._crypto_utils import get_hash_algorithm 

51 

52except ImportError: 

53 CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required" 

54 

55logger = logging.getLogger(__name__) 

56 

57 

58@dataclass 

59class _RSASignArgs: 

60 padding: "AsymmetricPadding" 

61 hash_algo: "HashAlgorithm" 

62 

63 

64@dataclass 

65class _ECDSASignArgs: 

66 sig_algo: "ECDSA" 

67 

68 

69@dataclass 

70class _NoSignArgs: 

71 pass 

72 

73 

74# for backwards compat: use when spec-deprecated keytype ecdsa-sha2-nistp256 

75# should be accepted in addition to "ecdsa" 

76_ECDSA_KEYTYPES = ["ecdsa", "ecdsa-sha2-nistp256"] 

77 

78 

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

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

81 padding: AsymmetricPadding 

82 if name == "pss": 

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

84 

85 if name == "pkcs1v15": 

86 padding = PKCS1v15() 

87 

88 return padding 

89 

90 

91class CryptoSigner(Signer): 

92 """PYCA/cryptography Signer implementations. 

93 

94 A CryptoSigner can be created from: 

95 

96 a. private key file -- see ``Signer.from_priv_key_uri()`` 

97 

98 This is the generic (not CryptoSigner specific) way to 

99 create a signer: use this when you already have a private 

100 key (and a private key URI) you can use. 

101 

102 b. newly generated key pair -- see ``CryptoSigner.generate_*()`` 

103 

104 Use this when you need a brand new private key pair. 

105 

106 c. existing pyca/cryptography private key object -- ``CryptoSigner()`` 

107 

108 Use this if you need a brand new private key pair and option 

109 b is not flexible enough for your case. 

110 """ 

111 

112 SCHEME = "file2" 

113 PREFIX_ENV_VAR = "CRYPTO_SIGNER_PATH_PREFIX" 

114 

115 def __init__( 

116 self, 

117 private_key: "PrivateKeyTypes", 

118 public_key: SSlibKey | None = None, 

119 ): 

120 if CRYPTO_IMPORT_ERROR: 

121 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

122 

123 if public_key is None: 

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

125 

126 self._private_key: PrivateKeyTypes 

127 self._sign_args: _RSASignArgs | _ECDSASignArgs | _NoSignArgs 

128 

129 if public_key.keytype == "rsa" and public_key.scheme in [ 

130 "rsassa-pss-sha224", 

131 "rsassa-pss-sha256", 

132 "rsassa-pss-sha384", 

133 "rsassa-pss-sha512", 

134 "rsa-pkcs1v15-sha224", 

135 "rsa-pkcs1v15-sha256", 

136 "rsa-pkcs1v15-sha384", 

137 "rsa-pkcs1v15-sha512", 

138 ]: 

139 if not isinstance(private_key, RSAPrivateKey): 

140 raise ValueError(f"invalid rsa key: {type(private_key)}") 

141 

142 hash_name = public_key.get_hash_algorithm_name() 

143 hash_algo = get_hash_algorithm(hash_name) 

144 

145 padding_name = public_key.get_padding_name() 

146 padding = _get_rsa_padding(padding_name, hash_algo) 

147 

148 self._sign_args = _RSASignArgs(padding, hash_algo) 

149 self._private_key = private_key 

150 

151 elif ( 

152 public_key.keytype in _ECDSA_KEYTYPES 

153 and public_key.scheme == "ecdsa-sha2-nistp256" 

154 ): 

155 if not isinstance(private_key, EllipticCurvePrivateKey): 

156 raise ValueError(f"invalid ecdsa key: {type(private_key)}") 

157 

158 signature_algorithm = ECDSA(SHA256()) 

159 self._sign_args = _ECDSASignArgs(signature_algorithm) 

160 self._private_key = private_key 

161 

162 elif public_key.keytype == "ed25519" and public_key.scheme == "ed25519": 

163 if not isinstance(private_key, Ed25519PrivateKey): 

164 raise ValueError(f"invalid ed25519 key: {type(private_key)}") 

165 

166 self._sign_args = _NoSignArgs() 

167 self._private_key = private_key 

168 

169 else: 

170 raise ValueError( 

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

172 ) 

173 

174 self._public_key = public_key 

175 

176 @property 

177 def public_key(self) -> SSlibKey: 

178 return self._public_key 

179 

180 @property 

181 def private_bytes(self) -> bytes: 

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

183 

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

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

186 return self._private_key.private_bytes( 

187 encoding=Encoding.PEM, 

188 format=PrivateFormat.PKCS8, 

189 encryption_algorithm=NoEncryption(), 

190 ) 

191 

192 @classmethod 

193 def from_priv_key_uri( 

194 cls, 

195 priv_key_uri: str, 

196 public_key: Key, 

197 secrets_handler: SecretsHandler | None = None, 

198 ) -> "CryptoSigner": 

199 """Constructor for Signer to call 

200 

201 Please refer to Signer.from_priv_key_uri() documentation. 

202 

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

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

205 but are not guaranteed to work. 

206 

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

208 private key file. If CRYPTO_SIGNER_PATH_PREFIX environment variable 

209 is set, the private key will be read from 

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

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

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

213 

214 Additionally raises: 

215 UnsupportedLibraryError: pyca/cryptography not installed 

216 OSError: file cannot be read 

217 ValueError: various errors passed arguments 

218 ValueError, TypeError, \ 

219 cryptography.exceptions.UnsupportedAlgorithm: 

220 pyca/cryptography deserialization failed 

221 

222 """ 

223 if CRYPTO_IMPORT_ERROR: 

224 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

225 

226 if not isinstance(public_key, SSlibKey): 

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

228 

229 uri = parse.urlparse(priv_key_uri) 

230 

231 if uri.scheme != cls.SCHEME: 

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

233 

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

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

236 try: 

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

238 private_pem = f.read() 

239 except FileNotFoundError as e: 

240 raise FileNotFoundError( 

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

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

243 ) from e 

244 

245 private_key = load_pem_private_key(private_pem, None) 

246 return CryptoSigner(private_key, public_key) 

247 

248 @staticmethod 

249 def generate_ed25519( 

250 keyid: str | None = None, 

251 ) -> "CryptoSigner": 

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

253 

254 Args: 

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

256 

257 Raises: 

258 UnsupportedLibraryError: pyca/cryptography not installed 

259 

260 Returns: 

261 ED25519Signer 

262 """ 

263 if CRYPTO_IMPORT_ERROR: 

264 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

265 

266 private_key = Ed25519PrivateKey.generate() 

267 public_key = SSlibKey.from_crypto(private_key.public_key(), keyid, "ed25519") 

268 return CryptoSigner(private_key, public_key) 

269 

270 @staticmethod 

271 def generate_rsa( 

272 keyid: str | None = None, 

273 scheme: str | None = "rsassa-pss-sha256", 

274 size: int = 3072, 

275 ) -> "CryptoSigner": 

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

277 

278 Args: 

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

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

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

282 

283 Raises: 

284 UnsupportedLibraryError: pyca/cryptography not installed 

285 

286 Returns: 

287 RSASigner 

288 """ 

289 if CRYPTO_IMPORT_ERROR: 

290 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

291 

292 private_key = generate_rsa_private_key( 

293 public_exponent=65537, 

294 key_size=size, 

295 ) 

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

297 return CryptoSigner(private_key, public_key) 

298 

299 @staticmethod 

300 def generate_ecdsa( 

301 keyid: str | None = None, 

302 ) -> "CryptoSigner": 

303 """Generate new key pair as "ecdsa-sha2-nistp256" signer. 

304 

305 Args: 

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

307 

308 Raises: 

309 UnsupportedLibraryError: pyca/cryptography not installed 

310 

311 Returns: 

312 ECDSASigner 

313 """ 

314 if CRYPTO_IMPORT_ERROR: 

315 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

316 

317 private_key = generate_ec_private_key(SECP256R1()) 

318 public_key = SSlibKey.from_crypto( 

319 private_key.public_key(), keyid, "ecdsa-sha2-nistp256" 

320 ) 

321 return CryptoSigner(private_key, public_key) 

322 

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

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

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