diff --git a/ffi/README.md b/ffi/README.md index 8660f90..c39b677 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -2,11 +2,18 @@ FIPS 205 FFI Thank you to Daniel Kahn Gillmor for the FIPS 203 example. -Currently only implemented for `slh_dsa_sha2_128f` until A) more testing has been developed, B) better integration with build flow. +This is a work in progress, and currently only implements `slh_dsa_sha2_128f` until A) more +testing has been developed, B) better integration with build flow. ~~~ +To generate the C bindings... + $ cd ffi # here $ cargo build $ cd tests $ make + + +For Python (after generating the bindings) +$ python3 fips205.py ~~~ \ No newline at end of file diff --git a/ffi/fips205.py b/ffi/fips205.py new file mode 100644 index 0000000..b2c9533 --- /dev/null +++ b/ffi/fips205.py @@ -0,0 +1,186 @@ +"""FIPS 205 (SLH-DSA) Asymmetric Post-Quantum Cryptography + +This Python module provides an implementation of FIPS 205, the +Stateless Hash-Based Digital Signature Standard (SLH-DSA). Until +further testing is performed, support is limited to the +`slh_dsa_sha2_128f` security parameter set. + + +## Example + +The following example shows using the standard SLH_DSA algorithm +to sign and verify message byte arrays: + +``` +from fips205 import slh_dsa_sha2_128f + +message = b"this is the message" +(public_key, private_key) = slh_dsa_sha2_128f.keygen() +signature = private_key.sign(message) +check = public_key.verify(message, signature) +assert(check) +``` + +Public and private keys can be serialized by accessing them as +`bytes`, and deserialized by initializing them with the +appropriate size `bytes` object. Signature are `bytes` objects. + +An example demonstrating key generation, signature creation, +serialization and deserialization of the public key, and +signature verification: + +``` +from fips205 import slh_dsa_sha2_128f + +message = b"this is another message" +(public_key, private_key) = slh_dsa_sha2_128f.keygen() +signature = private_key.sign(message) + +with open('public.bin', 'wb') as f: + f.write(bytes(public_key)) + +from fips205 import PublicKey + +with open('public.bin', 'rb') as f: + pk_bytes = f.read() + +pk = PublicKey(pk_bytes, slh_dsa_sha2_128f) +check = pk.verify(message, signature) +``` + + +## Implementation Notes + +This is a wrapper around libfips205, built from the Rust fips205-ffi crate. +If that library is not installed in the expected path for libraries on +your system, any attempt to use this module will fail. + +Thank you to Daniel Kahn Gillmor for providing an example for FIPS 203. + + +## See Also + +- https://doi.org/10.6028/NIST.FIPS.205.ipd +- https://github.com/integritychain/fips205 + +""" + +__version__ = "0.1.2" +__author__ = "Eric Schorn " + +import ctypes +import enum +from abc import ABC +from typing import Tuple, Any, Callable + +lib = ctypes.CDLL("../target/debug/libfips205.so") + + +class Err(enum.IntEnum): + OK = 0 + NULL_PTR_ERROR = 1 + SERIALIZATION_ERROR = 2 + DESERIALIZATION_ERROR = 3 + KEYGEN_ERROR = 4 + SIGN_ERROR = 5 + VERIFY_ERROR = 6 + + +class PublicKey: + """SLH_DSA Public Key + + Serialize this object by asking for it as `bytes`. + Verify a message + signature by invoking verify() on it. + """ + + def __init__(self, data: bytes, source: Any) -> None: + """Create SLH_DSA Public Key from bytes, noting its source.""" + if len(data) != source.PK_LEN: + raise Exception(f"Inconsistent public key source/length") + self.key = data + self.source = source + + def __repr__(self) -> str: + return f"<{self.source.__name__} public key: {self.key.hex()}>" + + def __bytes__(self) -> bytes: + return bytes(self.key) + + def verify(self, message: bytes, signature: bytes) -> bool: + """Verify a message and signature pair""" + if not (isinstance(signature, bytes) and isinstance(message, bytes)): + raise Exception(f"Signature and message must be bytes") + if len(signature) != self.source.SIG_LEN: + raise Exception(f"Inconsistent signature source/length") + ret = self.source.verify_func(message, len(message), self.key, signature) + if (ret != Err.OK) & (ret != Err.VERIFY_ERROR): + raise Exception(f"{self.source.__name__}_verify() returned {ret}") + return ret == Err.OK + + +class PrivateKey: + """SLH_DSA Private Key + + Serialize this object by asking for it as `bytes`. + Sign a message by invoking sign() on it. + """ + + def __init__(self, data: bytes, source: Any) -> None: + """Create SLH_DSA Private Key from bytes, noting its source.""" + if len(data) != source.SK_LEN: + raise Exception(f"Inconsistent private key source/length") + self.key = data + self.source = source + + def __repr__(self) -> str: + return f"<{self.source.__name__} private key: {self.key.hex()}>" + + def __bytes__(self) -> bytes: + return bytes(self.key) + + def sign(self, message: bytes) -> bytes: + """Sign a message""" + if not isinstance(message, bytes): + raise Exception(f"Message must be bytes") + signature = bytes([0] * self.source.SIG_LEN) + ret = self.source.sign_func(message, len(message), self.key, signature) + if ret != Err.OK: + raise Exception(f"{self.source.__name__}_sign() returned {ret}") + return signature + + +class _Slh_Dsa(ABC): + """Abstract base class for all SLH-DSA (FIPS 205) parameter sets.""" + PK_LEN: int + SK_LEN: int + SIG_LEN: int + keygen_func: Callable + sign_func: Callable + verify_func: Callable + + @classmethod + def keygen(cls) -> Tuple[PublicKey, PrivateKey]: + """Generate a pair of Public and Private Keys.""" + (pk, sk) = (bytes([0] * cls.PK_LEN), bytes([0] * cls.SK_LEN)) + ret = cls.keygen_func(pk, sk) + if ret != Err.OK: + raise Exception(f"{cls.__name__}_keygen() returned {ret}") + return PublicKey(pk, cls), PrivateKey(sk, cls) + + +class slh_dsa_sha2_128f(_Slh_Dsa): + """slh_dsa_sha2_128f (FIPS 205) implementation.""" + PK_LEN: int = 32 + SK_LEN: int = 64 + SIG_LEN: int = 17088 + keygen_func: Callable = lib.slh_dsa_sha2_128f_keygen + sign_func: Callable = lib.slh_dsa_sha2_128f_sign + verify_func: Callable = lib.slh_dsa_sha2_128f_verify + + +# A quick test can be run from the command line: `python3 fips205.py` +if __name__ == "__main__": + ppk, ssk = slh_dsa_sha2_128f.keygen() + sig = ssk.sign(bytes([1, 2, 3])) + res = ppk.verify(bytes([1, 2, 3]), sig) + print(f"{res}\n{ppk}\n{ssk}") diff --git a/src/types.rs b/src/types.rs index 3391782..347e4e1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -47,15 +47,12 @@ pub(crate) struct ForsPk { } -/// Fig 10? +/// Fig 10 #[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)] pub(crate) struct Auth { pub(crate) tree: [[u8; N]; A], } -impl Default for Auth { - fn default() -> Self { Auth { tree: [[0u8; N]; A] } } -} #[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)] pub(crate) struct HtSig { @@ -82,7 +79,6 @@ pub struct XmssSig { impl XmssSig { pub(crate) fn get_wots_sig(&self) -> &WotsSig { &self.sig_wots } - pub(crate) fn get_xmss_auth(&self) -> &[[u8; N]; HP] { &self.auth } }