Skip to content

Commit 760523c

Browse files
authored
Merge pull request #319 from MVrachev/signer-interface
Add the Signer interface
2 parents b5023f3 + 2bc36c9 commit 760523c

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

securesystemslib/signer.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""Signer interface and example interface implementations.
2+
3+
The goal of this module is to provide a signing interface supporting multiple
4+
signing implementations and a couple of example implementations.
5+
6+
"""
7+
8+
import abc
9+
import securesystemslib.keys as sslib_keys
10+
from typing import Dict
11+
12+
13+
class Signature:
14+
"""A container class containing information about a signature.
15+
16+
Contains a signature and the keyid uniquely identifying the key used
17+
to generate the signature.
18+
19+
Provides utility methods to easily create an object from a dictionary
20+
and return the dictionary representation of the object.
21+
22+
Attributes:
23+
keyid: HEX string used as a unique identifier of the key.
24+
signature: HEX string representing the signature.
25+
26+
"""
27+
def __init__(self, keyid: str, sig: str):
28+
self.keyid = keyid
29+
self.signature = sig
30+
31+
32+
@classmethod
33+
def from_dict(cls, signature_dict: Dict) -> "Signature":
34+
"""Creates a Signature object from its JSON/dict representation.
35+
36+
Arguments:
37+
signature_dict:
38+
A dict containing a valid keyid and a signature.
39+
Note that the fields in it should be named "keyid" and "sig"
40+
respectively.
41+
42+
Raises:
43+
KeyError: If any of the "keyid" and "sig" fields are missing from
44+
the signature_dict.
45+
46+
Returns:
47+
A "Signature" instance.
48+
"""
49+
50+
return cls(signature_dict["keyid"], signature_dict["sig"])
51+
52+
53+
def to_dict(self) -> Dict:
54+
"""Returns the JSON-serializable dictionary representation of self."""
55+
56+
return {
57+
"keyid": self.keyid,
58+
"sig": self.signature
59+
}
60+
61+
62+
63+
class Signer:
64+
"""Signer interface created to support multiple signing implementations."""
65+
66+
__metaclass__ = abc.ABCMeta
67+
68+
@abc.abstractmethod
69+
def sign(payload: bytes) -> "Signature":
70+
"""Signs a given payload by the key assigned to the Signer instance.
71+
72+
Arguments:
73+
payload: The bytes to be signed.
74+
75+
Returns:
76+
Returns a "Signature" class instance.
77+
"""
78+
raise NotImplementedError # pragma: no cover
79+
80+
81+
82+
class SSlibSigner(Signer):
83+
"""A securesystemslib signer implementation.
84+
85+
Provides a sign method to generate a cryptographic signature with a
86+
securesystemslib-style rsa, ed25519 or ecdsa private key on the instance.
87+
The signature scheme is determined by the key and must be one of:
88+
89+
- rsa(ssa-pss|pkcs1v15)-(md5|sha1|sha224|sha256|sha384|sha512) (12 schemes)
90+
- ed25519
91+
- ecdsa-sha2-nistp256
92+
93+
See "securesystemslib.interface" for functions to generate and load keys.
94+
95+
Attributes:
96+
key_dict:
97+
A securesystemslib-style key dictionary, which includes a keyid,
98+
key type, signature scheme, and the public and private key values,
99+
e.g.::
100+
101+
{
102+
"keytype": "rsa",
103+
"scheme": "rsassa-pss-sha256",
104+
"keyid": "f30a0870d026980100c0573bd557394f8c1bbd6...",
105+
"keyval": {
106+
"public": "-----BEGIN RSA PUBLIC KEY----- ...",
107+
"private": "-----BEGIN RSA PRIVATE KEY----- ..."
108+
}
109+
}
110+
111+
The public and private keys are strings in PEM format.
112+
"""
113+
def __init__(self, key_dict: Dict):
114+
self.key_dict = key_dict
115+
116+
117+
def sign(self, payload: bytes) -> "Signature":
118+
"""Signs a given payload by the key assigned to the SSlibSigner instance.
119+
120+
Arguments:
121+
payload: The bytes to be signed.
122+
123+
Raises:
124+
securesystemslib.exceptions.FormatError: Key argument is malformed.
125+
securesystemslib.exceptions.CryptoError, \
126+
securesystemslib.exceptions.UnsupportedAlgorithmError:
127+
Signing errors.
128+
129+
Returns:
130+
Returns a "Signature" class instance.
131+
"""
132+
133+
sig_dict = sslib_keys.create_signature(self.key_dict, payload)
134+
return Signature(**sig_dict)

tests/test_signer.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python
2+
3+
"""Test cases for "signer.py". """
4+
5+
import sys
6+
import unittest
7+
8+
import unittest
9+
import securesystemslib.formats
10+
import securesystemslib.keys as KEYS
11+
from securesystemslib.exceptions import FormatError, UnsupportedAlgorithmError
12+
13+
# TODO: Remove case handling when fully dropping support for versions < 3.6
14+
IS_PY_VERSION_SUPPORTED = sys.version_info >= (3, 6)
15+
16+
# Use setUpModule to tell unittest runner to skip this test module gracefully.
17+
def setUpModule():
18+
if not IS_PY_VERSION_SUPPORTED:
19+
raise unittest.SkipTest("requires Python 3.6 or higher")
20+
21+
# Since setUpModule is called after imports we need to import conditionally.
22+
if IS_PY_VERSION_SUPPORTED:
23+
from securesystemslib.signer import Signature, SSlibSigner
24+
25+
26+
class TestSSlibSigner(unittest.TestCase):
27+
28+
@classmethod
29+
def setUpClass(cls):
30+
cls.rsakey_dict = KEYS.generate_rsa_key()
31+
cls.ed25519key_dict = KEYS.generate_ed25519_key()
32+
cls.ecdsakey_dict = KEYS.generate_ecdsa_key()
33+
cls.DATA_STR = "SOME DATA REQUIRING AUTHENTICITY."
34+
cls.DATA = securesystemslib.formats.encode_canonical(
35+
cls.DATA_STR).encode("utf-8")
36+
37+
38+
def test_sslib_sign(self):
39+
dicts = [self.rsakey_dict, self.ecdsakey_dict, self.ed25519key_dict]
40+
for scheme_dict in dicts:
41+
# Test generation of signatures.
42+
sslib_signer = SSlibSigner(scheme_dict)
43+
sig_obj = sslib_signer.sign(self.DATA)
44+
45+
# Verify signature
46+
verified = KEYS.verify_signature(scheme_dict, sig_obj.to_dict(),
47+
self.DATA)
48+
self.assertTrue(verified, "Incorrect signature.")
49+
50+
# Removing private key from "scheme_dict".
51+
private = scheme_dict["keyval"]["private"]
52+
scheme_dict["keyval"]["private"] = ""
53+
sslib_signer.key_dict = scheme_dict
54+
55+
with self.assertRaises((ValueError, FormatError)):
56+
sslib_signer.sign(self.DATA)
57+
58+
scheme_dict["keyval"]["private"] = private
59+
60+
# Test for invalid signature scheme.
61+
valid_scheme = scheme_dict["scheme"]
62+
scheme_dict["scheme"] = "invalid_scheme"
63+
sslib_signer = SSlibSigner(scheme_dict)
64+
65+
with self.assertRaises((UnsupportedAlgorithmError, FormatError)):
66+
sslib_signer.sign(self.DATA)
67+
68+
scheme_dict["scheme"] = valid_scheme
69+
70+
71+
def test_signature_from_to_dict(self):
72+
signature_dict = {
73+
"sig": "30460221009342e4566528fcecf6a7a5d53ebacdb1df151e242f55f8775883469cb01dbc6602210086b426cc826709acfa2c3f9214610cb0a832db94bbd266fd7c5939a48064a851",
74+
"keyid": "11fa391a0ed7a447cbfeb4b2667e286fc248f64d5e6d0eeed2e5e23f97f9f714"
75+
}
76+
sig_obj = Signature.from_dict(signature_dict)
77+
78+
self.assertDictEqual(signature_dict, sig_obj.to_dict())
79+
80+
81+
# Run the unit tests.
82+
if __name__ == "__main__":
83+
unittest.main()

0 commit comments

Comments
 (0)