Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sphincs+ To Signer Interface #427

Merged
merged 15 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ notes=TODO

[STRING]
check-quote-consistency=yes

[TYPECHECK]
generated-members=shake_128s.*
1 change: 1 addition & 0 deletions requirements-pinned.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ cryptography==38.0.2
pycparser==2.21 # via cffi
pynacl==1.5.0
six==1.16.0 # via pynacl
PySPX==0.5.0
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
#
cryptography >= 37.0.0; python_version >= '3'
pynacl
PySPX
rugo marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions securesystemslib/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
SCHEMA.String("ed25519"),
SCHEMA.String("ecdsa"),
SCHEMA.RegularExpression(r"ecdsa-sha2-nistp(256|384)"),
SCHEMA.String("sphincs"),
]
)

Expand Down Expand Up @@ -290,19 +291,27 @@
# 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()

# Ed25519 signature schemes. The vanilla Ed25519 signature scheme is currently
# 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",
Expand Down
42 changes: 42 additions & 0 deletions securesystemslib/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
formats,
rsa_keys,
settings,
sphincs_keys,
util,
)
from securesystemslib.hash import digest
Expand Down Expand Up @@ -349,6 +350,33 @@ 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):
"""
<Purpose>
Expand Down Expand Up @@ -697,6 +725,11 @@ 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
Expand Down Expand Up @@ -856,6 +889,15 @@ 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.
Expand Down
95 changes: 95 additions & 0 deletions securesystemslib/sphincs_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
<Program Name>
sphincs_keys.py

<Author>
Ruben Gonzalez <mail@ruben-gonzalez.de>

<Started>
Otober 12, 2022.

<Copyright>
See LICENSE for licensing information.

<Purpose>
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)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
extras_require={
"crypto": ["cryptography>=37.0.0"],
"pynacl": ["pynacl>1.2.0"],
"PySPX": ["PySPX==0.5.0"],
},
packages=find_packages(exclude=["tests", "debian"]),
scripts=[],
Expand Down
18 changes: 18 additions & 0 deletions tests/check_public_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ 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(
Expand All @@ -230,6 +237,17 @@ 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(
Expand Down
41 changes: 41 additions & 0 deletions tests/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ 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.sphincskey_dict = KEYS.generate_sphincs_key()

def test_generate_rsa_key(self):
_rsakey_dict = KEYS.generate_rsa_key() # pylint: disable=invalid-name
Expand Down Expand Up @@ -273,6 +274,7 @@ def test_create_signature(self):
# Creating a signature for 'DATA'.
rsa_signature = KEYS.create_signature(self.rsakey_dict, DATA)
ed25519_signature = KEYS.create_signature(self.ed25519key_dict, DATA)
sphincs_signature = KEYS.create_signature(self.sphincskey_dict, DATA)

# Check format of output.
self.assertEqual(
Expand All @@ -289,6 +291,13 @@ def test_create_signature(self):
),
FORMAT_ERROR_MSG,
)
self.assertEqual(
None,
securesystemslib.formats.SIGNATURE_SCHEMA.check_match(
sphincs_signature
),
FORMAT_ERROR_MSG,
)

# Test for invalid signature scheme.
args = (self.rsakey_dict, DATA)
Expand Down Expand Up @@ -342,6 +351,7 @@ def test_verify_signature(self): # pylint: disable=too-many-statements
rsa_signature = KEYS.create_signature(self.rsakey_dict, DATA)
ed25519_signature = KEYS.create_signature(self.ed25519key_dict, DATA)
ecdsa_signature = KEYS.create_signature(self.ecdsakey_dict, DATA)
sphincs_signature = KEYS.create_signature(self.sphincskey_dict, DATA)

# Verifying the 'signature' of 'DATA'.
verified = KEYS.verify_signature(self.rsakey_dict, rsa_signature, DATA)
Expand All @@ -365,6 +375,24 @@ def test_verify_signature(self): # pylint: disable=too-many-statements
)
self.ed25519key_dict["scheme"] = valid_scheme

# Verifying the 'sphincs_signature' of 'DATA'.
verified = KEYS.verify_signature(
self.sphincskey_dict, sphincs_signature, DATA
)
self.assertTrue(verified, "Incorrect signature.")

# Verify that an invalid sphincs signature scheme is rejected.
valid_scheme = self.sphincskey_dict["scheme"]
self.sphincskey_dict["scheme"] = "invalid_scheme"
self.assertRaises(
securesystemslib.exceptions.UnsupportedAlgorithmError,
KEYS.verify_signature,
self.sphincskey_dict,
sphincs_signature,
DATA,
)
self.sphincskey_dict["scheme"] = valid_scheme

# Verifying the 'ecdsa_signature' of 'DATA'.
verified = KEYS.verify_signature(
self.ecdsakey_dict, ecdsa_signature, DATA
Expand Down Expand Up @@ -410,6 +438,11 @@ def test_verify_signature(self): # pylint: disable=too-many-statements
)
self.assertFalse(verified, "Returned 'True' on an incorrect signature.")

verified = KEYS.verify_signature(
self.sphincskey_dict, sphincs_signature, _DATA
)
self.assertFalse(verified, "Returned 'True' on an incorrect signature.")

verified = KEYS.verify_signature(
self.ecdsakey_dict, ecdsa_signature, _DATA
)
Expand Down Expand Up @@ -458,6 +491,14 @@ def test_verify_signature(self): # pylint: disable=too-many-statements
)
self.assertTrue(verified, "Incorrect signature.")

# Verify that sphincs fails if PySPX is not installed
KEYS.sphincs_keys.SPX_AVAIL = False # Monkey patch availability
with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError
):
KEYS.verify_signature(self.sphincskey_dict, sphincs_signature, DATA)
KEYS.sphincs_keys.SPX_AVAIL = True

# Verify ecdsa key with HEX encoded keyval instead of PEM encoded keyval
ecdsa_key = KEYS.generate_ecdsa_key()
ecdsa_key["keyval"]["public"] = "abcd"
Expand Down
8 changes: 7 additions & 1 deletion tests/test_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ 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.sphincskey_dict = KEYS.generate_sphincs_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]
dicts = [
self.rsakey_dict,
self.ecdsakey_dict,
self.ed25519key_dict,
self.sphincskey_dict,
]
for scheme_dict in dicts:
# Test generation of signatures.
sslib_signer = SSlibSigner(scheme_dict)
Expand Down