diff --git a/securesystemslib/signer.py b/securesystemslib/signer.py new file mode 100644 index 00000000..ed8e994c --- /dev/null +++ b/securesystemslib/signer.py @@ -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) diff --git a/tests/test_signer.py b/tests/test_signer.py new file mode 100644 index 00000000..0d8bcf44 --- /dev/null +++ b/tests/test_signer.py @@ -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()