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())