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

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

122 statements  

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

2 

3import logging 

4import os 

5from dataclasses import astuple, dataclass 

6from typing import Optional, Union 

7from urllib import parse 

8 

9from securesystemslib.exceptions import UnsupportedLibraryError 

10from securesystemslib.signer._key import Key, SSlibKey 

11from securesystemslib.signer._signature import Signature 

12from securesystemslib.signer._signer import SecretsHandler, Signer 

13 

14CRYPTO_IMPORT_ERROR = None 

15try: 

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

17 ECDSA, 

18 SECP256R1, 

19 EllipticCurvePrivateKey, 

20 ) 

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

22 generate_private_key as generate_ec_private_key, 

23 ) 

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

25 Ed25519PrivateKey, 

26 ) 

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

28 MGF1, 

29 PSS, 

30 PKCS1v15, 

31 ) 

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

33 AsymmetricPadding, 

34 RSAPrivateKey, 

35 ) 

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

37 generate_private_key as generate_rsa_private_key, 

38 ) 

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

40 from cryptography.hazmat.primitives.hashes import ( 

41 SHA256, 

42 HashAlgorithm, 

43 ) 

44 from cryptography.hazmat.primitives.serialization import ( 

45 Encoding, 

46 NoEncryption, 

47 PrivateFormat, 

48 load_pem_private_key, 

49 ) 

50 

51 from securesystemslib.signer._crypto_utils import get_hash_algorithm 

52 

53except ImportError: 

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

55 

56logger = logging.getLogger(__name__) 

57 

58 

59@dataclass 

60class _RSASignArgs: 

61 padding: "AsymmetricPadding" 

62 hash_algo: "HashAlgorithm" 

63 

64 

65@dataclass 

66class _ECDSASignArgs: 

67 sig_algo: "ECDSA" 

68 

69 

70@dataclass 

71class _NoSignArgs: 

72 pass 

73 

74 

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

76# should be accepted in addition to "ecdsa" 

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

78 

79 

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

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

82 padding: AsymmetricPadding 

83 if name == "pss": 

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

85 

86 if name == "pkcs1v15": 

87 padding = PKCS1v15() 

88 

89 return padding 

90 

91 

92class CryptoSigner(Signer): 

93 """PYCA/cryptography Signer implementations. 

94 

95 A CryptoSigner can be created from: 

96 

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

98 

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

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

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

102 

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

104 

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

106 

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

108 

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

110 b is not flexible enough for your case. 

111 """ 

112 

113 SCHEME = "file2" 

114 PREFIX_ENV_VAR = "CRYPTO_SIGNER_PATH_PREFIX" 

115 

116 def __init__( 

117 self, 

118 private_key: "PrivateKeyTypes", 

119 public_key: Optional[SSlibKey] = None, 

120 ): 

121 if CRYPTO_IMPORT_ERROR: 

122 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

123 

124 if public_key is None: 

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

126 

127 self._private_key: PrivateKeyTypes 

128 self._sign_args: Union[_RSASignArgs, _ECDSASignArgs, _NoSignArgs] 

129 

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

131 "rsassa-pss-sha224", 

132 "rsassa-pss-sha256", 

133 "rsassa-pss-sha384", 

134 "rsassa-pss-sha512", 

135 "rsa-pkcs1v15-sha224", 

136 "rsa-pkcs1v15-sha256", 

137 "rsa-pkcs1v15-sha384", 

138 "rsa-pkcs1v15-sha512", 

139 ]: 

140 if not isinstance(private_key, RSAPrivateKey): 

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

142 

143 hash_name = public_key.get_hash_algorithm_name() 

144 hash_algo = get_hash_algorithm(hash_name) 

145 

146 padding_name = public_key.get_padding_name() 

147 padding = _get_rsa_padding(padding_name, hash_algo) 

148 

149 self._sign_args = _RSASignArgs(padding, hash_algo) 

150 self._private_key = private_key 

151 

152 elif ( 

153 public_key.keytype in _ECDSA_KEYTYPES 

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

155 ): 

156 if not isinstance(private_key, EllipticCurvePrivateKey): 

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

158 

159 signature_algorithm = ECDSA(SHA256()) 

160 self._sign_args = _ECDSASignArgs(signature_algorithm) 

161 self._private_key = private_key 

162 

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

164 if not isinstance(private_key, Ed25519PrivateKey): 

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

166 

167 self._sign_args = _NoSignArgs() 

168 self._private_key = private_key 

169 

170 else: 

171 raise ValueError( 

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

173 ) 

174 

175 self._public_key = public_key 

176 

177 @property 

178 def public_key(self) -> SSlibKey: 

179 return self._public_key 

180 

181 @property 

182 def private_bytes(self) -> bytes: 

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

184 

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

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

187 return self._private_key.private_bytes( 

188 encoding=Encoding.PEM, 

189 format=PrivateFormat.PKCS8, 

190 encryption_algorithm=NoEncryption(), 

191 ) 

192 

193 @classmethod 

194 def from_priv_key_uri( 

195 cls, 

196 priv_key_uri: str, 

197 public_key: Key, 

198 secrets_handler: Optional[SecretsHandler] = None, 

199 ) -> "CryptoSigner": 

200 """Constructor for Signer to call 

201 

202 Please refer to Signer.from_priv_key_uri() documentation. 

203 

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

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

206 but are not guaranteed to work. 

207 

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

209 private key file. If CRYPTO_SIGNER_PATH_PREFIX environment variable 

210 is set, the private key will be read from 

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

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

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

214 

215 Additionally raises: 

216 UnsupportedLibraryError: pyca/cryptography not installed 

217 OSError: file cannot be read 

218 ValueError: various errors passed arguments 

219 ValueError, TypeError, \ 

220 cryptography.exceptions.UnsupportedAlgorithm: 

221 pyca/cryptography deserialization failed 

222 

223 """ 

224 if CRYPTO_IMPORT_ERROR: 

225 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

226 

227 if not isinstance(public_key, SSlibKey): 

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

229 

230 uri = parse.urlparse(priv_key_uri) 

231 

232 if uri.scheme != cls.SCHEME: 

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

234 

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

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

237 try: 

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

239 private_pem = f.read() 

240 except FileNotFoundError as e: 

241 raise FileNotFoundError( 

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

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

244 ) from e 

245 

246 private_key = load_pem_private_key(private_pem, None) 

247 return CryptoSigner(private_key, public_key) 

248 

249 @staticmethod 

250 def generate_ed25519( 

251 keyid: Optional[str] = None, 

252 ) -> "CryptoSigner": 

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

254 

255 Args: 

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

257 

258 Raises: 

259 UnsupportedLibraryError: pyca/cryptography not installed 

260 

261 Returns: 

262 ED25519Signer 

263 """ 

264 if CRYPTO_IMPORT_ERROR: 

265 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

266 

267 private_key = Ed25519PrivateKey.generate() 

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

269 return CryptoSigner(private_key, public_key) 

270 

271 @staticmethod 

272 def generate_rsa( 

273 keyid: Optional[str] = None, 

274 scheme: Optional[str] = "rsassa-pss-sha256", 

275 size: int = 3072, 

276 ) -> "CryptoSigner": 

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

278 

279 Args: 

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

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

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

283 

284 Raises: 

285 UnsupportedLibraryError: pyca/cryptography not installed 

286 

287 Returns: 

288 RSASigner 

289 """ 

290 if CRYPTO_IMPORT_ERROR: 

291 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

292 

293 private_key = generate_rsa_private_key( 

294 public_exponent=65537, 

295 key_size=size, 

296 ) 

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

298 return CryptoSigner(private_key, public_key) 

299 

300 @staticmethod 

301 def generate_ecdsa( 

302 keyid: Optional[str] = None, 

303 ) -> "CryptoSigner": 

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

305 

306 Args: 

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

308 

309 Raises: 

310 UnsupportedLibraryError: pyca/cryptography not installed 

311 

312 Returns: 

313 ECDSASigner 

314 """ 

315 if CRYPTO_IMPORT_ERROR: 

316 raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) 

317 

318 private_key = generate_ec_private_key(SECP256R1()) 

319 public_key = SSlibKey.from_crypto( 

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

321 ) 

322 return CryptoSigner(private_key, public_key) 

323 

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

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

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