Skip to content

Commit

Permalink
Merge pull request #18 from dkg/ffi-enable-seeds
Browse files Browse the repository at this point in the history
WIP: enable explicit use of seeds in ffi and Python
  • Loading branch information
integritychain authored Oct 12, 2024
2 parents 0fe4729 + 214d08e commit b775d89
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 22 deletions.
3 changes: 3 additions & 0 deletions ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
19 changes: 19 additions & 0 deletions ffi/fips203.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
26 changes: 22 additions & 4 deletions ffi/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,38 @@ 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:

```
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:
Expand All @@ -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
Expand Down
97 changes: 89 additions & 8 deletions ffi/python/fips203.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,38 @@
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:
```
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:
Expand All @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -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 '<ML-KEM Seed>'

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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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.'''

Expand All @@ -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):
Expand Down
97 changes: 97 additions & 0 deletions ffi/python/test/nist/keygen.py
Original file line number Diff line number Diff line change
@@ -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-(?P<strength>512|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()
Loading

0 comments on commit b775d89

Please sign in to comment.