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