-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #319 from MVrachev/signer-interface
Add the Signer interface
- Loading branch information
Showing
2 changed files
with
217 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
"""Signer interface and example interface implementations. | ||
The goal of this module is to provide a signing interface supporting multiple | ||
signing implementations and a couple of example implementations. | ||
""" | ||
|
||
import abc | ||
import securesystemslib.keys as sslib_keys | ||
from typing import Dict | ||
|
||
|
||
class Signature: | ||
"""A container class containing information about a signature. | ||
Contains a signature and the keyid uniquely identifying the key used | ||
to generate the signature. | ||
Provides utility methods to easily create an object from a dictionary | ||
and return the dictionary representation of the object. | ||
Attributes: | ||
keyid: HEX string used as a unique identifier of the key. | ||
signature: HEX string representing the signature. | ||
""" | ||
def __init__(self, keyid: str, sig: str): | ||
self.keyid = keyid | ||
self.signature = sig | ||
|
||
|
||
@classmethod | ||
def from_dict(cls, signature_dict: Dict) -> "Signature": | ||
"""Creates a Signature object from its JSON/dict representation. | ||
Arguments: | ||
signature_dict: | ||
A dict containing a valid keyid and a signature. | ||
Note that the fields in it should be named "keyid" and "sig" | ||
respectively. | ||
Raises: | ||
KeyError: If any of the "keyid" and "sig" fields are missing from | ||
the signature_dict. | ||
Returns: | ||
A "Signature" instance. | ||
""" | ||
|
||
return cls(signature_dict["keyid"], signature_dict["sig"]) | ||
|
||
|
||
def to_dict(self) -> Dict: | ||
"""Returns the JSON-serializable dictionary representation of self.""" | ||
|
||
return { | ||
"keyid": self.keyid, | ||
"sig": self.signature | ||
} | ||
|
||
|
||
|
||
class Signer: | ||
"""Signer interface created to support multiple signing implementations.""" | ||
|
||
__metaclass__ = abc.ABCMeta | ||
|
||
@abc.abstractmethod | ||
def sign(payload: bytes) -> "Signature": | ||
"""Signs a given payload by the key assigned to the Signer instance. | ||
Arguments: | ||
payload: The bytes to be signed. | ||
Returns: | ||
Returns a "Signature" class instance. | ||
""" | ||
raise NotImplementedError # pragma: no cover | ||
|
||
|
||
|
||
class SSlibSigner(Signer): | ||
"""A securesystemslib signer implementation. | ||
Provides a sign method to generate a cryptographic signature with a | ||
securesystemslib-style rsa, ed25519 or ecdsa private key on the instance. | ||
The signature scheme is determined by the key and must be one of: | ||
- rsa(ssa-pss|pkcs1v15)-(md5|sha1|sha224|sha256|sha384|sha512) (12 schemes) | ||
- ed25519 | ||
- ecdsa-sha2-nistp256 | ||
See "securesystemslib.interface" for functions to generate and load keys. | ||
Attributes: | ||
key_dict: | ||
A securesystemslib-style key dictionary, which includes a keyid, | ||
key type, signature scheme, and the public and private key values, | ||
e.g.:: | ||
{ | ||
"keytype": "rsa", | ||
"scheme": "rsassa-pss-sha256", | ||
"keyid": "f30a0870d026980100c0573bd557394f8c1bbd6...", | ||
"keyval": { | ||
"public": "-----BEGIN RSA PUBLIC KEY----- ...", | ||
"private": "-----BEGIN RSA PRIVATE KEY----- ..." | ||
} | ||
} | ||
The public and private keys are strings in PEM format. | ||
""" | ||
def __init__(self, key_dict: Dict): | ||
self.key_dict = key_dict | ||
|
||
|
||
def sign(self, payload: bytes) -> "Signature": | ||
"""Signs a given payload by the key assigned to the SSlibSigner instance. | ||
Arguments: | ||
payload: The bytes to be signed. | ||
Raises: | ||
securesystemslib.exceptions.FormatError: Key argument is malformed. | ||
securesystemslib.exceptions.CryptoError, \ | ||
securesystemslib.exceptions.UnsupportedAlgorithmError: | ||
Signing errors. | ||
Returns: | ||
Returns a "Signature" class instance. | ||
""" | ||
|
||
sig_dict = sslib_keys.create_signature(self.key_dict, payload) | ||
return Signature(**sig_dict) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
#!/usr/bin/env python | ||
|
||
"""Test cases for "signer.py". """ | ||
|
||
import sys | ||
import unittest | ||
|
||
import unittest | ||
import securesystemslib.formats | ||
import securesystemslib.keys as KEYS | ||
from securesystemslib.exceptions import FormatError, UnsupportedAlgorithmError | ||
|
||
# TODO: Remove case handling when fully dropping support for versions < 3.6 | ||
IS_PY_VERSION_SUPPORTED = sys.version_info >= (3, 6) | ||
|
||
# Use setUpModule to tell unittest runner to skip this test module gracefully. | ||
def setUpModule(): | ||
if not IS_PY_VERSION_SUPPORTED: | ||
raise unittest.SkipTest("requires Python 3.6 or higher") | ||
|
||
# Since setUpModule is called after imports we need to import conditionally. | ||
if IS_PY_VERSION_SUPPORTED: | ||
from securesystemslib.signer import Signature, SSlibSigner | ||
|
||
|
||
class TestSSlibSigner(unittest.TestCase): | ||
|
||
@classmethod | ||
def setUpClass(cls): | ||
cls.rsakey_dict = KEYS.generate_rsa_key() | ||
cls.ed25519key_dict = KEYS.generate_ed25519_key() | ||
cls.ecdsakey_dict = KEYS.generate_ecdsa_key() | ||
cls.DATA_STR = "SOME DATA REQUIRING AUTHENTICITY." | ||
cls.DATA = securesystemslib.formats.encode_canonical( | ||
cls.DATA_STR).encode("utf-8") | ||
|
||
|
||
def test_sslib_sign(self): | ||
dicts = [self.rsakey_dict, self.ecdsakey_dict, self.ed25519key_dict] | ||
for scheme_dict in dicts: | ||
# Test generation of signatures. | ||
sslib_signer = SSlibSigner(scheme_dict) | ||
sig_obj = sslib_signer.sign(self.DATA) | ||
|
||
# Verify signature | ||
verified = KEYS.verify_signature(scheme_dict, sig_obj.to_dict(), | ||
self.DATA) | ||
self.assertTrue(verified, "Incorrect signature.") | ||
|
||
# Removing private key from "scheme_dict". | ||
private = scheme_dict["keyval"]["private"] | ||
scheme_dict["keyval"]["private"] = "" | ||
sslib_signer.key_dict = scheme_dict | ||
|
||
with self.assertRaises((ValueError, FormatError)): | ||
sslib_signer.sign(self.DATA) | ||
|
||
scheme_dict["keyval"]["private"] = private | ||
|
||
# Test for invalid signature scheme. | ||
valid_scheme = scheme_dict["scheme"] | ||
scheme_dict["scheme"] = "invalid_scheme" | ||
sslib_signer = SSlibSigner(scheme_dict) | ||
|
||
with self.assertRaises((UnsupportedAlgorithmError, FormatError)): | ||
sslib_signer.sign(self.DATA) | ||
|
||
scheme_dict["scheme"] = valid_scheme | ||
|
||
|
||
def test_signature_from_to_dict(self): | ||
signature_dict = { | ||
"sig": "30460221009342e4566528fcecf6a7a5d53ebacdb1df151e242f55f8775883469cb01dbc6602210086b426cc826709acfa2c3f9214610cb0a832db94bbd266fd7c5939a48064a851", | ||
"keyid": "11fa391a0ed7a447cbfeb4b2667e286fc248f64d5e6d0eeed2e5e23f97f9f714" | ||
} | ||
sig_obj = Signature.from_dict(signature_dict) | ||
|
||
self.assertDictEqual(signature_dict, sig_obj.to_dict()) | ||
|
||
|
||
# Run the unit tests. | ||
if __name__ == "__main__": | ||
unittest.main() |