diff --git a/mypy.ini b/mypy.ini index 1a25ff52..52661c3f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -26,3 +26,6 @@ ignore_missing_imports = True [mypy-sigstore_protobuf_specs.*] ignore_missing_imports = True + +[mypy-pyspx.*] +ignore_missing_imports = True diff --git a/securesystemslib/formats.py b/securesystemslib/formats.py index 8a0e01cd..0d131b2f 100755 --- a/securesystemslib/formats.py +++ b/securesystemslib/formats.py @@ -206,7 +206,6 @@ SCHEMA.String("ed25519"), SCHEMA.String("ecdsa"), SCHEMA.RegularExpression(r"ecdsa-sha2-nistp(256|384)"), - SCHEMA.String("sphincs"), ] ) @@ -283,18 +282,11 @@ # An ED25519 raw public key, which must be 32 bytes. ED25519PUBLIC_SCHEMA = SCHEMA.LengthBytes(32) -SPHINCSPUBLIC_SCHEMA = SCHEMA.LengthBytes(32) - # An ED25519 raw seed key, which must be 32 bytes. ED25519SEED_SCHEMA = SCHEMA.LengthBytes(32) -SPHINCSPRIVATE_SCHEMA = SCHEMA.LengthBytes(64) - # An ED25519 raw signature, which must be 64 bytes. ED25519SIGNATURE_SCHEMA = SCHEMA.LengthBytes(64) - -SPHINCSSIGNATURE_SCHEMA = SCHEMA.LengthBytes(7_856) - # An ECDSA signature. ECDSASIGNATURE_SCHEMA = SCHEMA.AnyBytes() @@ -302,8 +294,6 @@ # supported. ED25519_SIG_SCHEMA = SCHEMA.OneOf([SCHEMA.String("ed25519")]) -SPHINCS_SIG_SCHEMA = SCHEMA.OneOf([SCHEMA.String("sphincs-shake-128s")]) - # An ed25519 key. ED25519KEY_SCHEMA = SCHEMA.Object( object_name="ED25519KEY_SCHEMA", diff --git a/securesystemslib/keys.py b/securesystemslib/keys.py index 33d7f4b3..1c6f9243 100755 --- a/securesystemslib/keys.py +++ b/securesystemslib/keys.py @@ -57,7 +57,6 @@ formats, rsa_keys, settings, - sphincs_keys, util, ) from securesystemslib.hash import digest @@ -344,33 +343,6 @@ def generate_ed25519_key(scheme="ed25519"): return ed25519_key -def generate_sphincs_key(scheme="sphincs-shake-128s"): - """Generate a SPHINCS+ key pair. - Arguments: - scheme (str): Name of the scheme as defined in formats.py. - Returns: - dict: A dictionary containing the SPHINCS+ keys. - Raises: - UnsupportedLibraryError: In case pyspx is not available. - """ - formats.SPHINCS_SIG_SCHEMA.check_match(scheme) - - sphincs_key = {} - keytype = "sphincs" - public, private = sphincs_keys.generate_public_and_private() - - key_value = {"public": public.hex(), "private": private.hex()} - keyid = _get_keyid(keytype, scheme, key_value) - - sphincs_key["keytype"] = keytype - sphincs_key["scheme"] = scheme - sphincs_key["keyid"] = keyid - sphincs_key["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - sphincs_key["keyval"] = key_value - - return sphincs_key - - def format_keyval_to_metadata(keytype, scheme, key_value, private=False): """ @@ -716,11 +688,6 @@ def create_signature(key_dict, data): elif keytype in ["ecdsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]: sig, scheme = ecdsa_keys.create_signature(public, private, data, scheme) - elif keytype == "sphincs": - sig, scheme = sphincs_keys.create_signature( - bytes.fromhex(public), bytes.fromhex(private), data, scheme - ) - # 'securesystemslib.formats.ANYKEY_SCHEMA' should have detected invalid key # types. This is a defensive check against an invalid key type. else: # pragma: no cover @@ -879,15 +846,6 @@ def verify_signature( raise exceptions.UnsupportedAlgorithmError( "Unsupported" " signature scheme is specified: " + repr(scheme) ) - elif keytype == "sphincs": - if scheme == "sphincs-shake-128s": - valid_signature = sphincs_keys.verify_signature( - bytes.fromhex(public), scheme, sig, data - ) - else: - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " signature scheme is specified: " + repr(scheme) - ) # 'securesystemslib.formats.ANYKEY_SCHEMA' should have detected invalid key # types. This is a defensive check against an invalid key type. diff --git a/securesystemslib/signer/__init__.py b/securesystemslib/signer/__init__.py index 3f43c3c7..d00be3dc 100644 --- a/securesystemslib/signer/__init__.py +++ b/securesystemslib/signer/__init__.py @@ -16,6 +16,11 @@ SSlibSigner, ) from securesystemslib.signer._sigstore_signer import SigstoreKey, SigstoreSigner +from securesystemslib.signer._spx_signer import ( + SpxKey, + SpxSigner, + generate_spx_key_pair, +) # Register supported private key uri schemes and the Signers implementing them SIGNER_FOR_URI_SCHEME.update( @@ -44,7 +49,7 @@ ("rsa", "rsa-pkcs1v15-sha256"): SSlibKey, ("rsa", "rsa-pkcs1v15-sha384"): SSlibKey, ("rsa", "rsa-pkcs1v15-sha512"): SSlibKey, - ("sphincs", "sphincs-shake-128s"): SSlibKey, + ("sphincs", "sphincs-shake-128s"): SpxKey, ("rsa", "pgp+rsa-pkcsv1.5"): GPGKey, ("dsa", "pgp+dsa-fips-180-2"): GPGKey, ("eddsa", "pgp+eddsa-ed25519"): GPGKey, diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index 11be617b..cc4a26c8 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -145,7 +145,7 @@ def verify_signature(self, signature: Signature, data: bytes) -> None: class SSlibKey(Key): - """Key implementation for RSA, Ed25519, ECDSA and Sphincs keys""" + """Key implementation for RSA, Ed25519, ECDSA keys""" def to_securesystemslib_key(self) -> Dict[str, Any]: """Internal helper, returns a classic securesystemslib keydict""" diff --git a/securesystemslib/signer/_spx_signer.py b/securesystemslib/signer/_spx_signer.py new file mode 100644 index 00000000..433940e4 --- /dev/null +++ b/securesystemslib/signer/_spx_signer.py @@ -0,0 +1,134 @@ +"""Signer implementation for project SPHINCS+ post-quantum signature support. + +""" +import logging +import os +from typing import Any, Dict, Optional, Tuple + +from securesystemslib.exceptions import ( + UnsupportedLibraryError, + UnverifiedSignatureError, + VerificationError, +) +from securesystemslib.signer._key import Key +from securesystemslib.signer._signature import Signature +from securesystemslib.signer._signer import SecretsHandler, Signer + +SPX_IMPORT_ERROR = None +try: + from pyspx import shake_128s +except ImportError: + SPX_IMPORT_ERROR = "spinhcs+ key support requires the pyspx library" + +_SHAKE_SEED_LEN = 48 + +logger = logging.getLogger(__name__) + + +def generate_spx_key_pair() -> Tuple[bytes, bytes]: + """Generate SPHINCS+ key pair and return public and private bytes.""" + if SPX_IMPORT_ERROR: + raise UnsupportedLibraryError(SPX_IMPORT_ERROR) + + seed = os.urandom(_SHAKE_SEED_LEN) + public, private = shake_128s.generate_keypair(seed) + + return public, private + + +class SpxKey(Key): + """SPHINCS+ verifier.""" + + DEFAULT_KEY_TYPE = "sphincs" + DEFAULT_SCHEME = "sphincs-shake-128s" + + @classmethod + def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "SpxKey": + keytype, scheme, keyval = cls._from_dict(key_dict) + return cls(keyid, keytype, scheme, keyval, key_dict) + + @classmethod + def from_bytes(cls, public: bytes) -> "SpxKey": + """Create SpxKey instance from public key bytes.""" + keytype = cls.DEFAULT_KEY_TYPE + scheme = cls.DEFAULT_SCHEME + keyval = {"public": public.hex()} + + keyid = SpxSigner._get_keyid( # pylint: disable=protected-access + keytype, scheme, keyval + ) + return cls(keyid, keytype, scheme, keyval) + + def to_dict(self) -> Dict[str, Any]: + return self._to_dict() + + def verify_signature(self, signature: Signature, data: bytes) -> None: + valid = None + try: + if SPX_IMPORT_ERROR: + raise UnsupportedLibraryError(SPX_IMPORT_ERROR) + + key = bytes.fromhex(self.keyval["public"]) + sig = bytes.fromhex(signature.signature) + + valid = shake_128s.verify(data, sig, key) + + except Exception as e: + logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) + raise VerificationError( + f"Unknown failure to verify signature by {self.keyid}" + ) from e + + if not valid: + raise UnverifiedSignatureError( + f"Failed to verify signature by {self.keyid}" + ) + + +class SpxSigner(Signer): + """SPHINCS+ signer. + + Usage:: + + public_bytes, private_bytes = generate_spx_key_pair() + public_key = SpxKey.from_bytes(public_bytes) + signer = SpxSigner(private_bytes, public_key) + signature = signer.sign(b"payload") + + # Use public_key.to_dict() / Key.from_dict() to transport public key data + public_key = signer.public_key + public_key.verify_signature(signature, b"payload") + + """ + + def __init__(self, private: bytes, public: SpxKey): + self.private_key = private + self.public_key = public + + @classmethod + def from_priv_key_uri( + cls, + priv_key_uri: str, + public_key: Key, + secrets_handler: Optional[SecretsHandler] = None, + ) -> "SpxSigner": + raise NotImplementedError + + def sign(self, payload: bytes) -> Signature: + """Signs payload with SPHINCS+ private key on the instance. + + Arguments: + payload: bytes to be signed. + + Raises: + UnsupportedLibraryError: PySPX is not available. + + Returns: + Signature. + + """ + if SPX_IMPORT_ERROR: + raise UnsupportedLibraryError(SPX_IMPORT_ERROR) + + raw = shake_128s.sign(payload, self.private_key) + return Signature(self.public_key.keyid, raw.hex()) diff --git a/securesystemslib/sphincs_keys.py b/securesystemslib/sphincs_keys.py deleted file mode 100755 index 13b4d829..00000000 --- a/securesystemslib/sphincs_keys.py +++ /dev/null @@ -1,95 +0,0 @@ -""" - - sphincs_keys.py - - - Ruben Gonzalez - - - Otober 12, 2022. - - - See LICENSE for licensing information. - - - The goal of this module is to include SPHINCS+ post-quantum signature support. - """ -# 'os' required to generate OS-specific randomness (os.urandom) suitable for -# cryptographic use. -# http://docs.python.org/2/library/os.html#miscellaneous-functions -import os - -from securesystemslib import exceptions, formats - -SPX_AVAIL = True -NO_SPX_MSG = "spinhcs+ key support requires the pyspx library" - -try: - from pyspx import shake_128s -except ImportError: - SPX_AVAIL = False - -_SHAKE_SEED_LEN = 48 - - -def generate_public_and_private(): - """Generates spx public and private key. - - Returns: - tuple: Containing the (public, private) keys. - Raises: - UnsupportedLibraryError: In case pyspx is not available. - """ - if not SPX_AVAIL: - raise exceptions.UnsupportedLibraryError(NO_SPX_MSG) - seed = os.urandom(_SHAKE_SEED_LEN) - public, private = shake_128s.generate_keypair(seed) - return public, private - - -def create_signature(public_key, private_key, data, scheme): - """Signs data with the private key. - Arguments: - public_key (bytes): The public key. Not used so far. - private_key (bytes): The private key. - data (bytes): The data to be signed. - scheme (str): The name of the scheme as defined in formats.py. - Returns: - tuple: Containing the values (signature, scheme). - Raises: - UnsupportedLibraryError: In case pyspx is not available. - """ - if not SPX_AVAIL: - raise exceptions.UnsupportedLibraryError(NO_SPX_MSG) - formats.SPHINCSPUBLIC_SCHEMA.check_match(public_key) - formats.SPHINCSPRIVATE_SCHEMA.check_match(private_key) - formats.SPHINCS_SIG_SCHEMA.check_match(scheme) - - signature = shake_128s.sign(data, private_key) - - return signature, scheme - - -def verify_signature(public_key, scheme, signature, data): - """Verify a signature using the public key. - Arguments: - public_key (bytes): The public key used for verification. - scheme (str): The name of the scheme as defined in formats.py. - signature (bytes): The sphincs+ signature as generated with create_signature. - data (bytes): The data that was signed. - Returns: - bool: True if the signature was valid, False otherwise. - Raises: - UnsupportedLibraryError: In case pyspx is not available. - """ - if not SPX_AVAIL: - raise exceptions.UnsupportedLibraryError(NO_SPX_MSG) - formats.SPHINCSPUBLIC_SCHEMA.check_match(public_key) - - # Is 'scheme' properly formatted? - formats.SPHINCS_SIG_SCHEMA.check_match(scheme) - - # Is 'signature' properly formatted? - formats.SPHINCSSIGNATURE_SCHEMA.check_match(signature) - - return shake_128s.verify(data, signature, public_key) diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index ea330e88..6dbfc3f8 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -43,7 +43,14 @@ UnsupportedLibraryError, VerificationError, ) -from securesystemslib.signer import GPGKey, Key, Signature, SSlibKey +from securesystemslib.signer import ( + GPGKey, + Key, + Signature, + SpxKey, + SpxSigner, + SSlibKey, +) from securesystemslib.signer._sigstore_signer import SigstoreKey @@ -200,13 +207,6 @@ def test_keys(self): ): securesystemslib.keys.create_signature(keydict, data) - keydict["keytype"] = "sphincs" - keydict["scheme"] = "sphincs-shake-128s" - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.create_signature(keydict, data) - keydict["keytype"] = "ecdsa" keydict["scheme"] = "ecdsa-sha2-nistp256" with self.assertRaises( @@ -233,17 +233,6 @@ def test_keys(self): ): securesystemslib.keys.verify_signature(keydict, sig, data) - keydict["keytype"] = "sphincs" - keydict["scheme"] = "sphincs-shake-128s" - sig = { - "keyid": "f00", - "sig": "A" * 7_856, - } - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.verify_signature(keydict, sig, data) - keydict["keytype"] = "rsa" keydict["scheme"] = "rsassa-pss-sha256" with self.assertRaises( @@ -332,6 +321,7 @@ def test_signer_verify(self): "Fulcio", {"identity": "val", "issuer": "val"}, ), + SpxKey(keyid, "sphincs", "sphincs-shake-128s", {"public": "val"}), ] for key in keys: @@ -342,6 +332,21 @@ def test_signer_verify(self): ctx.exception.__cause__, (UnsupportedLibraryError, ImportError) ) + def test_signer_sign(self): + """Assert UnsupportedLibraryError in sign.""" + signers = [ + SpxSigner( + b"private", + SpxKey( + "aa", "sphincs", "sphincs-shake-128s", {"public": "val"} + ), + ) + ] + + for signer in signers: + with self.assertRaises(UnsupportedLibraryError): + signer.sign(b"data") + def test_signer_ed25519_fallback(self): """Assert ed25519 signature verification works in pure Python.""" data = b"The quick brown fox jumps over the lazy dog" diff --git a/tests/test_keys.py b/tests/test_keys.py index b550197c..731c73b4 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -16,7 +16,6 @@ """ import copy -import os import unittest import securesystemslib.ecdsa_keys @@ -33,27 +32,6 @@ DATA = securesystemslib.formats.encode_canonical(DATA_STR).encode("utf-8") -@unittest.skipIf(os.name == "nt", "PySPX n/a on Windows") -class TestSphincsKeys(unittest.TestCase): - """Test create keys, sign and verify for sphincs keys.""" - - def test_sphincs_keys(self): - key = KEYS.generate_sphincs_key() - sig = KEYS.create_signature(key, b"data") - self.assertTrue(securesystemslib.formats.SIGNATURE_SCHEMA.matches(sig)) - - # Assert valid/invalid signature - self.assertTrue(KEYS.verify_signature(key, sig, b"data")) - self.assertFalse(KEYS.verify_signature(key, sig, b"not data")) - - # Assert verificaiton failure for unsupported signing scheme - key["scheme"] = "invalid_scheme" - with self.assertRaises( - securesystemslib.exceptions.UnsupportedAlgorithmError - ): - KEYS.verify_signature(key, sig, b"data") - - class TestKeys(unittest.TestCase): # pylint: disable=missing-class-docstring @classmethod def setUpClass(cls): diff --git a/tests/test_signer.py b/tests/test_signer.py index 929f8769..8cc20a0b 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -26,8 +26,11 @@ SecretsHandler, Signature, Signer, + SpxKey, + SpxSigner, SSlibKey, SSlibSigner, + generate_spx_key_pair, ) @@ -280,8 +283,6 @@ def setUpClass(cls): KEYS.generate_ed25519_key(), KEYS.generate_ecdsa_key(), ] - if os.name != "nt": - cls.keys.append(KEYS.generate_sphincs_key()) cls.DATA = b"DATA" @@ -639,6 +640,31 @@ def test_get_keyid(self): Signer._get_keyid("foo", "bar", {"baz": 1.1}) +@unittest.skipIf(os.name == "nt", "PySPX n/a on Windows") +class TestSphincs(unittest.TestCase): + """Test create keys, sign and verify for sphincs keys.""" + + def test_sphincs(self): + """sphincs signer smoketest.""" + + # Test create/sign/verify + public_bytes, private_bytes = generate_spx_key_pair() + public_key = SpxKey.from_bytes(public_bytes) + signer = SpxSigner(private_bytes, public_key) + sig = signer.sign(b"data") + self.assertIsNone(signer.public_key.verify_signature(sig, b"data")) + with self.assertRaises(UnverifiedSignatureError): + signer.public_key.verify_signature(sig, b"not data") + + # Test de/serialization + self.assertEqual( + signer.public_key, + SpxKey.from_dict( + signer.public_key.keyid, signer.public_key.to_dict() + ), + ) + + # Run the unit tests. if __name__ == "__main__": unittest.main()