Skip to content

Commit 052b304

Browse files
committed
signer: refactor SSlibSigner (WIP)
TODO: - describe/justify design backwards-comapt of SSlibSigner(keydict) + removal of legacy data structure in new signers --> demo a signer creation w/o keydict - describe test/exception-handling breaking changes - ValueError instead of FormatError/UnsupportedAlogrithm - ValueError in __init__ instead of sign (this is what makes python-tuf fail) - mention follow-up work - de-duplicate scheme string dissection - constants for schemes - maybe RSAKey, ECDSAKey, ED25519Key after alli -> for more robust data model, so that we don't have to check schemes in key and signer - move "rm legacy code" to separate branch and point to diff Signed-off-by: Lukas Puehringer <[email protected]>
1 parent 135567f commit 052b304

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)