Skip to content

Commit acb84c5

Browse files
authored
Merge pull request #675 from lukpueh/refactor-crytposigner
CryptoSigner: support init from PrivateKeyTypes
2 parents feb0c39 + 0f8d337 commit acb84c5

File tree

2 files changed

+155
-134
lines changed

2 files changed

+155
-134
lines changed

securesystemslib/signer/_crypto_signer.py

Lines changed: 135 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Signer implementation for pyca/cryptography signing. """
22

33
import logging
4-
from abc import ABCMeta
5-
from typing import Any, Dict, Optional, cast
4+
from dataclasses import astuple, dataclass
5+
from typing import Any, Dict, Optional, Union
66
from urllib import parse
77

88
from securesystemslib.exceptions import UnsupportedLibraryError
@@ -53,15 +53,133 @@
5353
logger = logging.getLogger(__name__)
5454

5555

56-
class CryptoSigner(Signer, metaclass=ABCMeta):
57-
"""Base class for PYCA/cryptography Signer implementations."""
56+
@dataclass
57+
class _RSASignArgs:
58+
padding: "AsymmetricPadding"
59+
hash_algo: "HashAlgorithm"
60+
61+
62+
@dataclass
63+
class _ECDSASignArgs:
64+
sig_algo: "ECDSA"
65+
66+
67+
@dataclass
68+
class _NoSignArgs:
69+
pass
70+
71+
72+
def _get_hash_algorithm(name: str) -> "HashAlgorithm":
73+
"""Helper to return hash algorithm for name."""
74+
algorithm: HashAlgorithm
75+
if name == "sha224":
76+
algorithm = SHA224()
77+
if name == "sha256":
78+
algorithm = SHA256()
79+
if name == "sha384":
80+
algorithm = SHA384()
81+
if name == "sha512":
82+
algorithm = SHA512()
83+
84+
return algorithm
85+
86+
87+
def _get_rsa_padding(
88+
name: str, hash_algorithm: "HashAlgorithm"
89+
) -> "AsymmetricPadding":
90+
"""Helper to return rsa signature padding for name."""
91+
padding: AsymmetricPadding
92+
if name == "pss":
93+
padding = PSS(mgf=MGF1(hash_algorithm), salt_length=PSS.DIGEST_LENGTH)
94+
95+
if name == "pkcs1v15":
96+
padding = PKCS1v15()
97+
98+
return padding
99+
100+
101+
class CryptoSigner(Signer):
102+
"""PYCA/cryptography Signer implementations.
103+
104+
A CryptoSigner can be created from:
105+
106+
a. private key file -- ``Signer.from_priv_key_uri()``
107+
108+
URI has the format "file:<PATH>?encrypted=[true|false]", where
109+
PATH is the path to a file with private key data in a standard
110+
PEM/PKCS8 format.
111+
112+
A related public key must be passed.
113+
114+
If ``encrypted=true``, the optional secrets handler is expected to
115+
return a decryption password.
116+
117+
b. newly generated key pair -- ``CryptoSigner.generate_*()``
118+
119+
c. existing pyca/cryptography private key object -- ``CryptoSigner()``
120+
121+
"""
58122

59123
FILE_URI_SCHEME = "file"
60124

61-
def __init__(self, public_key: SSlibKey):
125+
def __init__(
126+
self,
127+
private_key: "PrivateKeyTypes",
128+
public_key: Optional[SSlibKey] = None,
129+
):
62130
if CRYPTO_IMPORT_ERROR:
63131
raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR)
64132

133+
if public_key is None:
134+
public_key = SSlibKey._from_crypto_public_key(
135+
private_key.public_key(), None, None
136+
)
137+
138+
self._private_key: PrivateKeyTypes
139+
self._sign_args: Union[_RSASignArgs, _ECDSASignArgs, _NoSignArgs]
140+
141+
if public_key.keytype == "rsa" and public_key.scheme in [
142+
"rsassa-pss-sha224",
143+
"rsassa-pss-sha256",
144+
"rsassa-pss-sha384",
145+
"rsassa-pss-sha512",
146+
"rsa-pkcs1v15-sha224",
147+
"rsa-pkcs1v15-sha256",
148+
"rsa-pkcs1v15-sha384",
149+
"rsa-pkcs1v15-sha512",
150+
]:
151+
if not isinstance(private_key, RSAPrivateKey):
152+
raise ValueError(f"invalid rsa key: {type(private_key)}")
153+
154+
padding_name, hash_name = public_key.scheme.split("-")[1:]
155+
hash_algo = _get_hash_algorithm(hash_name)
156+
padding = _get_rsa_padding(padding_name, hash_algo)
157+
self._sign_args = _RSASignArgs(padding, hash_algo)
158+
self._private_key = private_key
159+
160+
elif (
161+
public_key.keytype == "ecdsa"
162+
and public_key.scheme == "ecdsa-sha2-nistp256"
163+
):
164+
if not isinstance(private_key, EllipticCurvePrivateKey):
165+
raise ValueError(f"invalid ecdsa key: {type(private_key)}")
166+
167+
signature_algorithm = ECDSA(SHA256())
168+
self._sign_args = _ECDSASignArgs(signature_algorithm)
169+
self._private_key = private_key
170+
171+
elif public_key.keytype == "ed25519" and public_key.scheme == "ed25519":
172+
if not isinstance(private_key, Ed25519PrivateKey):
173+
raise ValueError(f"invalid ed25519 key: {type(private_key)}")
174+
175+
self._sign_args = _NoSignArgs()
176+
self._private_key = private_key
177+
178+
else:
179+
raise ValueError(
180+
f"unsupported public key {public_key.keytype}/{public_key.scheme}"
181+
)
182+
65183
self.public_key = public_key
66184

67185
@classmethod
@@ -73,49 +191,18 @@ def from_securesystemslib_key(
73191
public_key = SSlibKey.from_securesystemslib_key(key_dict)
74192

75193
private_key: PrivateKeyTypes
76-
if public_key.keytype == "rsa":
77-
private_key = cast(
78-
RSAPrivateKey,
79-
load_pem_private_key(private.encode(), password=None),
80-
)
81-
return _RSASigner(public_key, private_key)
194+
if public_key.keytype in ["rsa", "ecdsa"]:
195+
private_key = load_pem_private_key(private.encode(), password=None)
82196

83-
if public_key.keytype == "ecdsa":
84-
private_key = cast(
85-
EllipticCurvePrivateKey,
86-
load_pem_private_key(private.encode(), password=None),
87-
)
88-
return _ECDSASigner(public_key, private_key)
89-
90-
if public_key.keytype == "ed25519":
197+
elif public_key.keytype == "ed25519":
91198
private_key = Ed25519PrivateKey.from_private_bytes(
92199
bytes.fromhex(private)
93200
)
94-
return _Ed25519Signer(public_key, private_key)
95-
96-
raise ValueError(f"unsupported keytype: {public_key.keytype}")
97-
98-
@classmethod
99-
def _from_pem(
100-
cls, private_pem: bytes, secret: Optional[bytes], public_key: SSlibKey
101-
):
102-
"""Helper factory to create CryptoSigner from private PEM."""
103-
private_key = load_pem_private_key(private_pem, secret)
104-
105-
if public_key.keytype == "rsa":
106-
return _RSASigner(public_key, cast(RSAPrivateKey, private_key))
107-
108-
if public_key.keytype == "ecdsa":
109-
return _ECDSASigner(
110-
public_key, cast(EllipticCurvePrivateKey, private_key)
111-
)
112201

113-
if public_key.keytype == "ed25519":
114-
return _Ed25519Signer(
115-
public_key, cast(Ed25519PrivateKey, private_key)
116-
)
202+
else:
203+
raise ValueError(f"unsupported keytype: {public_key.keytype}")
117204

118-
raise ValueError(f"unsupported keytype: {public_key.keytype}")
205+
return CryptoSigner(private_key, public_key)
119206

120207
@classmethod
121208
def from_priv_key_uri(
@@ -167,7 +254,8 @@ def from_priv_key_uri(
167254
with open(uri.path, "rb") as f:
168255
private_pem = f.read()
169256

170-
return cls._from_pem(private_pem, secret, public_key)
257+
private_key = load_pem_private_key(private_pem, secret)
258+
return CryptoSigner(private_key, public_key)
171259

172260
@staticmethod
173261
def generate_ed25519(
@@ -191,7 +279,7 @@ def generate_ed25519(
191279
public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access
192280
private_key.public_key(), keyid, "ed25519"
193281
)
194-
return _Ed25519Signer(public_key, private_key)
282+
return CryptoSigner(private_key, public_key)
195283

196284
@staticmethod
197285
def generate_rsa(
@@ -222,7 +310,7 @@ def generate_rsa(
222310
public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access
223311
private_key.public_key(), keyid, scheme
224312
)
225-
return _RSASigner(public_key, private_key)
313+
return CryptoSigner(private_key, public_key)
226314

227315
@staticmethod
228316
def generate_ecdsa(
@@ -246,95 +334,8 @@ def generate_ecdsa(
246334
public_key = SSlibKey._from_crypto_public_key( # pylint: disable=protected-access
247335
private_key.public_key(), keyid, "ecdsa-sha2-nistp256"
248336
)
249-
return _ECDSASigner(public_key, private_key)
250-
251-
252-
class _RSASigner(CryptoSigner):
253-
"""Internal pyca/cryptography rsa signer implementation"""
254-
255-
def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"):
256-
if public_key.scheme not in [
257-
"rsassa-pss-sha224",
258-
"rsassa-pss-sha256",
259-
"rsassa-pss-sha384",
260-
"rsassa-pss-sha512",
261-
"rsa-pkcs1v15-sha224",
262-
"rsa-pkcs1v15-sha256",
263-
"rsa-pkcs1v15-sha384",
264-
"rsa-pkcs1v15-sha512",
265-
]:
266-
raise ValueError(f"unsupported scheme {public_key.scheme}")
267-
268-
super().__init__(public_key)
269-
self._private_key = private_key
270-
padding_name, hash_name = public_key.scheme.split("-")[1:]
271-
self._algorithm = self._get_hash_algorithm(hash_name)
272-
self._padding = self._get_rsa_padding(padding_name, self._algorithm)
273-
274-
@staticmethod
275-
def _get_hash_algorithm(name: str) -> "HashAlgorithm":
276-
"""Helper to return hash algorithm for name."""
277-
algorithm: HashAlgorithm
278-
if name == "sha224":
279-
algorithm = SHA224()
280-
if name == "sha256":
281-
algorithm = SHA256()
282-
if name == "sha384":
283-
algorithm = SHA384()
284-
if name == "sha512":
285-
algorithm = SHA512()
286-
287-
return algorithm
288-
289-
@staticmethod
290-
def _get_rsa_padding(
291-
name: str, hash_algorithm: "HashAlgorithm"
292-
) -> "AsymmetricPadding":
293-
"""Helper to return rsa signature padding for name."""
294-
padding: AsymmetricPadding
295-
if name == "pss":
296-
padding = PSS(
297-
mgf=MGF1(hash_algorithm), salt_length=PSS.DIGEST_LENGTH
298-
)
299-
300-
if name == "pkcs1v15":
301-
padding = PKCS1v15()
302-
303-
return padding
304-
305-
def sign(self, payload: bytes) -> Signature:
306-
sig = self._private_key.sign(payload, self._padding, self._algorithm)
307-
return Signature(self.public_key.keyid, sig.hex())
308-
309-
310-
class _ECDSASigner(CryptoSigner):
311-
"""Internal pyca/cryptography ecdsa signer implementation"""
312-
313-
def __init__(
314-
self, public_key: SSlibKey, private_key: "EllipticCurvePrivateKey"
315-
):
316-
if public_key.scheme != "ecdsa-sha2-nistp256":
317-
raise ValueError(f"unsupported scheme {public_key.scheme}")
318-
319-
super().__init__(public_key)
320-
self._private_key = private_key
321-
self._signature_algorithm = ECDSA(SHA256())
322-
323-
def sign(self, payload: bytes) -> Signature:
324-
sig = self._private_key.sign(payload, self._signature_algorithm)
325-
return Signature(self.public_key.keyid, sig.hex())
326-
327-
328-
class _Ed25519Signer(CryptoSigner):
329-
"""Internal pyca/cryptography ecdsa signer implementation"""
330-
331-
def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"):
332-
if public_key.scheme != "ed25519":
333-
raise ValueError(f"unsupported scheme {public_key.scheme}")
334-
335-
super().__init__(public_key)
336-
self._private_key = private_key
337+
return CryptoSigner(private_key, public_key)
337338

338339
def sign(self, payload: bytes) -> Signature:
339-
sig = self._private_key.sign(payload)
340+
sig = self._private_key.sign(payload, *astuple(self._sign_args)) # type: ignore
340341
return Signature(self.public_key.keyid, sig.hex())

tests/test_signer.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from pathlib import Path
99
from typing import Any, Dict, Optional
1010

11+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
12+
1113
import securesystemslib.keys as KEYS
1214
from securesystemslib.exceptions import (
1315
CryptoError,
@@ -742,6 +744,24 @@ def test_sphincs(self):
742744
class TestCryptoSigner(unittest.TestCase):
743745
"""CryptoSigner tests"""
744746

747+
def test_init(self):
748+
"""Test CryptoSigner constructor."""
749+
for keytype in ["rsa", "ecdsa", "ed25519"]:
750+
path = PEMS_DIR / f"{keytype}_private.pem"
751+
752+
with open(path, "rb") as f:
753+
data = f.read()
754+
755+
private_key = load_pem_private_key(data, None)
756+
757+
# Init w/o public key (public key is created from private key)
758+
signer = CryptoSigner(private_key)
759+
self.assertEqual(keytype, signer.public_key.keytype)
760+
761+
# Re-init with passed public key
762+
signer2 = CryptoSigner(private_key, signer.public_key)
763+
self.assertEqual(keytype, signer2.public_key.keytype)
764+
745765
def test_from_priv_key_uri(self):
746766
"""Test load and use PEM/PKCS#8 files for each sslib keytype"""
747767
test_data = [

0 commit comments

Comments
 (0)