hi Aaron,
Here are the revised instructions for Priority 3, updated with all the fixes we discovered.
This new plan is ordered to prevent the build conflicts we saw earlier. It isolates your library's test code before running the no_std fuzzing, which will solve the "no global memory allocator" errors.
Priority 3 (Revised): FFI, Interop, and Fuzzing
Step 1: Add FFI and Interoperability (The std Code)
First, we will add all the new std-dependent code and dependencies at once.
1.A: Update Cargo.toml
Add the [lib] section for the FFI, the libc dependency, and the [dev-dependencies] for interoperability testing.
Ini, TOML
[package]
name = "pqc-combo"
version = "0.0.4" # <-- Updated version
edition = "2021"
description = "Hardened FFI-compatible PQC Core (FIPS 203/204). no_std, no_alloc, alloc, std. Fuzz-tested."
license = "MIT"
keywords = ["kyber", "dilithium", "pqc", "no-std", "no-alloc", "ffi"]
categories = ["cryptography", "no-std", "embedded", "external-ffi-bindings"]
repository = "https://github.com/AaronSchnacky1/pqc-combo"
readme = "README.md"
authors = ["Aaron Schnacky <aaronschnacky@gmail.com>"]
homepage = "https://www.aaronschnacky.com/"
documentation = "https://github.com/AaronSchnacky1/pqc-combo/blob/main/README.md"
include = [
"src/**/*.rs",
"Cargo.toml",
"Cargo.lock",
"README.md",
"SECURITY.md",
"tests/integration.rs", # <-- Add this
"tests/test_ffi.py" # <-- Add this
]
# --- NEW: FFI Configuration ---
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
alloc = []
std = [
"pqcrypto-kyber/std",
"pqcrypto-dilithium/std",
"rand_core/std",
"zeroize/std",
"alloc", # std implies alloc
]
[dependencies]
pqcrypto-kyber = { version = "0.8.1", default-features = false }
pqcrypto-dilithium = { version = "0.5.0", default-features = false }
pqcrypto-traits = { version = "0.3.5", default-features = false }
zeroize = { version = "1.8.2", default-features = false, features = ["derive"] }
rand_core = { version = "0.6", default-features = false }
# --- NEW: FFI Dependencies ---
libc = { version = "0.2", default-features = false }
[dev-dependencies]
rand = "0.8"
hex = "0.4"
rand_chacha = "0.3"
rand_core = { version = "0.6", features = ["std"] }
# --- NEW: Interoperability Test Dependencies ---
kyber-rs = "0.7"
dilithium = "0.6" # <-- Corrected package name
1.B: Create the FFI Module (src/ffi.rs)
Create this new file. It contains the C-compatible API and all the trait imports needed to compile.
Rust
// In src/ffi.rs
#![cfg(feature = "std")] // Only compile this file when "std" is enabled
use super::*;
use libc::{c_uchar, size_t};
use std::slice;
// --- Import all necessary traits ---
use pqcrypto_traits::kem::{
Ciphertext as KemCiphertext, PublicKey as KemPublicKey, SecretKey as KemSecretKey,
SharedSecret as KemSharedSecret,
};
use pqcrypto_traits::sign::{
PublicKey as SignPublicKey, SecretKey as SignSecretKey, SignedMessage as SignSignedMessage,
};
/// C-compatible error codes
#[repr(C)]
pub enum PqcComboResult {
Success = 0,
ErrorNullPointer = -1,
ErrorBadKey = -2,
ErrorBadCiphertext = -3,
ErrorBadSignature = -4,
ErrorEncapFailed = -5,
ErrorDecapFailed = -6,
ErrorVerifyFailed = -7,
}
// --- Kyber FFI ---
#[no_mangle]
pub extern "C" fn pqc_combo_kyber_keypair(
pk_out: *mut c_uchar,
sk_out: *mut c_uchar,
) -> PqcComboResult {
if pk_out.is_null() || sk_out.is_null() {
return PqcComboResult::ErrorNullPointer;
}
let pk_slice = unsafe {
slice::from_raw_parts_mut(pk_out, pqcrypto_kyber::kyber1024::public_key_bytes())
};
let sk_slice = unsafe {
slice::from_raw_parts_mut(sk_out, pqcrypto_kyber::kyber1024::secret_key_bytes())
};
let keys = KyberKeys::generate_key_pair();
pk_slice.copy_from_slice(keys.pk.as_bytes());
sk_slice.copy_from_slice(keys.sk.as_bytes());
PqcComboResult::Success
}
#[no_mangle]
pub extern "C" fn pqc_combo_kyber_encapsulate(
ct_out: *mut c_uchar,
ss_out: *mut c_uchar,
pk_in: *const c_uchar,
) -> PqcComboResult {
if ct_out.is_null() || ss_out.is_null() || pk_in.is_null() {
return PqcComboResult::ErrorNullPointer;
}
let pk_slice = unsafe {
slice::from_raw_parts(pk_in, pqcrypto_kyber::kyber1024::public_key_bytes())
};
let ct_slice = unsafe {
slice::from_raw_parts_mut(ct_out, pqcrypto_kyber::kyber1024::ciphertext_bytes())
};
let ss_slice = unsafe {
slice::from_raw_parts_mut(ss_out, pqcrypto_kyber::kyber1024::shared_secret_bytes())
};
let pk = match KyberPublicKey::from_bytes(pk_slice) {
Ok(k) => k,
Err(_) => return PqcComboResult::ErrorBadKey,
};
let (ct, ss) = encapsulate_shared_secret(&pk);
ss_slice.copy_from_slice(ss.as_bytes());
ct_slice.copy_from_slice(ct.as_bytes());
PqcComboResult::Success
}
#[no_mangle]
pub extern "C" fn pqc_combo_kyber_decapsulate(
ss_out: *mut c_uchar,
ct_in: *const c_uchar,
sk_in: *const c_uchar,
) -> PqcComboResult {
if ss_out.is_null() || ct_in.is_null() || sk_in.is_null() {
return PqcComboResult::ErrorNullPointer;
}
let ct_slice = unsafe {
slice::from_raw_parts(ct_in, pqcrypto_kyber::kyber1024::ciphertext_bytes())
};
let sk_slice = unsafe {
slice::from_raw_parts(sk_in, pqcrypto_kyber::kyber1024::secret_key_bytes())
};
let ss_slice = unsafe {
slice::from_raw_parts_mut(ss_out, pqcrypto_kyber::kyber1024::shared_secret_bytes())
};
let ct = match KyberCiphertext::from_bytes(ct_slice) {
Ok(c) => c,
Err(_) => return PqcComboResult::ErrorBadCiphertext,
};
let sk = match KyberSecretKey::from_bytes(sk_slice) {
Ok(k) => k,
Err(_) => return PqcComboResult::ErrorBadKey,
};
let ss = decapsulate_shared_secret(&sk, &ct);
ss_slice.copy_from_slice(ss.as_bytes());
PqcComboResult::Success
}
// --- Dilithium FFI ---
#[no_mangle]
pub extern "C" fn pqc_combo_dilithium_keypair(
pk_out: *mut c_uchar,
sk_out: *mut c_uchar,
) -> PqcComboResult {
if pk_out.is_null() || sk_out.is_null() {
return PqcComboResult::ErrorNullPointer;
}
let pk_slice = unsafe {
slice::from_raw_parts_mut(pk_out, pqcrypto_dilithium::dilithium3::public_key_bytes())
};
let sk_slice = unsafe {
slice::from_raw_parts_mut(sk_out, pqcrypto_dilithium::dilithium3::secret_key_bytes())
};
let (pk, sk) = pqcrypto_dilithium::dilithium3::keypair();
pk_slice.copy_from_slice(pk.as_bytes());
sk_slice.copy_from_slice(sk.as_bytes());
PqcComboResult::Success
}
#[no_mangle]
pub extern "C" fn pqc_combo_dilithium_sign(
sig_out: *mut c_uchar,
msg_in: *const c_uchar,
msg_len: size_t,
sk_in: *const c_uchar,
) -> PqcComboResult {
if sig_out.is_null() || msg_in.is_null() || sk_in.is_null() {
return PqcComboResult::ErrorNullPointer;
}
let sig_slice = unsafe {
slice::from_raw_parts_mut(sig_out, pqcrypto_dilithium::dilithium3::signature_bytes())
};
let msg_slice = unsafe { slice::from_raw_parts(msg_in, msg_len as usize) };
let sk_slice = unsafe {
slice::from_raw_parts(sk_in, pqcrypto_dilithium::dilithium3::secret_key_bytes())
};
let sk = match DilithiumSecretKey::from_bytes(sk_slice) {
Ok(k) => k,
Err(_) => return PqcComboResult::ErrorBadKey,
};
let sig = sign_message(&sk, msg_slice);
sig_slice.copy_from_slice(sig.as_bytes());
PqcComboResult::Success
}
#[no_mangle]
pub extern "C" fn pqc_combo_dilithium_verify(
sig_in: *const c_uchar,
msg_in: *const c_uchar,
msg_len: size_t,
pk_in: *const c_uchar,
) -> PqcComboResult {
if sig_in.is_null() || msg_in.is_null() || pk_in.is_null() {
return PqcComboResult::ErrorNullPointer;
}
let sig_slice = unsafe {
slice::from_raw_parts(sig_in, pqcrypto_dilithium::dilithium3::signature_bytes())
};
let msg_slice = unsafe { slice::from_raw_parts(msg_in, msg_len as usize) };
let pk_slice = unsafe {
slice::from_raw_parts(pk_in, pqcrypto_dilithium::dilithium3::public_key_bytes())
};
let pk = match DilithiumPublicKey::from_bytes(pk_slice) {
Ok(k) => k,
Err(_) => return PqcComboResult::ErrorBadKey,
};
let sig = match DilithiumSignedMessage::from_bytes(sig_slice) {
Ok(s) => s,
Err(_) => return PqcComboResult::ErrorBadSignature,
};
if verify_signature(&pk, msg_slice, &sig) {
PqcComboResult::Success
} else {
PqcComboResult::ErrorVerifyFailed
}
}
Step 2: Isolate the Library Core (The Critical Fix)
This is the most important step to fix the fuzzing build errors. We will move your entire test suite out of src/lib.rs and into its own file.
2.A: Create tests/integration.rs
Create a new file at tests/integration.rs. Copy your entire #[cfg(test)] mod tests { ... } block into this file. Then, replace use super::* at the top with the correct imports.
Rust
// In tests/integration.rs
// (This file should be ~500 lines long and contain ALL your tests)
// FIX: Use the crate name `pqc_combo` instead of `super::*`
use pqc_combo::*;
use pqcrypto_dilithium::dilithium3::keypair as dilithium_keypair;
use pqcrypto_kyber::kyber1024;
// --- Imports for advanced tests ---
use pqcrypto_traits::kem::{
PublicKey as KemPublicKeyTrait, SecretKey as KemSecretKeyTrait,
Ciphertext as KemCiphertextTrait, SharedSecret as KemSharedSecretTrait
};
use pqcrypto_traits::sign::{
PublicKey as SignPublicKeyTrait, SecretKey as SignSecretKeyTrait,
SignedMessage as SignSignedMessageTrait
};
// Imports for alloc/std features
#[cfg(feature = "alloc")]
use alloc::{vec, vec::Vec, boxed::Box};
#[cfg(feature = "std")]
use std::{thread, sync::Arc};
// Imports for Priority 3 robustness tests
#[cfg(feature = "alloc")]
use rand_core::{RngCore, SeedableRng};
#[cfg(feature = "alloc")]
use rand_chacha::ChaCha8Rng;
// --- Original Tests (Grouped) ---
#[test]
fn test_kyber_kem_round_trip_success() {
let keys = KyberKeys::generate_key_pair();
let (ct, ss_a) = encapsulate_shared_secret(&keys.pk);
let ss_b = decapsulate_shared_secret(&keys.sk, &ct);
assert_eq!(ss_a.as_bytes(), ss_b.as_bytes());
}
// ... (Paste ALL your other tests here, from test_dilithium_sign_verify_success_and_wrong_msg
// all the way down to test_dilithium_interop_with_dilithium_rs) ...
// --- Make sure to include the interop tests: ---
#[test]
#[cfg(feature = "std")]
fn test_kyber_interop_with_kyber_rs() {
let mut rng = rand::thread_rng();
let keys = KyberKeys::generate_key_pair();
let pk_bytes = keys.pk.as_bytes();
let kyber_rs_pk = kyber_rs::PublicKey::from_bytes(pk_bytes).unwrap();
let (ct_other, ss_other) = kyber_rs::encapsulate(&kyber_rs_pk, &mut rng).unwrap();
let ct_from_other = KyberCiphertext::from_bytes(ct_other.as_bytes()).unwrap();
let ss_mine = decapsulate_shared_secret(&keys.sk, &ct_from_other);
assert_eq!(ss_mine.as_bytes(), ss_other.as_bytes());
}
#[test]
#[cfg(feature = "std")]
fn test_dilithium_interop_with_dilithium_rs() {
let (pk, sk) = dilithium_keypair();
let msg = b"test message for interoperability";
let sig_mine = sign_message(&sk, msg);
let dilithium_pk = dilithium::PublicKey::from_bytes(pk.as_bytes()).unwrap();
let verification_result = dilithium::verify(msg, sig_mine.as_bytes(), &dilithium_pk);
assert!(verification_result.is_ok(), "dilithium crate failed to verify signature");
}
2.B: Clean up src/lib.rs
Now, delete the entire #[cfg(test)] mod tests { ... } block from src/lib.rs. Your src/lib.rs file should be much shorter and contain no tests. It should look like this:
Rust
// In src/lib.rs
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(feature = "alloc", allow(unused_imports))]
#[cfg(feature = "alloc")]
extern crate alloc;
use pqcrypto_kyber::kyber1024::{
self,
PublicKey as KyberPublicKey,
SecretKey as KyberSecretKey,
Ciphertext as KyberCiphertext,
SharedSecret as KyberSharedSecret,
};
use pqcrypto_dilithium::dilithium3::{
sign,
open,
PublicKey as DilithiumPublicKey,
SecretKey as DilithiumSecretKey,
SignedMessage as DilithiumSignedMessage,
};
pub mod error;
pub use error::{PqcError, Result};
// FFI module, guarded by "std" feature
#[cfg(feature = "std")]
pub mod ffi;
pub struct KyberKeys {
pub pk: KyberPublicKey,
pub sk: KyberSecretKey,
}
impl KyberKeys {
pub fn generate_key_pair() -> Self {
let (pk, sk) = kyber1024::keypair();
KyberKeys { pk, sk }
}
}
pub fn encapsulate_shared_secret(
pk: &KyberPublicKey,
) -> (KyberCiphertext, KyberSharedSecret) {
let (ss, ct) = kyber1024::encapsulate(pk);
(ct, ss)
}
pub fn decapsulate_shared_secret(
sk: &KyberSecretKey,
ciphertext: &KyberCiphertext,
) -> KyberSharedSecret {
kyber1024::decapsulate(ciphertext, sk)
}
pub fn sign_message(sk: &DilithiumSecretKey, message: &[u8]) -> DilithiumSignedMessage {
sign(message, sk)
}
pub fn verify_signature(
pk: &DilithiumPublicKey,
expected_message: &[u8],
signed_message: &DilithiumSignedMessage,
) -> bool {
match open(signed_message, pk) {
Ok(extracted) => extracted == expected_message,
Err(_) => false,
}
}
Step 3: Fuzzing (The no_std Task)
Now that src/lib.rs is clean, fuzzing will build correctly.
3.A: Initialize Fuzzing
Run these commands in your project root:
Bash
cargo install cargo-fuzz
cargo fuzz init
3.B: Configure Fuzzing
Delete the placeholder fuzz/fuzz_targets/fuzz_target_1.rs. Then, create/overwrite fuzz/Cargo.toml with this:
Ini, TOML
# In fuzz/Cargo.toml
[package]
name = "pqc-combo-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
pqc-combo = { path = ".." } # Link to your library
# Add dev-dependencies to get the byte constants
[dev-dependencies]
pqcrypto-kyber = "0.8.1"
pqcrypto-dilithium = "0.5.0"
# --- Fuzz Targets ---
[[bin]]
name = "fuzz_decapsulate"
path = "fuzz_targets/fuzz_decapsulate.rs"
test = false
doc = false
[[bin]]
name = "fuzz_verify"
path = "fuzz_targets/fuzz_verify.rs"
test = false
doc = false
[[bin]]
name = "fuzz_deserializers"
path = "fuzz_targets/fuzz_deserializers.rs"
test = false
doc = false
3.C: Create Fuzz Targets
Create these three files in fuzz/fuzz_targets/:
Rust
// In fuzz/fuzz_targets/fuzz_decapsulate.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use pqc_combo::{KyberSecretKey, KyberCiphertext, decapsulate_shared_secret};
use pqcrypto_kyber::kyber1024; // Use dev-dependency for constants
fuzz_target!(|data: &[u8]| {
const SK_LEN: usize = kyber1024::secret_key_bytes();
const CT_LEN: usize = kyber1024::ciphertext_bytes();
if data.len() < SK_LEN + CT_LEN { return; }
let (sk_bytes, ct_bytes) = data.split_at(SK_LEN);
let ct_bytes = &ct_bytes[..CT_LEN];
if let Ok(sk) = KyberSecretKey::from_bytes(sk_bytes) {
if let Ok(ct) = KyberCiphertext::from_bytes(ct_bytes) {
let _ss = decapsulate_shared_secret(&sk, &ct);
}
}
});
Rust
// In fuzz/fuzz_targets/fuzz_verify.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use pqc_combo::{DilithiumPublicKey, DilithiumSignedMessage, verify_signature};
use pqcrypto_dilithium::dilithium3; // Use dev-dependency for constants
fuzz_target!(|data: &[u8]| {
const PK_LEN: usize = dilithium3::public_key_bytes();
const SIG_LEN: usize = dilithium3::signature_bytes();
if data.len() < PK_LEN + SIG_LEN { return; }
let (pk_bytes, rest) = data.split_at(PK_LEN);
let (sig_bytes, msg) = rest.split_at(SIG_LEN);
if let Ok(pk) = DilithiumPublicKey::from_bytes(pk_bytes) {
if let Ok(sig) = DilithiumSignedMessage::from_bytes(sig_bytes) {
let _ = verify_signature(&pk, msg, &sig);
}
}
});
Rust
// In fuzz/fuzz_targets/fuzz_deserializers.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use pqc_combo::*;
fuzz_target!(|data: &[u8]| {
let _ = KyberPublicKey::from_bytes(data);
let _ = KyberSecretKey::from_bytes(data);
let _ = KyberCiphertext::from_bytes(data);
let _ = DilithiumPublicKey::from_bytes(data);
let _ = DilithiumSecretKey::from_bytes(data);
let _ = DilithiumSignedMessage::from_bytes(data);
});
3.D: Create the FFI Python Test
Create this file at tests/test_ffi.py:
Python
# In tests/test_ffi.py
import ctypes
import os
# Define constants from your library
KYBER_PK_LEN = 1568
KYBER_SK_LEN = 3168
KYBER_CT_LEN = 1568
KYBER_SS_LEN = 32
DILITHIUM_PK_LEN = 1952
DILITHIUM_SK_LEN = 4016
DILITHIUM_SIG_LEN = 3293
# --- Load the library ---
lib_path = "./target/release/libpqc_combo.so" # Linux/macOS
if not os.path.exists(lib_path):
lib_path = "./target/release/pqc_combo.dll" # Windows
assert os.path.exists(lib_path), f"Library not found at {os.path.abspath(lib_path)}. Run 'cargo build --release --features std' first."
lib = ctypes.CDLL(lib_path)
# --- Define FFI function signatures ---
lib.pqc_combo_kyber_keypair.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
lib.pqc_combo_kyber_keypair.restype = ctypes.c_int
# ... (add other function signatures)
print("--- pqc-combo FFI Test Suite ---")
# --- Test 1: Kyber Round-trip ---
print("Testing Kyber KEM...")
pk_k = (ctypes.c_ubyte * KYBER_PK_LEN)()
sk_k = (ctypes.c_ubyte * KYBER_SK_LEN)()
ct = (ctypes.c_ubyte * KYBER_CT_LEN)()
ss1 = (ctypes.c_ubyte * KYBER_SS_LEN)()
ss2 = (ctypes.c_ubyte * KYBER_SS_LEN)()
res = lib.pqc_combo_kyber_keypair(pk_k, sk_k)
assert res == 0, f"Kyber keygen failed with code {res}"
res = lib.pqc_combo_kyber_encapsulate(ct, ss1, pk_k)
assert res == 0, f"Kyber encapsulate failed with code {res}"
res = lib.pqc_combo_kyber_decapsulate(ss2, ct, sk_k)
assert res == 0, f"Kyber decapsulate failed with code {res}"
assert bytes(ss1) == bytes(ss2), "Kyber shared secrets do not match!"
print("...Kyber OK")
# --- Test 2: Dilithium Round-trip ---
print("Testing Dilithium Signature...")
pk_d = (ctypes.c_ubyte * DILITHIUM_PK_LEN)()
sk_d = (ctypes.c_ubyte * DILITHIUM_SK_LEN)()
sig = (ctypes.c_ubyte * DILITHIUM_SIG_LEN)()
msg = b"This is the message to sign"
msg_len = len(msg)
res = lib.pqc_combo_dilithium_keypair(pk_d, sk_d)
assert res == 0, f"Dilithium keygen failed with code {res}"
res = lib.pqc_combo_dilithium_sign(sig, msg, msg_len, sk_d)
assert res == 0, f"Dilithium sign failed with code {res}"
res = lib.pqc_combo_dilithium_verify(sig, msg, msg_len, pk_d)
assert res == 0, f"Dilithium verify failed with code {res}"
# Test failure case
print("Testing Dilithium invalid signature...")
invalid_msg = b"This is the wrong message"
res_fail = lib.pqc_combo_dilithium_verify(sig, invalid_msg, len(invalid_msg), pk_d)
assert res_fail != 0, "Dilithium verify succeeded with wrong message!"
print("...Dilithium OK")
print("\nFFI Tests Passed!")
Switch to Nightly:
rustup default nightly
Run Fuzzers (for a few minutes each):
cargo fuzz run fuzz_decapsulate
cargo fuzz run fuzz_verify
cargo fuzz run fuzz_deserializers
Run Integration & Interop Tests:
cargo test --features std
Run FFI Tests:
cargo build --release --features std
python3 tests/test_ffi.py