Skip to content

Commit ad64dcd

Browse files
committed
signer: refactor SSlibSigner
This patch refactors the SSlibSigner signing implementation, akin to #585, which refactored signature verification (see PR for details about legacy code and rationale). Unlike, #585 this patch does not only condense and move the code for singing, but creates a new hierarchy of signers to achieve two additional goals: 1. Provide tiny rsa, ecdsa and ed25519 pyca/cryptography Signer implementations, which are independent of private key serialization formats, above all, of the proprietary legacy keydict format. This is particularly interesting, when refactoring existing or designing new key generation or import interfaces, where it would be annoying to move back and forth over the legacy keydict. 2. Preserve SSlibSigner including its internal legacy keydict data structure. SSlibSigner is and remains a backwards-compatibility crutch. Breaking its existing users to make it a little less awkward would defeat that purpose. And even though the Signer API doc says that the internal data structure is not part of the public API, users may rely on it (python-tuf actually does so at least in tests and demos). To achieve these goals, SSlibSigner becomes a container for the newly added CryptoSigner class, whose implementations can also be used as independent Signers, and above all created or imported, with very few lines of pyca/cryptography code. **Caveat:** Latest python-tuf tests pass against this patch, except for one, which expects a keydict deserialization failure in `sign`, which now happens in `__init__` initialization time. This seems feasible to fix in python-tuf. Also note that private key format errors are now ValueErrors and no longer unreliably either FormatErrors or sometimes UnsupportedAlgorithmErrors. **Future work (will ticketize):** - Signing schemes strings should not be hardcoded all over the place but defined once in constants for all of securesystemslib. - There is some duplicate code for scheme string dissection and algorithm selection, which could be unified for all signers and public keys. - (bonus) #585 considered creating separate RSAKey, ECDSAKey, ED25519Key classes, but ended up putting everything into SSlibKey. Now that we have separate signers for each of these key types, which have a field for the corresponding public key object, it might make sense to reconsider this separation. This would give us a more robust data model, where e.g. allowed signing schemes are only validated once in the public key constructor and are thus validated implicitly in the signer constructor. Signed-off-by: Lukas Puehringer <[email protected]>
1 parent 135567f commit ad64dcd

File tree

3 files changed

+194
-31
lines changed

3 files changed

+194
-31
lines changed

securesystemslib/signer/_signer.py

Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,49 @@
33
import logging
44
import os
55
from abc import ABCMeta, abstractmethod
6-
from typing import Any, Callable, Dict, Optional, Type
6+
from typing import Any, Callable, Dict, Optional, Type, cast
77
from urllib import parse
88

99
import securesystemslib.keys as sslib_keys
10+
from securesystemslib.exceptions import UnsupportedLibraryError
1011
from securesystemslib.formats import encode_canonical
1112
from securesystemslib.hash import digest
1213
from securesystemslib.signer._key import Key, SSlibKey
1314
from securesystemslib.signer._signature import Signature
1415

16+
CRYPTO_IMPORT_ERROR = None
17+
try:
18+
from cryptography.hazmat.primitives.asymmetric.ec import (
19+
ECDSA,
20+
EllipticCurvePrivateKey,
21+
)
22+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
23+
Ed25519PrivateKey,
24+
)
25+
from cryptography.hazmat.primitives.asymmetric.padding import (
26+
MGF1,
27+
PSS,
28+
AsymmetricPadding,
29+
PKCS1v15,
30+
)
31+
from cryptography.hazmat.primitives.asymmetric.rsa import (
32+
AsymmetricPadding,
33+
RSAPrivateKey,
34+
)
35+
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
36+
from cryptography.hazmat.primitives.hashes import (
37+
SHA224,
38+
SHA256,
39+
SHA384,
40+
SHA512,
41+
HashAlgorithm,
42+
)
43+
from cryptography.hazmat.primitives.serialization import (
44+
load_pem_private_key,
45+
)
46+
except ImportError:
47+
CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required"
48+
1549
logger = logging.getLogger(__name__)
1650

1751
# NOTE Signer dispatch table is defined here so it's usable by Signer,
@@ -164,6 +198,7 @@ class SSlibSigner(Signer):
164198

165199
def __init__(self, key_dict: Dict):
166200
self.key_dict = key_dict
201+
self._crypto_signer = CryptoSigner.from_securesystemslib_key(key_dict)
167202

168203
@classmethod
169204
def from_priv_key_uri(
@@ -212,6 +247,7 @@ def from_priv_key_uri(
212247

213248
keydict = public_key.to_securesystemslib_key()
214249
keydict["keyval"]["private"] = private
250+
215251
return cls(keydict)
216252

217253
def sign(self, payload: bytes) -> Signature:
@@ -225,5 +261,146 @@ def sign(self, payload: bytes) -> Signature:
225261
securesystemslib.exceptions.UnsupportedAlgorithmError:
226262
Signing errors.
227263
"""
228-
sig_dict = sslib_keys.create_signature(self.key_dict, payload)
229-
return Signature(**sig_dict)
264+
return self._crypto_signer.sign(payload)
265+
266+
267+
class CryptoSigner(Signer, metaclass=ABCMeta):
268+
"""Base class for PYCA/cryptography Signer implementations."""
269+
270+
def __init__(self, public_key: SSlibKey):
271+
if CRYPTO_IMPORT_ERROR:
272+
raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR)
273+
274+
self.public_key = public_key
275+
276+
@classmethod
277+
def from_securesystemslib_key(
278+
cls, key_dict: Dict[str, Any]
279+
) -> "CryptoSigner":
280+
"""Factory to create CryptoSigner from securesystemslib private key dict."""
281+
private = key_dict["keyval"]["private"]
282+
public_key = SSlibKey.from_securesystemslib_key(key_dict)
283+
284+
private_key: PrivateKeyTypes
285+
if public_key.keytype == "rsa":
286+
private_key = cast(
287+
RSAPrivateKey,
288+
load_pem_private_key(private.encode(), password=None),
289+
)
290+
return RSASigner(public_key, private_key)
291+
292+
if public_key.keytype == "ecdsa":
293+
private_key = cast(
294+
EllipticCurvePrivateKey,
295+
load_pem_private_key(private.encode(), password=None),
296+
)
297+
return ECDSASigner(public_key, private_key)
298+
299+
if public_key.keytype == "ed25519":
300+
private_key = Ed25519PrivateKey.from_private_bytes(
301+
bytes.fromhex(private)
302+
)
303+
return Ed25519Signer(public_key, private_key)
304+
305+
raise ValueError(f"unsupported keytype: {public_key.keytype}")
306+
307+
@classmethod
308+
def from_priv_key_uri(
309+
cls,
310+
priv_key_uri: str,
311+
public_key: Key,
312+
secrets_handler: Optional[SecretsHandler] = None,
313+
) -> "Signer":
314+
# Do not raise NotImplementedError to appease pylint for all subclasses
315+
raise RuntimeError("use SSlibSigner.from_priv_key_uri")
316+
317+
318+
class RSASigner(CryptoSigner):
319+
"""pyca/cryptography rsa signer implementation"""
320+
321+
def __init__(self, public_key: SSlibKey, private_key: "RSAPrivateKey"):
322+
if public_key.scheme not in [
323+
"rsassa-pss-sha224",
324+
"rsassa-pss-sha256",
325+
"rsassa-pss-sha384",
326+
"rsassa-pss-sha512",
327+
"rsa-pkcs1v15-sha224",
328+
"rsa-pkcs1v15-sha256",
329+
"rsa-pkcs1v15-sha384",
330+
"rsa-pkcs1v15-sha512",
331+
]:
332+
raise ValueError(f"unsupported scheme {public_key.scheme}")
333+
334+
super().__init__(public_key)
335+
self._private_key = private_key
336+
padding_name, hash_name = public_key.scheme.split("-")[1:]
337+
self._algorithm = self._get_hash_algorithm(hash_name)
338+
self._padding = self._get_rsa_padding(padding_name, self._algorithm)
339+
340+
@staticmethod
341+
def _get_hash_algorithm(name: str) -> "HashAlgorithm":
342+
"""Helper to return hash algorithm for name."""
343+
algorithm: HashAlgorithm
344+
if name == "sha224":
345+
algorithm = SHA224()
346+
if name == "sha256":
347+
algorithm = SHA256()
348+
if name == "sha384":
349+
algorithm = SHA384()
350+
if name == "sha512":
351+
algorithm = SHA512()
352+
353+
return algorithm
354+
355+
@staticmethod
356+
def _get_rsa_padding(
357+
name: str, hash_algorithm: "HashAlgorithm"
358+
) -> "AsymmetricPadding":
359+
"""Helper to return rsa signature padding for name."""
360+
padding: AsymmetricPadding
361+
if name == "pss":
362+
padding = PSS(
363+
mgf=MGF1(hash_algorithm), salt_length=PSS.DIGEST_LENGTH
364+
)
365+
366+
if name == "pkcs1v15":
367+
padding = PKCS1v15()
368+
369+
return padding
370+
371+
def sign(self, payload: bytes) -> Signature:
372+
sig = self._private_key.sign(payload, self._padding, self._algorithm)
373+
return Signature(self.public_key.keyid, sig.hex())
374+
375+
376+
class ECDSASigner(CryptoSigner):
377+
"""pyca/cryptography ecdsa signer implementation"""
378+
379+
def __init__(
380+
self, public_key: SSlibKey, private_key: "EllipticCurvePrivateKey"
381+
):
382+
if public_key.scheme != "ecdsa-sha2-nistp256":
383+
raise ValueError(f"unsupported scheme {public_key.scheme}")
384+
385+
super().__init__(public_key)
386+
self._private_key = private_key
387+
self._signature_algorithm = ECDSA(SHA256())
388+
389+
def sign(self, payload: bytes) -> Signature:
390+
sig = self._private_key.sign(payload, self._signature_algorithm)
391+
return Signature(self.public_key.keyid, sig.hex())
392+
393+
394+
class Ed25519Signer(CryptoSigner):
395+
"""pyca/cryptography ecdsa signer implementation"""
396+
397+
def __init__(self, public_key: SSlibKey, private_key: "Ed25519PrivateKey"):
398+
if public_key.scheme != "ed25519":
399+
raise ValueError(f"unsupported scheme {public_key.scheme}")
400+
401+
super().__init__(public_key)
402+
self._private_key = private_key
403+
404+
def sign(self, payload: bytes) -> Signature:
405+
sig = self._private_key.sign(payload)
406+
return Signature(self.public_key.keyid, sig.hex())

tests/test_dsse.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55

66
import securesystemslib.keys as KEYS
77
from securesystemslib.dsse import Envelope
8-
from securesystemslib.exceptions import (
9-
FormatError,
10-
UnsupportedAlgorithmError,
11-
VerificationError,
12-
)
8+
from securesystemslib.exceptions import VerificationError
139
from securesystemslib.signer import Signature, SSlibKey, SSlibSigner
1410

1511

@@ -96,9 +92,8 @@ def test_sign_and_verify(self):
9692
# Test for invalid scheme.
9793
valid_scheme = key_dict["scheme"]
9894
key_dict["scheme"] = "invalid_scheme"
99-
signer = SSlibSigner(key_dict)
100-
with self.assertRaises((FormatError, UnsupportedAlgorithmError)):
101-
envelope_obj.sign(signer)
95+
with self.assertRaises(ValueError):
96+
signer = SSlibSigner(key_dict)
10297

10398
# Sign the payload.
10499
key_dict["scheme"] = valid_scheme

tests/test_signer.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from securesystemslib.exceptions import (
1212
CryptoError,
1313
FormatError,
14-
UnsupportedAlgorithmError,
1514
UnverifiedSignatureError,
1615
VerificationError,
1716
)
@@ -390,25 +389,17 @@ def test_sslib_signer_sign(self):
390389
)
391390
self.assertTrue(verified, "Incorrect signature.")
392391

393-
# Removing private key from "scheme_dict".
394-
private = scheme_dict["keyval"]["private"]
395-
scheme_dict["keyval"]["private"] = ""
396-
sslib_signer.key_dict = scheme_dict
397-
398-
with self.assertRaises((ValueError, FormatError)):
399-
sslib_signer.sign(self.DATA)
400-
401-
scheme_dict["keyval"]["private"] = private
402-
403-
# Test for invalid signature scheme.
404-
valid_scheme = scheme_dict["scheme"]
405-
scheme_dict["scheme"] = "invalid_scheme"
406-
sslib_signer = SSlibSigner(scheme_dict)
407-
408-
with self.assertRaises((UnsupportedAlgorithmError, FormatError)):
409-
sslib_signer.sign(self.DATA)
410-
411-
scheme_dict["scheme"] = valid_scheme
392+
# Assert error for invalid private key data
393+
bad_private = copy.deepcopy(scheme_dict)
394+
bad_private["keyval"]["private"] = ""
395+
with self.assertRaises(ValueError):
396+
SSlibSigner(bad_private)
397+
398+
# Assert error for invalid scheme
399+
invalid_scheme = copy.deepcopy(scheme_dict)
400+
invalid_scheme["scheme"] = "invalid_scheme"
401+
with self.assertRaises(ValueError):
402+
SSlibSigner(invalid_scheme)
412403

413404
def test_custom_signer(self):
414405
# setup

0 commit comments

Comments
 (0)