Repository: integritychain/fips203 (language: python)

Issue:
Add minimalist C shared library crate.
This shared object provides a C API/ABI for a dynamically linked library.

It is deliberately minimal, and can be linked against with the included fips203.h, and subsequently linked against the .so built by this cdylib.

The caller handles only serialized forms of the underlying cryptographic objects, and is responsible for all memory allocation/destruction.

A more sophisticated library interface seems possible, but this one is clean and can be easily bound to other programming languages that already have the ability to wrap a C FFI.

This includes a simple test, which should be able to be run with:

    (cd tests && make)

On any system that has GNU make and a C compiler available.

I don't know how to integrate such a test into cargo's test suite, though.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 74d7196..f4a0828 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,4 @@
-workspace = { members = [], exclude = ["dudect", "ct_cm4"] }
+workspace = { members = ['ffi'], exclude = ["dudect", "ct_cm4"] }
 
 [package]
 name = "fips203"
diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml
new file mode 100644
index 0000000..4ccf94f
--- /dev/null
+++ b/ffi/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "fips203-ffi"
+version = "0.1.0"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+description = "C shared library exposing FIPS 203 (draft): Module-Lattice-Based Key-Encapsulation Mechanism"
+authors = ["Daniel Kahn Gillmor <dkg@fifthhorseman.net>"]
+documentation = "https://docs.rs/fips203"
+categories = ["cryptography"]
+repository = "https://github.com/integritychain/fips203"
+keywords = ["FIPS", "203", "lattice", "kem", "ml"]
+rust-version = "1.70"
+
+[lib]
+crate-type = ["staticlib", "cdylib"]
+bench = false
+name = "fips203"
+
+[dependencies.fips203]
+path = ".."
+version = "0.1.2"
diff --git a/ffi/README.md b/ffi/README.md
new file mode 100644
index 0000000..bfa2dee
--- /dev/null
+++ b/ffi/README.md
@@ -0,0 +1,35 @@
+# C Shared Object for ML-KEM
+
+This crate provides a shared object (dynamically-linked library) using standard C FFI ABI that provides a functional implementation of ML-KEM.
+
+The goals of this implementation are:
+
+- simplicity
+- correctness
+- caller deals only with serialized objects
+- no library-specific memory management (caller manages all objects)
+- no internal state held by the library between calls
+- minimal symbol visibility
+- stable API/ABI
+
+security-related goals:
+
+- constant-time operations (needs improvement in underlying Rust code)
+- clean library RAM (objects should be zeroed out of any library-allocated memory before function exit)
+
+non-goals are:
+
+- speed
+- efficiency
+- size
+
+# Outstanding work
+
+- better internal error handling
+- testing!
+- reduce symbol visibility in shared object
+- SONAME (bound to major version?)
+
+# Paths considered but discarded
+
+- Autogenerate stable C headers (e.g. with cbindgen); manually-crafted headers are probably fine, given the simplicity of the API/ABI
diff --git a/ffi/fips203.h b/ffi/fips203.h
new file mode 100644
index 0000000..8b30674
--- /dev/null
+++ b/ffi/fips203.h
@@ -0,0 +1,108 @@
+#ifndef __FIPS203_H__
+#define __FIPS203_H__
+/*
+  Minimalist ML-KEM C interface
+  Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+
+  Memory allocation and tracking are entirely the job of the caller.
+
+  The shared object backing this interface has no internal state
+  between calls, and should be completely reentrant.
+
+  These functions return true on success, false on error.
+*/
+#include <stdint.h>
+
+typedef uint8_t ml_kem_err;
+
+const ml_kem_err ML_KEM_OK = 0;
+const ml_kem_err ML_KEM_NULL_PTR_ERROR = 1;
+const ml_kem_err ML_KEM_SERIALIZATION_ERROR = 2;
+const ml_kem_err ML_KEM_DESERIALIZATION_ERROR = 3;
+const ml_kem_err ML_KEM_KEYGEN_ERROR = 4;
+const ml_kem_err ML_KEM_ENCAPSULATION_ERROR = 5;
+const ml_kem_err ML_KEM_DECAPSULATION_ERROR = 6;
+
+
+typedef struct ml_kem_shared_secret {
+  uint8_t data[32];
+} ml_kem_shared_secret;
+
+
+typedef struct ml_kem_512_encaps_key {
+  uint8_t data[800];
+} ml_kem_512_encaps_key;
+
+typedef struct ml_kem_512_decaps_key {
+  uint8_t data[1632];
+} ml_kem_512_decaps_key;
+
+typedef struct ml_kem_512_ciphertext {
+  uint8_t data[768];
+} ml_kem_512_ciphertext;
+
+typedef struct ml_kem_768_encaps_key {
+  uint8_t data[1184];
+} ml_kem_768_encaps_key;
+
+typedef struct ml_kem_768_decaps_key {
+  uint8_t data[2400];
+} ml_kem_768_decaps_key;
+
+typedef struct ml_kem_768_ciphertext {
+  uint8_t data[1088];
+} ml_kem_768_ciphertext;
+
+typedef struct ml_kem_1024_encaps_key {
+  uint8_t data[1568];
+} ml_kem_1024_encaps_key;
+
+typedef struct ml_kem_1024_decaps_key {
+  uint8_t data[3168];
+} ml_kem_1024_decaps_key;
+
+typedef struct ml_kem_1024_ciphertext {
+  uint8_t data[1568];
+} ml_kem_1024_ciphertext;
+
+#ifdef  __cplusplus
+extern "C" {
+#endif
+
+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_encaps(const ml_kem_512_encaps_key *encaps,
+                             ml_kem_512_ciphertext *ciphertext_out,
+                             ml_kem_shared_secret *shared_secret_out);
+
+ml_kem_err ml_kem_512_decaps(const ml_kem_512_decaps_key *decaps,
+                             const ml_kem_512_ciphertext *ciphertext,
+                             ml_kem_shared_secret *shared_secret_out);
+
+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_encaps(const ml_kem_768_encaps_key *encaps,
+                             ml_kem_768_ciphertext *ciphertext_out,
+                             ml_kem_shared_secret *shared_secret_out);
+
+ml_kem_err ml_kem_768_decaps(const ml_kem_768_decaps_key *decaps,
+                             const ml_kem_768_ciphertext *ciphertext,
+                             ml_kem_shared_secret *shared_secret_out);
+
+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_encaps(const ml_kem_1024_encaps_key *encaps,
+                              ml_kem_1024_ciphertext *ciphertext_out,
+                              ml_kem_shared_secret *shared_secret_out);
+
+ml_kem_err ml_kem_1024_decaps(const ml_kem_1024_decaps_key *decaps,
+                              const ml_kem_1024_ciphertext *ciphertext,
+                              ml_kem_shared_secret *shared_secret_out);
+
+#ifdef  __cplusplus
+} // extern "C"
+#endif
+#endif // __FIPS203_H__
diff --git a/ffi/fips203.py b/ffi/fips203.py
new file mode 100644
index 0000000..fe7d56f
--- /dev/null
+++ b/ffi/fips203.py
@@ -0,0 +1,361 @@
+'''FIPS 203 (ML-KEM) Asymmetric Post-Quantum Cryptography
+
+This Python module provides an implementation of FIPS 203, the
+Module-Lattice-based Key Encapsulation Mechanism Standard.
+
+## Example
+
+The following example shows using the standard ML-KEM algorithm to
+produce identical 32-byte shared secrets:
+
+```
+from fips203 import ML_KEM_768
+
+(encapsulation_key, decapsulation_key) = ML_KEM_768.keygen()
+(ciphertext, shared_secret_1) = encapsulation_key.encaps()
+shared_secret_2 = decapsulation_key.decaps(ct)
+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
+initializing them with the appropriate size bytes object.
+
+A serialization example:
+
+```
+from fips203 import ML_KEM_768
+
+(ek,dk) = ML_KEM_768.keygen()
+with open('encapskey.bin', 'wb') as f:
+    f.write(bytes(ek))
+with open('decapskey.bin', 'wb') as f:
+    f.write(bytes(dk))
+```
+
+A deserialization example, followed by use:
+
+```
+import fips203
+
+with open('encapskey.bin', 'b') as f:
+    ekdata = f.read()
+
+ek = fips203.EncapsulationKey(ekdata)
+(ct, ss) = ek.Encaps()
+```
+
+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`:
+
+```
+from fips203 import ML_KEM_768
+
+print(f"ML-KEM-768 Ciphertext size (in bytes) is {ML_KEM_768.CT_SIZE}")
+```
+
+## Implementation Notes
+
+This is a wrapper around libfips203, built from the Rust fips203-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.
+
+## See Also
+
+- https://doi.org/10.6028/NIST.FIPS.203.ipd
+- https://github.com/integritychain/fips203
+
+'''
+
+__version__ = '0.1'
+__author__ = 'Daniel Kahn Gillmor <dkg@fifthhorseman.net>'
+__all__ = [
+    'ML_KEM_512',
+    'ML_KEM_768',
+    'ML_KEM_1024',
+    'Ciphertext',
+    'EncapsulationKey',
+    'DecapsulationKey',
+]
+
+import ctypes
+import ctypes.util
+import enum
+from typing import Tuple, Dict, Any, Union
+from abc import ABC
+
+
+class _SharedSecret(ctypes.Structure):
+    _fields_ = [('data', ctypes.c_uint8 * 32)]
+
+
+class Err(enum.IntEnum):
+    OK = 0
+    NULL_PTR_ERROR = 1
+    SERIALIZATION_ERROR = 2
+    DESERIALIZATION_ERROR = 3
+    KEYGEN_ERROR = 4
+    ENCAPSULATION_ERROR = 5
+    DECAPSULATION_ERROR = 6
+
+
+class Ciphertext():
+    '''ML-KEM Ciphertext
+
+    Serialize this object by asking for it as `bytes`.
+
+    You can convert it to a 32-byte shared secret by passing it to the
+    Decaps() function of the appropriate Decapsulation Key.
+
+    '''
+    def __init__(self, data: Union[bytes, int]) -> None:
+        '''Create ML-KEM Ciphertext from bytes (or strength level).'''
+        if isinstance(data, bytes):
+            self._strength = _ML_KEM.strength_from_length('CT_SIZE', len(data))
+        elif isinstance(data, int):
+            self._strength = data
+        else:
+            raise Exception("Initialize ML-KEM Ciphertext object with "
+                            f"bytes or a strength level, not {type(data)}")
+        self._ffi = _ML_KEM.strength(self._strength)
+        self._ct = self._ffi['Ciphertext']()
+
+        if isinstance(data, bytes):
+            self._set(data)
+
+    def __repr__(self) -> str:
+        return f'<ML-KEM-{self._strength} Ciphertext>'
+
+    def __bytes__(self) -> bytes:
+        return bytes(self._ct.data)
+
+    def _set(self, data: bytes) -> None:
+        if len(data) != len(self._ct.data):
+            raise ValueError(f"Expected {len(self._ct.data)} bytes, "
+                             f"got {len(data)}")
+        for i in range(len(data)):
+            self._ct.data[i] = data[i]
+
+
+class EncapsulationKey():
+    '''ML-KEM Encapsulation Key
+
+    Serialize this object by asking for it as `bytes`.
+
+    Produce a Ciphertext and a 32-byte shared secret by invoking
+    Encaps() on it.
+    '''
+    def __init__(self, data: Union[bytes, int]) -> None:
+        '''Create ML-KEM Encapsulation Key from bytes (or strength level).'''
+        if isinstance(data, bytes):
+            self._strength = _ML_KEM.strength_from_length('EK_SIZE', len(data))
+        elif isinstance(data, int):
+            self._strength = data
+        else:
+            raise Exception("Initialize ML-KEM Encapsulation Key with "
+                            f"bytes or a strength level, not {type(data)}")
+        self._ffi = _ML_KEM.strength(self._strength)
+        self._ek = self._ffi['EncapsKey']()
+        if isinstance(data, bytes):
+            self._set(data)
+
+    def __repr__(self) -> str:
+        return f'<ML-KEM-{self._strength} Encapsulation Key>'
+
+    def __bytes__(self) -> bytes:
+        return bytes(self._ek.data)
+
+    def _set(self, data: bytes) -> None:
+        if len(data) != len(self._ek.data):
+            raise ValueError(f"Expected {len(self._ek.data)} bytes, "
+                             f"got {len(data)}")
+        for i in range(len(data)):
+            self._ek.data[i] = data[i]
+
+    def encaps(self) -> Tuple[Ciphertext, bytes]:
+        '''Produce a new Ciphertext and corresponding 32-byte shared secret.'''
+        ct = Ciphertext(self._strength)
+        ss = _SharedSecret()
+        ret = Err(self._ffi['encaps'](ctypes.byref(self._ek),
+                                      ctypes.byref(ct._ct),
+                                      ctypes.byref(ss)))
+        if ret is not Err.OK:
+            raise Exception(f"ml_kem_{self._strength}_encaps() "
+                            f"returned {ret} ({ret.name})")
+        return (ct, bytes(ss.data))
+
+
+class DecapsulationKey():
+    '''ML-KEM Decapsulation Key
+
+    Serialize this object by asking for it as `bytes`.
+
+    Produce a 32-byte shared secret from a Ciphertext by invoking
+    Decaps() on it.
+    '''
+    def __init__(self, data: Union[bytes, int]) -> None:
+        '''Create ML-KEM Decapsulation Key from bytes (or strength level).'''
+        if isinstance(data, bytes):
+            self._strength = _ML_KEM.strength_from_length('DK_SIZE', len(data))
+        elif isinstance(data, int):
+            self._strength = data
+        else:
+            raise Exception("Initialize ML-KEM Encapsulation Key with bytes "
+                            f"or a strength level, not {type(data)}")
+        self._ffi = _ML_KEM.strength(self._strength)
+        self._dk = self._ffi['DecapsKey']()
+        if isinstance(data, bytes):
+            self._set(data)
+
+    def __repr__(self) -> str:
+        return f'<ML-KEM-{self._strength} Decapsulation Key>'
+
+    def __bytes__(self) -> bytes:
+        return bytes(self._dk.data)
+
+    def _set(self, data: bytes) -> None:
+        if len(data) != len(self._dk.data):
+            raise ValueError(f"Expected {len(self._dk.data)} bytes, "
+                             f"got {len(data)}")
+        for i in range(len(data)):
+            self._dk.data[i] = data[i]
+
+    def decaps(self, ct: Ciphertext) -> bytes:
+        '''Get 32-byte shared secret corresponding to the given Ciphertext.'''
+        if self._strength != ct._strength:
+            raise Exception(f"Cannot decapsulate {ct} with {self}")
+        ss = _SharedSecret()
+        ret = Err(self._ffi['decaps'](ctypes.byref(self._dk),
+                                      ctypes.byref(ct._ct),
+                                      ctypes.byref(ss)))
+        if ret is not Err.OK:
+            raise Exception(f"ml_kem_{self._strength}_decaps() "
+                            f"returned {ret} ({ret.name})")
+        return bytes(ss.data)
+
+
+class _ML_KEM():
+    params: Dict[int, Dict[str, int]] = {
+        512: {
+            'EK_SIZE': 800,
+            'DK_SIZE': 1632,
+            'CT_SIZE': 768,
+            },
+        768: {
+            'EK_SIZE': 1184,
+            'DK_SIZE': 2400,
+            'CT_SIZE': 1088,
+            },
+        1024: {
+            'EK_SIZE': 1568,
+            'DK_SIZE': 3168,
+            'CT_SIZE': 1568,
+            },
+        }
+    lib = ctypes.CDLL(ctypes.util.find_library('fips203'))
+
+    # use Any below because i don't know how to specify the type of the FuncPtr
+    ffi: Dict[int, Dict[str, Any]] = {}
+
+    @classmethod
+    def strength(cls, level: int) -> Dict[str, Any]:
+        if level not in cls.ffi:
+            class _EncapsKey(ctypes.Structure):
+                _fields_ = [('data', ctypes.c_uint8 *
+                             cls.params[level]['EK_SIZE'])]
+
+            class _DecapsKey(ctypes.Structure):
+                _fields_ = [('data', ctypes.c_uint8 *
+                             cls.params[level]['DK_SIZE'])]
+
+            class _Ciphertext(ctypes.Structure):
+                _fields_ = [('data', ctypes.c_uint8 *
+                             cls.params[level]['CT_SIZE'])]
+            ffi: Dict[str, Any] = {}
+
+            ffi['keygen'] = cls.lib[f'ml_kem_{level}_keygen']
+            ffi['keygen'].argtypes = [ctypes.POINTER(_EncapsKey),
+                                      ctypes.POINTER(_DecapsKey)]
+            ffi['keygen'].restype = ctypes.c_uint8
+
+            ffi['encaps'] = cls.lib[f'ml_kem_{level}_encaps']
+            ffi['encaps'].argtypes = [ctypes.POINTER(_EncapsKey),
+                                      ctypes.POINTER(_Ciphertext),
+                                      ctypes.POINTER(_SharedSecret)]
+            ffi['encaps'].restype = ctypes.c_uint8
+
+            ffi['decaps'] = cls.lib[f'ml_kem_{level}_decaps']
+            ffi['decaps'].argtypes = [ctypes.POINTER(_DecapsKey),
+                                      ctypes.POINTER(_Ciphertext),
+                                      ctypes.POINTER(_SharedSecret)]
+            ffi['decaps'].restype = ctypes.c_uint8
+
+            ffi['EncapsKey'] = _EncapsKey
+            ffi['DecapsKey'] = _DecapsKey
+            ffi['Ciphertext'] = _Ciphertext
+
+            cls.ffi[level] = ffi
+
+        return cls.ffi[level]
+
+    @classmethod
+    def strength_from_length(cls, object_type: str, object_len: int) -> int:
+        for strength in cls.params:
+            if cls.params[strength][object_type] == object_len:
+                return strength
+        raise Exception(f"No ML-KEM parameter set has {object_type} "
+                        f"of {object_len} bytes")
+
+    @classmethod
+    def _keygen(cls, strength: int) -> Tuple[EncapsulationKey,
+                                             DecapsulationKey]:
+        ek = EncapsulationKey(strength)
+        dk = DecapsulationKey(strength)
+
+        ret = Err(cls.strength(strength)['keygen'](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.'''
+
+    _strength: int
+    EK_SIZE: int
+    DK_SIZE: int
+    CT_SIZE: int
+    SS_SIZE: int = 32
+
+    @classmethod
+    def keygen(cls) -> Tuple[EncapsulationKey, DecapsulationKey]:
+        '''Generate a pair of Encapsulation and Decapsulation Keys.'''
+        return _ML_KEM._keygen(cls._strength)
+
+
+class ML_KEM_512(ML_KEM):
+    '''ML-KEM-512 (FIPS 203) Implementation.'''
+    _strength: int = 512
+    EK_SIZE: int = 800
+    DK_SIZE: int = 1632
+    CT_SIZE: int = 768
+
+
+class ML_KEM_768(ML_KEM):
+    '''ML-KEM-768 (FIPS 203) Implementation.'''
+    _strength: int = 768
+    EK_SIZE: int = 1184
+    DK_SIZE: int = 2400
+    CT_SIZE: int = 1088
+
+
+class ML_KEM_1024(ML_KEM):
+    '''ML-KEM-1024 (FIPS 203) Implementation.'''
+    _strength: int = 1024
+    EK_SIZE: int = 1568
+    DK_SIZE: int = 3168
+    CT_SIZE: int = 1568
diff --git a/ffi/src/.rustfmt.toml b/ffi/src/.rustfmt.toml
new file mode 100644
index 0000000..e0f52f0
--- /dev/null
+++ b/ffi/src/.rustfmt.toml
@@ -0,0 +1,1 @@
+fn_params_layout = "Vertical"
diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs
new file mode 100644
index 0000000..4cefca1
--- /dev/null
+++ b/ffi/src/lib.rs
@@ -0,0 +1,260 @@
+use fips203;
+
+#[repr(C)]
+pub struct ml_kem_shared_secret {
+    data: [u8; fips203::SSK_LEN],
+}
+
+pub const ML_KEM_OK: u8 = 0;
+pub const ML_KEM_NULL_PTR_ERROR: u8 = 1;
+pub const ML_KEM_SERIALIZATION_ERROR: u8 = 2;
+pub const ML_KEM_DESERIALIZATION_ERROR: u8 = 3;
+pub const ML_KEM_KEYGEN_ERROR: u8 = 4;
+pub const ML_KEM_ENCAPSULATION_ERROR: u8 = 5;
+pub const ML_KEM_DECAPSULATION_ERROR: u8 = 6;
+
+// ML-KEM-512
+
+#[repr(C)]
+pub struct ml_kem_512_encaps_key {
+    data: [u8; fips203::ml_kem_512::EK_LEN],
+}
+#[repr(C)]
+pub struct ml_kem_512_decaps_key {
+    data: [u8; fips203::ml_kem_512::DK_LEN],
+}
+#[repr(C)]
+pub struct ml_kem_512_ciphertext {
+    data: [u8; fips203::ml_kem_512::CT_LEN],
+}
+
+#[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>,
+) -> u8 {
+    use fips203::traits::{KeyGen, SerDes};
+
+    let (Some(encaps_out), Some(decaps_out)) = (encaps_out, decaps_out) else  {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok((ek, dk)) = fips203::ml_kem_512::KG::try_keygen_vt() else {
+        return ML_KEM_KEYGEN_ERROR;
+    };
+
+    encaps_out.data = ek.into_bytes();
+    decaps_out.data = dk.into_bytes();
+    return 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>,
+    shared_secret_out: Option<&mut ml_kem_shared_secret>,
+) -> u8 {
+    use fips203::traits::{Encaps, SerDes};
+
+    let (Some(encaps), Some(ciphertext_out), Some(shared_secret_out)) = (encaps, ciphertext_out, shared_secret_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok(ek) = fips203::ml_kem_512::EncapsKey::try_from_bytes(encaps.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok((ssk, ct)) = ek.try_encaps_vt() else {
+        return ML_KEM_ENCAPSULATION_ERROR;
+    };
+
+    shared_secret_out.data = ssk.into_bytes();
+    ciphertext_out.data = ct.into_bytes();
+    return ML_KEM_OK;
+}
+
+#[no_mangle]
+pub extern "C" fn ml_kem_512_decaps(
+    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};
+
+    let (Some(decaps), Some(ciphertext), Some(shared_secret_out)) = (decaps, ciphertext, shared_secret_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok(dk) = fips203::ml_kem_512::DecapsKey::try_from_bytes(decaps.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok(ct) = fips203::ml_kem_512::CipherText::try_from_bytes(ciphertext.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok(ssk) = dk.try_decaps_vt(&ct) else {
+        return ML_KEM_DECAPSULATION_ERROR;
+    };
+
+    shared_secret_out.data = ssk.into_bytes();
+    return ML_KEM_OK;
+}
+
+// ML-KEM-768
+
+#[repr(C)]
+pub struct ml_kem_768_encaps_key {
+    data: [u8; fips203::ml_kem_768::EK_LEN],
+}
+#[repr(C)]
+pub struct ml_kem_768_decaps_key {
+    data: [u8; fips203::ml_kem_768::DK_LEN],
+}
+#[repr(C)]
+pub struct ml_kem_768_ciphertext {
+    data: [u8; fips203::ml_kem_768::CT_LEN],
+}
+
+#[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>,
+) -> u8 {
+    use fips203::traits::{KeyGen, SerDes};
+
+    let (Some(encaps_out), Some(decaps_out)) = (encaps_out, decaps_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok((ek, dk)) = fips203::ml_kem_768::KG::try_keygen_vt() else {
+        return ML_KEM_KEYGEN_ERROR;
+    };
+
+    encaps_out.data = ek.into_bytes();
+    decaps_out.data = dk.into_bytes();
+    return 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>,
+    shared_secret_out: Option<&mut ml_kem_shared_secret>,
+) -> u8 {
+    use fips203::traits::{Encaps, SerDes};
+
+    let (Some(encaps), Some(ciphertext_out), Some(shared_secret_out)) = (encaps, ciphertext_out, shared_secret_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok(ek) = fips203::ml_kem_768::EncapsKey::try_from_bytes(encaps.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok((ssk, ct)) = ek.try_encaps_vt() else {
+        return ML_KEM_ENCAPSULATION_ERROR;
+    };
+
+    shared_secret_out.data = ssk.into_bytes();
+    ciphertext_out.data = ct.into_bytes();
+    return ML_KEM_OK;
+}
+
+#[no_mangle]
+pub extern "C" fn ml_kem_768_decaps(
+    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};
+
+    let (Some(decaps), Some(ciphertext), Some(shared_secret_out)) = (decaps, ciphertext, shared_secret_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok(dk) = fips203::ml_kem_768::DecapsKey::try_from_bytes(decaps.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok(ct) = fips203::ml_kem_768::CipherText::try_from_bytes(ciphertext.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok(ssk) = dk.try_decaps_vt(&ct) else {
+        return ML_KEM_DECAPSULATION_ERROR;
+    };
+
+    shared_secret_out.data = ssk.into_bytes();
+    return ML_KEM_OK;
+}
+
+// ML-KEM-1024
+
+#[repr(C)]
+pub struct ml_kem_1024_encaps_key {
+    data: [u8; fips203::ml_kem_1024::EK_LEN],
+}
+#[repr(C)]
+pub struct ml_kem_1024_decaps_key {
+    data: [u8; fips203::ml_kem_1024::DK_LEN],
+}
+#[repr(C)]
+pub struct ml_kem_1024_ciphertext {
+    data: [u8; fips203::ml_kem_1024::CT_LEN],
+}
+
+#[no_mangle]
+pub extern "C" fn ml_kem_1024_keygen(
+    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)) = (encaps_out, decaps_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok((ek, dk)) = fips203::ml_kem_1024::KG::try_keygen_vt() else {
+        return ML_KEM_KEYGEN_ERROR;
+    };
+
+    encaps_out.data = ek.into_bytes();
+    decaps_out.data = dk.into_bytes();
+    return 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>,
+    shared_secret_out: Option<&mut ml_kem_shared_secret>,
+) -> u8 {
+    use fips203::traits::{Encaps, SerDes};
+
+    let (Some(encaps), Some(ciphertext_out), Some(shared_secret_out)) = (encaps, ciphertext_out, shared_secret_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok(ek) = fips203::ml_kem_1024::EncapsKey::try_from_bytes(encaps.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok((ssk, ct)) = ek.try_encaps_vt() else {
+        return ML_KEM_ENCAPSULATION_ERROR;
+    };
+
+    shared_secret_out.data = ssk.into_bytes();
+    ciphertext_out.data = ct.into_bytes();
+    return ML_KEM_OK;
+}
+
+#[no_mangle]
+pub extern "C" fn ml_kem_1024_decaps(
+    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};
+
+    let (Some(decaps), Some(ciphertext), Some(shared_secret_out)) = (decaps, ciphertext, shared_secret_out) else {
+        return ML_KEM_NULL_PTR_ERROR;
+    };
+    let Ok(dk) = fips203::ml_kem_1024::DecapsKey::try_from_bytes(decaps.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok(ct) = fips203::ml_kem_1024::CipherText::try_from_bytes(ciphertext.data) else {
+        return ML_KEM_DESERIALIZATION_ERROR;
+    };
+    let Ok(ssk) = dk.try_decaps_vt(&ct) else {
+        return ML_KEM_DECAPSULATION_ERROR;
+    };
+
+    shared_secret_out.data = ssk.into_bytes();
+    return ML_KEM_OK;
+}


Test patch:
diff --git a/ffi/tests/Makefile b/ffi/tests/Makefile
new file mode 100644
index 0000000..4170665
--- /dev/null
+++ b/ffi/tests/Makefile
@@ -0,0 +1,19 @@
+SO_LOCATION = ../../target/debug
+SIZES = 512 768 1024
+FRAMES = encaps_key decaps_key ciphertext encaps decaps keygen
+
+BASELINES=$(foreach sz, $(SIZES), baseline-$(sz))
+CHECKS=$(foreach sz, $(SIZES), runtest-$(sz))
+
+check: $(CHECKS)
+
+runtest-%: baseline-%
+	LD_LIBRARY_PATH=$(SO_LOCATION) ./$<
+
+baseline-%: baseline.c ../fips203.h
+	$(CC) -o $@ -g -D MLKEM_size=$* $(foreach v, $(FRAMES),-D MLKEM_$(v)=ml_kem_$*_$(v)) -Werror -Wall -pedantic -L $(SO_LOCATION) $< -Wall -lfips203
+
+clean:
+	rm -f $(BASELINES)
+
+.PHONY: clean check
diff --git a/ffi/tests/baseline.c b/ffi/tests/baseline.c
new file mode 100644
index 0000000..9b87e8a
--- /dev/null
+++ b/ffi/tests/baseline.c
@@ -0,0 +1,114 @@
+#include <stdio.h>
+#include "../fips203.h"
+
+int main(int argc, const char **argv) {
+  MLKEM_encaps_key encaps;
+  MLKEM_decaps_key decaps;
+  MLKEM_ciphertext ct;
+  ml_kem_shared_secret ssk_a;
+  ml_kem_shared_secret ssk_b;
+  ml_kem_err err;
+  MLKEM_encaps_key encaps_weird;
+  MLKEM_decaps_key decaps_weird;
+
+  if (MLKEM_keygen (&encaps, &decaps))
+      return 1;
+
+  printf("Encaps (%d): ", MLKEM_size);
+  for (int n = 0; n < sizeof(encaps.data); n++)
+    printf ("%02x ", encaps.data[n]);
+  printf("\n");
+  
+  printf("Decaps (%d): ", MLKEM_size);
+  for (int n = 0; n < sizeof(decaps.data); n++)
+    printf ("%02x ", decaps.data[n]);
+  printf("\n");
+
+  if (MLKEM_encaps (&encaps, &ct, &ssk_a))
+    return 2;
+
+  printf("Ciphertext (%d): ", MLKEM_size);
+  for (int n = 0; n < sizeof(ct.data); n++)
+    printf ("%02x ", ct.data[n]);
+  printf("\n");
+
+  printf("Shared Secret A: ");
+  for (int n = 0; n < sizeof(ssk_a.data); n++)
+    printf ("%02x ", ssk_a.data[n]);
+  printf("\n");
+
+  if (MLKEM_decaps (&decaps, &ct, &ssk_b))
+    return 3;
+
+  printf("Shared Secret B: ");
+  for (int n = 0; n < sizeof(ssk_b.data); n++)
+    printf ("%02x ", ssk_b.data[n]);
+  printf("\n");
+
+  if (! MLKEM_keygen (&encaps, NULL)) {
+    fprintf (stderr, "keygen should have failed with NULL decaps\n");
+    return 1;
+  }
+  if (! MLKEM_keygen (NULL, &decaps)) {
+    fprintf (stderr, "keygen should have failed with NULL encaps\n");
+    return 1;
+  }
+  if (! MLKEM_keygen (NULL, NULL)) {
+    fprintf (stderr, "keygen should have failed with NULL encaps and decaps\n");
+    return 1;
+  }
+
+
+  if (! MLKEM_encaps (&encaps, &ct, NULL)) {
+    fprintf (stderr, "encaps should have failed with NULL shared_secret_out\n");
+    return 1;
+  }
+  if (! MLKEM_encaps (&encaps, NULL, &ssk_a)) {
+    fprintf (stderr, "encaps should have failed with NULL ciphertext_out\n");
+    return 1;
+  }
+  if (! MLKEM_encaps (NULL, &ct, &ssk_a)) {
+    fprintf (stderr, "encaps should have failed with NULL encaps_key\n");
+    return 1;
+  }
+  if (! MLKEM_encaps (NULL, NULL, NULL)) {
+    fprintf (stderr, "encaps should have failed with NULL arguments\n");
+    return 1;
+  }
+
+
+  if (! MLKEM_decaps (&decaps, &ct, NULL)) {
+    fprintf (stderr, "decaps should have failed with NULL shared_secret_out\n");
+    return 1;
+  }
+  if (! MLKEM_decaps (&decaps, NULL, &ssk_b)) {
+    fprintf (stderr, "decaps should have failed with NULL ciphertext\n");
+    return 1;
+  }
+  if (! MLKEM_decaps (NULL, &ct, &ssk_b)) {
+    fprintf (stderr, "decaps should have failed with NULL decaps_key\n");
+    return 1;
+  }
+  if (! MLKEM_decaps (NULL, NULL, NULL)) {
+    fprintf (stderr, "decaps should have failed with NULL arguments\n");
+    return 1;
+  }
+
+  for (int i = 0; i < sizeof(encaps_weird.data); i++)
+    encaps_weird.data[i] = 0xff;
+  err = MLKEM_encaps (&encaps_weird, &ct, &ssk_a);
+  if (err != ML_KEM_DESERIALIZATION_ERROR) {
+    fprintf (stderr, "encaps against an encaps_key of all 0xff octets should have failed with deserialization error, got %d\n", err);
+    return 1;
+  }
+
+  for (int i = 0; i < sizeof(decaps_weird.data); i++)
+    decaps_weird.data[i] = 0xff;
+  err = MLKEM_decaps (&decaps_weird, &ct, &ssk_a);
+  if (err != ML_KEM_DECAPSULATION_ERROR) {
+    fprintf (stderr, "decaps against a tampered decaps_key should have failed with decapsulation error, got %d\n", err);
+    return 1;
+  } 
+  
+  return 0;
+}