diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 4266e5f..a2b61ab 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -16,6 +16,9 @@ crate-type = ["staticlib", "cdylib"] bench = false name = "fips203" +[dependencies] +rand_core = { version = "0.6.4", features = ["getrandom"] } + [dependencies.fips203] path = ".." version = "0.4.0" diff --git a/ffi/fips203.h b/ffi/fips203.h index daff844..430507f 100644 --- a/ffi/fips203.h +++ b/ffi/fips203.h @@ -29,6 +29,10 @@ typedef struct ml_kem_shared_secret { uint8_t data[32]; } ml_kem_shared_secret; +typedef struct ml_kem_seed { + uint8_t data[64]; +} ml_kem_seed; + typedef struct ml_kem_512_encaps_key { uint8_t data[800]; @@ -70,9 +74,16 @@ typedef struct ml_kem_1024_ciphertext { extern "C" { #endif + +ml_kem_err ml_kem_populate_seed(ml_kem_seed *seed_out); + ml_kem_err ml_kem_512_keygen(ml_kem_512_encaps_key *encaps_out, ml_kem_512_decaps_key *decaps_out); +ml_kem_err ml_kem_512_keygen_from_seed(const ml_kem_seed *d_z, + ml_kem_512_encaps_key *encaps_out, + ml_kem_512_decaps_key *decaps_out); + ml_kem_err ml_kem_512_encaps(const ml_kem_512_encaps_key *encaps, ml_kem_512_ciphertext *ciphertext_out, ml_kem_shared_secret *shared_secret_out); @@ -84,6 +95,10 @@ ml_kem_err ml_kem_512_decaps(const ml_kem_512_decaps_key *decaps, ml_kem_err ml_kem_768_keygen(ml_kem_768_encaps_key *encaps_out, ml_kem_768_decaps_key *decaps_out); +ml_kem_err ml_kem_768_keygen_from_seed(const ml_kem_seed *d_z, + ml_kem_768_encaps_key *encaps_out, + ml_kem_768_decaps_key *decaps_out); + ml_kem_err ml_kem_768_encaps(const ml_kem_768_encaps_key *encaps, ml_kem_768_ciphertext *ciphertext_out, ml_kem_shared_secret *shared_secret_out); @@ -95,6 +110,10 @@ ml_kem_err ml_kem_768_decaps(const ml_kem_768_decaps_key *decaps, ml_kem_err ml_kem_1024_keygen(ml_kem_1024_encaps_key *encaps_out, ml_kem_1024_decaps_key *decaps_out); +ml_kem_err ml_kem_1024_keygen_from_seed(const ml_kem_seed *d_z, + ml_kem_1024_encaps_key *encaps_out, + ml_kem_1024_decaps_key *decaps_out); + ml_kem_err ml_kem_1024_encaps(const ml_kem_1024_encaps_key *encaps, ml_kem_1024_ciphertext *ciphertext_out, ml_kem_shared_secret *shared_secret_out); diff --git a/ffi/python/README.md b/ffi/python/README.md index 049d5b9..153db8a 100644 --- a/ffi/python/README.md +++ b/ffi/python/README.md @@ -20,8 +20,23 @@ shared_secret_2 = decapsulation_key.decaps(ciphertext) assert(shared_secret_1 == shared_secret_2) ``` -Encapsulation keys, decapsulation keys, and ciphertexts can all be -serialized by accessing them as `bytes`, and deserialized by + +Key generation can also be done deterministically, by passing a +`SEED_SIZE`-byte seed (the concatenation of d and z) to `keygen`: + +``` +from fips203 import ML_KEM_512, Seed + +seed1 = Seed() # Generate a random seed +(ek1, dk1) = ML_KEM_512.keygen(seed1) + +seed2 = Seed(b'\x00'*ML_KEM_512.SEED_SIZE) # This seed is clearly not a secret! +(ek2, dk2) = ML_KEM_512.keygen(seed2) +``` + + +Encapsulation keys, decapsulation keys, seeds, and ciphertexts can all +be serialized by accessing them as `bytes`, and deserialized by initializing them with the appropriate size bytes object. A serialization example: @@ -29,11 +44,14 @@ A serialization example: ``` from fips203 import ML_KEM_768 -(ek,dk) = ML_KEM_768.keygen() +seed = Seed() +(ek,dk) = ML_KEM_768.keygen(seed) with open('encapskey.bin', 'wb') as f: f.write(bytes(ek)) with open('decapskey.bin', 'wb') as f: f.write(bytes(dk)) +with open('seed.bin', 'wb') as f: + f.write(bytes(seed) ``` A deserialization example, followed by use: @@ -50,7 +68,7 @@ ek = fips203.EncapsulationKey(ekdata) The expected sizes (in bytes) of the different objects in each parameter set can be accessed with `EK_SIZE`, `DK_SIZE`, `CT_SIZE`, -and `SS_SIZE`: +`SEED_SIZE`, and `SS_SIZE`: ``` from fips203 import ML_KEM_768 diff --git a/ffi/python/fips203.py b/ffi/python/fips203.py index 6ad5ccb..b1a2b00 100644 --- a/ffi/python/fips203.py +++ b/ffi/python/fips203.py @@ -20,8 +20,23 @@ assert(shared_secret_1 == shared_secret_2) ``` -Encapsulation keys, decapsulation keys, and ciphertexts can all be -serialized by accessing them as `bytes`, and deserialized by + +Key generation can also be done deterministically, by passing a +`SEED_SIZE`-byte seed (the concatenation of d and z) to `keygen`: + +``` +from fips203 import ML_KEM_512, Seed + +seed1 = Seed() # Generate a random seed +(ek1, dk1) = ML_KEM_512.keygen(seed1) + +seed2 = Seed(b'\x00'*ML_KEM_512.SEED_SIZE) # This seed is clearly not a secret! +(ek2, dk2) = ML_KEM_512.keygen(seed2) +``` + + +Encapsulation keys, decapsulation keys, seeds, and ciphertexts can all +be serialized by accessing them as `bytes`, and deserialized by initializing them with the appropriate size bytes object. A serialization example: @@ -29,11 +44,14 @@ ``` from fips203 import ML_KEM_768 -(ek,dk) = ML_KEM_768.keygen() +seed = Seed() +(ek,dk) = ML_KEM_768.keygen(seed) with open('encapskey.bin', 'wb') as f: f.write(bytes(ek)) with open('decapskey.bin', 'wb') as f: f.write(bytes(dk)) +with open('seed.bin', 'wb') as f: + f.write(bytes(seed) ``` A deserialization example, followed by use: @@ -50,7 +68,7 @@ The expected sizes (in bytes) of the different objects in each parameter set can be accessed with `EK_SIZE`, `DK_SIZE`, `CT_SIZE`, -and `SS_SIZE`: +`SEED_SIZE`, and `SS_SIZE`: ``` from fips203 import ML_KEM_768 @@ -79,6 +97,7 @@ Please report issues at https://github.com/integritychain/fips203/issues ''' +from __future__ import annotations '''__version__ should track package.version from ../Cargo.toml''' __version__ = '0.4.0' @@ -90,18 +109,22 @@ 'Ciphertext', 'EncapsulationKey', 'DecapsulationKey', + 'Seed', ] import ctypes import ctypes.util import enum -from typing import Tuple, Dict, Any, Union +import secrets +from typing import Tuple, Dict, Any, Union, Optional from abc import ABC class _SharedSecret(ctypes.Structure): _fields_ = [('data', ctypes.c_uint8 * 32)] +class _Seed(ctypes.Structure): + _fields_ = [('data', ctypes.c_uint8 * 64)] class Err(enum.IntEnum): OK = 0 @@ -113,6 +136,35 @@ class Err(enum.IntEnum): DECAPSULATION_ERROR = 6 +class Seed(): + '''ML-KEM Seed + + This seed can be used to generate an ML-KEM keypair + ''' + def __init__(self, data: Optional[bytes] = None) -> None: + '''If initialized with None, the seed will be randomly populated.''' + self._seed = _Seed() + if data is None: + # FIXME: perhaps use ml_kem_populate_seed instead? + data = secrets.token_bytes(len(self._seed.data)) + if len(data) != len(self._seed.data): + raise ValueError(f"Expected {len(self._seed.data)} bytes, " + f"got {len(data)}.") + for i in range(len(data)): + self._seed.data[i] = data[i] + + def __repr__(self) -> str: + return '' + + def __bytes__(self) -> bytes: + return bytes(self._seed.data) + + def keygen(self, strength: int) -> Tuple[EncapsulationKey, DecapsulationKey]: + for kt in ML_KEM_512, ML_KEM_768, ML_KEM_1024: + if kt._strength == strength: + return kt.keygen(self) + raise Exception(f"Unknown strength: {strength}, must be 512, 768, or 1024.") + class Ciphertext(): '''ML-KEM Ciphertext @@ -293,6 +345,12 @@ class _Ciphertext(ctypes.Structure): ctypes.POINTER(_DecapsKey)] ffi['keygen'].restype = ctypes.c_uint8 + ffi['keygen_from_seed'] = cls.lib[f'ml_kem_{level}_keygen_from_seed'] + ffi['keygen_from_seed'].argtypes = [ctypes.POINTER(_Seed), + ctypes.POINTER(_EncapsKey), + ctypes.POINTER(_DecapsKey)] + ffi['keygen_from_seed'].restype = ctypes.c_uint8 + ffi['encaps'] = cls.lib[f'ml_kem_{level}_encaps'] ffi['encaps'].argtypes = [ctypes.POINTER(_EncapsKey), ctypes.POINTER(_Ciphertext), @@ -335,6 +393,22 @@ def _keygen(cls, strength: int) -> Tuple[EncapsulationKey, return (ek, dk) + @classmethod + def _keygen_from_seed(cls, strength: int, seed: Seed) -> Tuple[EncapsulationKey, + DecapsulationKey]: + ek = EncapsulationKey(strength) + dk = DecapsulationKey(strength) + + ret = Err(cls.strength(strength)['keygen_from_seed']( + ctypes.byref(seed._seed), + ctypes.byref(ek._ek), + ctypes.byref(dk._dk) + )) + if ret is not Err.OK: + raise Exception(f"ml_kem_{strength}_keygen() returned " + f"{ret} ({ret.name})") + return (ek, dk) + class ML_KEM(ABC): '''Abstract base class for all ML-KEM (FIPS 203) parameter sets.''' @@ -343,11 +417,18 @@ class ML_KEM(ABC): DK_SIZE: int CT_SIZE: int SS_SIZE: int = 32 + SEED_SIZE: int = 64 @classmethod - def keygen(cls) -> Tuple[EncapsulationKey, DecapsulationKey]: - '''Generate a pair of Encapsulation and Decapsulation Keys.''' - return _ML_KEM._keygen(cls._strength) + def keygen(cls, seed: Optional[Seed]) -> Tuple[EncapsulationKey, DecapsulationKey]: + '''Generate a pair of Encapsulation and Decapsulation Keys. + + If a Seed is supplied, do a deterministic generation from the seed. + Otherwise, randomly generate the key.''' + if seed is None: + return _ML_KEM._keygen(cls._strength) + else: + return _ML_KEM._keygen_from_seed(cls._strength, seed) class ML_KEM_512(ML_KEM): diff --git a/ffi/python/test/nist/keygen.py b/ffi/python/test/nist/keygen.py new file mode 100755 index 0000000..e5ba175 --- /dev/null +++ b/ffi/python/test/nist/keygen.py @@ -0,0 +1,97 @@ +#!/usr/bin/python3 +"""Tests for fips203 python module + +From the ffi/python/ directory, do: + +PYTHONPATH=. test/nist/keygen.py + +""" +from __future__ import annotations + +import fips203 +import json +import re +from binascii import a2b_hex, b2a_hex + +from typing import Dict, Union, List, TypedDict + +with open( + "../../tests/nist_vectors/ML-KEM-keyGen-FIPS203/internalProjection.json" +) as f: + t = json.load(f) + +assert t["vsId"] == 42 +assert t["algorithm"] == "ML-KEM" +assert t["mode"] == "keyGen" +assert t["revision"] == "FIPS203" +assert t["isSample"] == False + + +class KeyGenTestData(TypedDict): + tcId: int + deferred: bool + z: str + d: str + ek: str + dk: str + + +class KeyGenTest: + def __init__(self, data: KeyGenTestData): + self.tcId = data["tcId"] + self.deferred = data["deferred"] + self.d = a2b_hex(data["d"]) + self.z = a2b_hex(data["z"]) + self.ek = a2b_hex(data["ek"]) + self.dk = a2b_hex(data["dk"]) + + def run(self, group: TestGroup) -> None: + seed = fips203.Seed(self.d + self.z) + (ek, dk) = seed.keygen(group.strength) + if bytes(ek) != self.ek: + raise Exception( + f"""test {self.tcId} (group {group.tgId}, str: {group.strength}) ek failed: + got: {b2a_hex(bytes(ek))} + wanted: {b2a_hex(self.ek)}""" + ) + if bytes(dk) != self.dk: + raise Exception( + f"""test {self.tcId} (group {group.tgId}, str: {group.strength}) dk failed: + got: {b2a_hex(bytes(dk))} + wanted: {b2a_hex(self.dk)}""" + ) + + +class TestGroupData(TypedDict): + tgId: int + testType: str + parameterSet: str + tests: List[KeyGenTestData] + + +class TestGroup: + param_matcher = re.compile("^ML-KEM-(?P512|768|1024)$") + + def __init__(self, d: TestGroupData) -> None: + self.tgId: int = d["tgId"] + self.testType: str = d["testType"] + assert self.testType == "AFT" # i don't know what AFT means + self.parameterSet: str = d["parameterSet"] + m = self.param_matcher.match(self.parameterSet) + assert m + self.strength: int = int(m["strength"]) + self.tests: List[KeyGenTest] = [] + for t in d["tests"]: + self.tests.append(KeyGenTest(t)) + + def run(self) -> None: + for t in self.tests: + t.run(self) + + +groups: List[TestGroup] = [] +for g in t["testGroups"]: + groups.append(TestGroup(g)) + +for g in groups: + g.run() diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index e811cba..48a0e89 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -1,10 +1,15 @@ -//use fips203; +use rand_core::{OsRng, RngCore}; #[repr(C)] pub struct ml_kem_shared_secret { data: [u8; fips203::SSK_LEN], } +#[repr(C)] +pub struct ml_kem_seed { + data: [u8; 64], +} + pub const ML_KEM_OK: u8 = 0; pub const ML_KEM_NULL_PTR_ERROR: u8 = 1; pub const ML_KEM_SERIALIZATION_ERROR: u8 = 2; @@ -13,6 +18,15 @@ pub const ML_KEM_KEYGEN_ERROR: u8 = 4; pub const ML_KEM_ENCAPSULATION_ERROR: u8 = 5; pub const ML_KEM_DECAPSULATION_ERROR: u8 = 6; +#[no_mangle] +pub extern "C" fn ml_kem_populate_seed(seed_out: Option<&mut ml_kem_seed>) -> u8 { + let Some(seed_out) = seed_out else { + return ML_KEM_NULL_PTR_ERROR; + }; + OsRng.fill_bytes(&mut seed_out.data); + ML_KEM_OK +} + // ML-KEM-512 #[repr(C)] @@ -30,7 +44,8 @@ pub struct ml_kem_512_ciphertext { #[no_mangle] pub extern "C" fn ml_kem_512_keygen( - encaps_out: Option<&mut ml_kem_512_encaps_key>, decaps_out: Option<&mut ml_kem_512_decaps_key>, + encaps_out: Option<&mut ml_kem_512_encaps_key>, + decaps_out: Option<&mut ml_kem_512_decaps_key>, ) -> u8 { use fips203::traits::{KeyGen, SerDes}; @@ -46,9 +61,31 @@ pub extern "C" fn ml_kem_512_keygen( ML_KEM_OK } +#[no_mangle] +pub extern "C" fn ml_kem_512_keygen_from_seed( + seed: Option<&ml_kem_seed>, + encaps_out: Option<&mut ml_kem_512_encaps_key>, + decaps_out: Option<&mut ml_kem_512_decaps_key>, +) -> u8 { + use fips203::traits::{KeyGen, SerDes}; + + let (Some(encaps_out), Some(decaps_out), Some(seed)) = (encaps_out, decaps_out, seed) else { + return ML_KEM_NULL_PTR_ERROR; + }; + let (ek, dk) = fips203::ml_kem_512::KG::keygen_from_seed( + seed.data[0..32].try_into().unwrap(), + seed.data[32..64].try_into().unwrap(), + ); + + encaps_out.data = ek.into_bytes(); + decaps_out.data = dk.into_bytes(); + ML_KEM_OK +} + #[no_mangle] pub extern "C" fn ml_kem_512_encaps( - encaps: Option<&ml_kem_512_encaps_key>, ciphertext_out: Option<&mut ml_kem_512_ciphertext>, + encaps: Option<&ml_kem_512_encaps_key>, + ciphertext_out: Option<&mut ml_kem_512_ciphertext>, shared_secret_out: Option<&mut ml_kem_shared_secret>, ) -> u8 { use fips203::traits::{Encaps, SerDes}; @@ -72,7 +109,8 @@ pub extern "C" fn ml_kem_512_encaps( #[no_mangle] pub extern "C" fn ml_kem_512_decaps( - decaps: Option<&ml_kem_512_decaps_key>, ciphertext: Option<&ml_kem_512_ciphertext>, + decaps: Option<&ml_kem_512_decaps_key>, + ciphertext: Option<&ml_kem_512_ciphertext>, shared_secret_out: Option<&mut ml_kem_shared_secret>, ) -> u8 { use fips203::traits::{Decaps, SerDes}; @@ -113,7 +151,8 @@ pub struct ml_kem_768_ciphertext { #[no_mangle] pub extern "C" fn ml_kem_768_keygen( - encaps_out: Option<&mut ml_kem_768_encaps_key>, decaps_out: Option<&mut ml_kem_768_decaps_key>, + encaps_out: Option<&mut ml_kem_768_encaps_key>, + decaps_out: Option<&mut ml_kem_768_decaps_key>, ) -> u8 { use fips203::traits::{KeyGen, SerDes}; @@ -129,9 +168,31 @@ pub extern "C" fn ml_kem_768_keygen( ML_KEM_OK } +#[no_mangle] +pub extern "C" fn ml_kem_768_keygen_from_seed( + seed: Option<&ml_kem_seed>, + encaps_out: Option<&mut ml_kem_768_encaps_key>, + decaps_out: Option<&mut ml_kem_768_decaps_key>, +) -> u8 { + use fips203::traits::{KeyGen, SerDes}; + + let (Some(encaps_out), Some(decaps_out), Some(seed)) = (encaps_out, decaps_out, seed) else { + return ML_KEM_NULL_PTR_ERROR; + }; + let (ek, dk) = fips203::ml_kem_768::KG::keygen_from_seed( + seed.data[0..32].try_into().unwrap(), + seed.data[32..64].try_into().unwrap(), + ); + + encaps_out.data = ek.into_bytes(); + decaps_out.data = dk.into_bytes(); + ML_KEM_OK +} + #[no_mangle] pub extern "C" fn ml_kem_768_encaps( - encaps: Option<&ml_kem_768_encaps_key>, ciphertext_out: Option<&mut ml_kem_768_ciphertext>, + encaps: Option<&ml_kem_768_encaps_key>, + ciphertext_out: Option<&mut ml_kem_768_ciphertext>, shared_secret_out: Option<&mut ml_kem_shared_secret>, ) -> u8 { use fips203::traits::{Encaps, SerDes}; @@ -155,7 +216,8 @@ pub extern "C" fn ml_kem_768_encaps( #[no_mangle] pub extern "C" fn ml_kem_768_decaps( - decaps: Option<&ml_kem_768_decaps_key>, ciphertext: Option<&ml_kem_768_ciphertext>, + decaps: Option<&ml_kem_768_decaps_key>, + ciphertext: Option<&ml_kem_768_ciphertext>, shared_secret_out: Option<&mut ml_kem_shared_secret>, ) -> u8 { use fips203::traits::{Decaps, SerDes}; @@ -213,9 +275,31 @@ pub extern "C" fn ml_kem_1024_keygen( ML_KEM_OK } +#[no_mangle] +pub extern "C" fn ml_kem_1024_keygen_from_seed( + seed: Option<&ml_kem_seed>, + encaps_out: Option<&mut ml_kem_1024_encaps_key>, + decaps_out: Option<&mut ml_kem_1024_decaps_key>, +) -> u8 { + use fips203::traits::{KeyGen, SerDes}; + + let (Some(encaps_out), Some(decaps_out), Some(seed)) = (encaps_out, decaps_out, seed) else { + return ML_KEM_NULL_PTR_ERROR; + }; + let (ek, dk) = fips203::ml_kem_1024::KG::keygen_from_seed( + seed.data[0..32].try_into().unwrap(), + seed.data[32..64].try_into().unwrap(), + ); + + encaps_out.data = ek.into_bytes(); + decaps_out.data = dk.into_bytes(); + ML_KEM_OK +} + #[no_mangle] pub extern "C" fn ml_kem_1024_encaps( - encaps: Option<&ml_kem_1024_encaps_key>, ciphertext_out: Option<&mut ml_kem_1024_ciphertext>, + encaps: Option<&ml_kem_1024_encaps_key>, + ciphertext_out: Option<&mut ml_kem_1024_ciphertext>, shared_secret_out: Option<&mut ml_kem_shared_secret>, ) -> u8 { use fips203::traits::{Encaps, SerDes}; @@ -239,7 +323,8 @@ pub extern "C" fn ml_kem_1024_encaps( #[no_mangle] pub extern "C" fn ml_kem_1024_decaps( - decaps: Option<&ml_kem_1024_decaps_key>, ciphertext: Option<&ml_kem_1024_ciphertext>, + decaps: Option<&ml_kem_1024_decaps_key>, + ciphertext: Option<&ml_kem_1024_ciphertext>, shared_secret_out: Option<&mut ml_kem_shared_secret>, ) -> u8 { use fips203::traits::{Decaps, SerDes}; diff --git a/ffi/tests/Makefile b/ffi/tests/Makefile index 00b5379..3752e30 100644 --- a/ffi/tests/Makefile +++ b/ffi/tests/Makefile @@ -8,7 +8,7 @@ # (cd tests && make AS_INSTALLED=true) SIZES = 512 768 1024 -FRAMES = encaps_key decaps_key ciphertext encaps decaps keygen +FRAMES = encaps_key decaps_key ciphertext encaps decaps keygen keygen_from_seed # should derive SONAME somehow, e.g. from CARGO_PKG_VERSION_MAJOR SONAME = 0 diff --git a/ffi/tests/baseline.c b/ffi/tests/baseline.c index 611ee0c..8a065fc 100644 --- a/ffi/tests/baseline.c +++ b/ffi/tests/baseline.c @@ -1,16 +1,36 @@ #include +#include #include int main(int argc, const char **argv) { MLKEM_encaps_key encaps; MLKEM_decaps_key decaps; + MLKEM_encaps_key encaps_2; + MLKEM_decaps_key decaps_2; MLKEM_ciphertext ct; ml_kem_shared_secret ssk_a; ml_kem_shared_secret ssk_b; + ml_kem_seed seed; ml_kem_err err; MLKEM_encaps_key encaps_weird; MLKEM_decaps_key decaps_weird; + memset (&seed, 0, sizeof(seed)); + + /* ensure that seed-based generation is deterministic */ + if (MLKEM_keygen_from_seed (&seed, &encaps, &decaps)) + return 1; + if (MLKEM_keygen_from_seed (&seed, &encaps_2, &decaps_2)) + return 1; + if (memcmp(&encaps, &encaps_2, sizeof(encaps))) { + fprintf (stderr, "encaps keys generated by seed did not match\n"); + return 5; + } + if (memcmp(&decaps, &decaps_2, sizeof(decaps))) { + fprintf (stderr, "decaps keys generated by seed did not match\n"); + return 6; + } + if (MLKEM_keygen (&encaps, &decaps)) return 1;