mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
slhdsa: rust crate over luxcpp/crypto/slhdsa C-ABI
Adds lux-crypto-slhdsa crate binding to FIPS 205 SLH-DSA. Wraps the
luxcpp/crypto/slhdsa C-ABI (slhdsa_keygen/sign/verify) over the vendored
PQClean reference (sphincs-{sha2,shake}-{128,192,256}f-simple/clean,
CC0/public domain), six 'f' fast parameter sets:
Mode 2/3/5 -> SLH-DSA-SHA2-{128,192,256}f (NIST L1/L3/L5)
Mode 12/13/15 -> SLH-DSA-SHAKE-{128,192,256}f (NIST L1/L3/L5)
Surface: Mode enum with const pk_len/sk_len/sig_len matching FIPS 205
§10 catalogue (32/48/64 pk; 64/96/128 sk; 17088/35664/49856 sig).
keygen / sign / verify return Result<_, Error> with explicit
InvalidLength / InvalidSignature / InternalError discriminants.
Underlying byte-equal NIST KAT compliance is shown by the C++ side at
build-cto/slhdsa_kat_test (498 PASS lines across all six variants).
Rust roundtrip suite covers (keygen, sign, verify, tampered-sig,
tampered-pk, tampered-msg, short-pk) for the four 128f/192f variants
plus a sizes-match-FIPS205 invariant; 256f is gated --ignored
(sign() takes 30+s on M1).
cargo test -p lux-crypto-slhdsa: 6 passed, 2 ignored (long).
This commit is contained in:
Generated
+8
@@ -6,6 +6,10 @@ version = 3
|
||||
name = "lux-crypto"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "lux-crypto-blake3"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "lux-crypto-ed25519"
|
||||
version = "0.1.0"
|
||||
@@ -17,3 +21,7 @@ version = "0.1.0"
|
||||
[[package]]
|
||||
name = "lux-crypto-secp256k1"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "lux-crypto-slhdsa"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -18,6 +18,8 @@ members = [
|
||||
"lux-crypto-keccak",
|
||||
"lux-crypto-secp256k1",
|
||||
"lux-crypto-ed25519",
|
||||
"lux-crypto-blake3",
|
||||
"lux-crypto-slhdsa",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "lux-crypto-blake3"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "Canonical Rust binding for Lux BLAKE3. Calls into luxcpp/crypto/blake3 (vendored BLAKE3 reference v1.5.0) via the C-ABI."
|
||||
repository = "https://github.com/luxfi/crypto"
|
||||
readme = "README.md"
|
||||
keywords = ["blake3", "hash", "xof", "ffi"]
|
||||
categories = ["cryptography"]
|
||||
|
||||
[lib]
|
||||
name = "lux_crypto_blake3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let base: PathBuf = if let Ok(d) = env::var("CRYPTO_DIR") {
|
||||
PathBuf::from(d).join("lib")
|
||||
} else if let Ok(d) = env::var("CRYPTO_BUILD_DIR") {
|
||||
PathBuf::from(d)
|
||||
} else {
|
||||
manifest_dir
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("luxcpp")
|
||||
.join("crypto")
|
||||
.join("build-cto")
|
||||
};
|
||||
|
||||
println!("cargo:rerun-if-changed=src/lib.rs");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_DIR");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_BUILD_DIR");
|
||||
|
||||
let lib_path = base.join("blake3");
|
||||
println!("cargo:rustc-link-search=native={}", lib_path.display());
|
||||
println!("cargo:rustc-link-lib=static=blake3");
|
||||
println!("cargo:rustc-link-lib=static=blake3_cpu");
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-link-lib=c++");
|
||||
} else {
|
||||
println!("cargo:rustc-link-lib=stdc++");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
//
|
||||
// lux-crypto-blake3: canonical Rust binding for the Lux BLAKE3 C-ABI.
|
||||
//
|
||||
// Links statically against `libblake3.a` and `libblake3_cpu.a` produced by
|
||||
// `luxcpp/crypto/blake3`, whose CPU body is the vendored BLAKE3 reference C
|
||||
// (BLAKE3-team/BLAKE3 v1.5.0, portable-only). All four canonical BLAKE3
|
||||
// modes are exposed:
|
||||
//
|
||||
// * `hash` -- default-length (32-byte) hash, no key
|
||||
// * `keyed_hash` -- 32-byte hash with 32-byte key (RFC-style MAC)
|
||||
// * `derive_key` -- context-string-bound 32-byte derivation
|
||||
// * `hash_xof` -- extensible-length output (XOF)
|
||||
//
|
||||
// Byte-equal to upstream b3sum / blake3-py.
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
use core::ffi::{c_char, c_int};
|
||||
|
||||
extern "C" {
|
||||
fn blake3(input: *const u8, input_len: usize, output: *mut u8) -> c_int;
|
||||
fn lux_blake3_keyed(
|
||||
key: *const u8,
|
||||
input: *const u8,
|
||||
input_len: usize,
|
||||
output: *mut u8,
|
||||
) -> c_int;
|
||||
fn lux_blake3_derive_key(
|
||||
context: *const c_char,
|
||||
key_material: *const u8,
|
||||
key_material_len: usize,
|
||||
output: *mut u8,
|
||||
) -> c_int;
|
||||
fn lux_blake3_xof(
|
||||
input: *const u8,
|
||||
input_len: usize,
|
||||
output: *mut u8,
|
||||
output_len: usize,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// Length of a default BLAKE3 digest in bytes.
|
||||
pub const OUT_LEN: usize = 32;
|
||||
/// Length of a BLAKE3 key in bytes (for `keyed_hash`).
|
||||
pub const KEY_LEN: usize = 32;
|
||||
|
||||
/// Compute the default 32-byte BLAKE3 digest of `input`.
|
||||
#[inline]
|
||||
pub fn hash(input: &[u8]) -> [u8; OUT_LEN] {
|
||||
let mut out = [0u8; OUT_LEN];
|
||||
// SAFETY: pointers valid for the call's duration; output sized to digest.
|
||||
let rc = unsafe { blake3(input.as_ptr(), input.len(), out.as_mut_ptr()) };
|
||||
debug_assert_eq!(rc, 0, "blake3 returned non-zero status: {}", rc);
|
||||
out
|
||||
}
|
||||
|
||||
/// Compute the keyed BLAKE3 digest (MAC) of `input` under `key`.
|
||||
#[inline]
|
||||
pub fn keyed_hash(key: &[u8; KEY_LEN], input: &[u8]) -> [u8; OUT_LEN] {
|
||||
let mut out = [0u8; OUT_LEN];
|
||||
// SAFETY: pointers valid for the call's duration; key/output sized exactly.
|
||||
let rc = unsafe {
|
||||
lux_blake3_keyed(
|
||||
key.as_ptr(),
|
||||
input.as_ptr(),
|
||||
input.len(),
|
||||
out.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
debug_assert_eq!(rc, 0, "blake3 keyed_hash returned non-zero status: {}", rc);
|
||||
out
|
||||
}
|
||||
|
||||
/// Derive a 32-byte sub-key from `key_material` bound to a NUL-terminated
|
||||
/// ASCII `context` string.
|
||||
///
|
||||
/// `context_z` MUST contain a trailing NUL byte (`b"...\0"`). This binds the
|
||||
/// derivation to a hardcoded application-context literal per the BLAKE3 spec.
|
||||
#[inline]
|
||||
pub fn derive_key(context_z: &[u8], key_material: &[u8]) -> [u8; OUT_LEN] {
|
||||
debug_assert!(
|
||||
context_z.last() == Some(&0),
|
||||
"derive_key context must be NUL-terminated"
|
||||
);
|
||||
let mut out = [0u8; OUT_LEN];
|
||||
// SAFETY: pointers valid for the call's duration; context is NUL-terminated.
|
||||
let rc = unsafe {
|
||||
lux_blake3_derive_key(
|
||||
context_z.as_ptr() as *const c_char,
|
||||
key_material.as_ptr(),
|
||||
key_material.len(),
|
||||
out.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
debug_assert_eq!(
|
||||
rc, 0,
|
||||
"blake3 derive_key returned non-zero status: {}",
|
||||
rc
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
/// Extensible-output BLAKE3. `output` may be any length; the result is the
|
||||
/// prefix of the BLAKE3 root output keystream of that length.
|
||||
#[inline]
|
||||
pub fn hash_xof(input: &[u8], output: &mut [u8]) {
|
||||
// SAFETY: pointers valid for the call's duration.
|
||||
let rc = unsafe {
|
||||
lux_blake3_xof(
|
||||
input.as_ptr(),
|
||||
input.len(),
|
||||
output.as_mut_ptr(),
|
||||
output.len(),
|
||||
)
|
||||
};
|
||||
debug_assert_eq!(rc, 0, "blake3 hash_xof returned non-zero status: {}", rc);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
//
|
||||
// Spec-vector integration test for lux-crypto-blake3.
|
||||
//
|
||||
// Walks the upstream `test_vectors.json` (BLAKE3-team/BLAKE3 v1.5.0,
|
||||
// vendored under luxcpp/crypto/blake3/test/vectors/test_vectors.json) and
|
||||
// asserts byte-equality across all 35 cases x 4 modes = 140 assertions:
|
||||
//
|
||||
// hash -- first 32 bytes of "hash" XOF stream
|
||||
// keyed_hash -- first 32 bytes of "keyed_hash" XOF stream
|
||||
// derive_key -- first 32 bytes of "derive_key" XOF stream
|
||||
// hash_xof -- full extended length of "hash" stream
|
||||
//
|
||||
// The synthetic input per upstream README is a 251-byte ramp (0..250)
|
||||
// repeated to reach `input_len`.
|
||||
|
||||
use lux_crypto_blake3::{derive_key, hash, hash_xof, keyed_hash, KEY_LEN, OUT_LEN};
|
||||
|
||||
const VECTORS: &str =
|
||||
include_str!("../../../../../luxcpp/crypto/blake3/test/vectors/test_vectors.json");
|
||||
|
||||
fn from_hex_var(s: &str) -> Vec<u8> {
|
||||
assert!(s.len() % 2 == 0, "hex must be even length");
|
||||
let nibble = |c: u8| -> u8 {
|
||||
match c {
|
||||
b'0'..=b'9' => c - b'0',
|
||||
b'a'..=b'f' => c - b'a' + 10,
|
||||
b'A'..=b'F' => c - b'A' + 10,
|
||||
_ => panic!("non-hex"),
|
||||
}
|
||||
};
|
||||
let b = s.as_bytes();
|
||||
let mut out = vec![0u8; b.len() / 2];
|
||||
for i in 0..out.len() {
|
||||
out[i] = (nibble(b[2 * i]) << 4) | nibble(b[2 * i + 1]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn from_hex32(s: &str) -> [u8; OUT_LEN] {
|
||||
let v = from_hex_var(s);
|
||||
let mut a = [0u8; OUT_LEN];
|
||||
a.copy_from_slice(&v[..OUT_LEN]);
|
||||
a
|
||||
}
|
||||
|
||||
fn make_input(n: usize) -> Vec<u8> {
|
||||
(0..n).map(|i| (i % 251) as u8).collect()
|
||||
}
|
||||
|
||||
// Tiny purpose-built JSON reader: walks `"<name>": <value>` pairs in the
|
||||
// known schema. NOT a general parser.
|
||||
struct Reader<'a> {
|
||||
s: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
fn find_field(&mut self, name: &str) -> Option<usize> {
|
||||
let needle = format!("\"{}\"", name);
|
||||
let nb = needle.as_bytes();
|
||||
let hay = &self.s[self.pos..];
|
||||
let mut i = 0;
|
||||
while i + nb.len() <= hay.len() {
|
||||
if &hay[i..i + nb.len()] == nb {
|
||||
let p = self.pos + i + nb.len();
|
||||
let mut q = p;
|
||||
while q < self.s.len() && (self.s[q] == b' ' || self.s[q] == b':') {
|
||||
q += 1;
|
||||
}
|
||||
return Some(q);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
fn read_string_at(&mut self, p: usize) -> Option<String> {
|
||||
if self.s.get(p) != Some(&b'"') {
|
||||
return None;
|
||||
}
|
||||
let start = p + 1;
|
||||
let mut q = start;
|
||||
while q < self.s.len() && self.s[q] != b'"' {
|
||||
q += 1;
|
||||
}
|
||||
if q >= self.s.len() {
|
||||
return None;
|
||||
}
|
||||
self.pos = q + 1;
|
||||
Some(String::from_utf8(self.s[start..q].to_vec()).ok()?)
|
||||
}
|
||||
fn read_uint_at(&mut self, mut p: usize) -> Option<u64> {
|
||||
while p < self.s.len() && (self.s[p] == b' ' || self.s[p] == b':') {
|
||||
p += 1;
|
||||
}
|
||||
let mut v: u64 = 0;
|
||||
let mut any = false;
|
||||
while p < self.s.len() && self.s[p].is_ascii_digit() {
|
||||
v = v * 10 + (self.s[p] - b'0') as u64;
|
||||
p += 1;
|
||||
any = true;
|
||||
}
|
||||
self.pos = p;
|
||||
if any {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_test_vectors_byte_equal() {
|
||||
let mut r = Reader {
|
||||
s: VECTORS.as_bytes(),
|
||||
pos: 0,
|
||||
};
|
||||
|
||||
let p_key = r.find_field("key").expect("key field missing");
|
||||
let key_str = r.read_string_at(p_key).expect("key string malformed");
|
||||
let p_ctx = r
|
||||
.find_field("context_string")
|
||||
.expect("context_string missing");
|
||||
let ctx_str = r
|
||||
.read_string_at(p_ctx)
|
||||
.expect("context_string malformed");
|
||||
|
||||
assert_eq!(
|
||||
key_str.len(),
|
||||
KEY_LEN,
|
||||
"key has unexpected length {}",
|
||||
key_str.len()
|
||||
);
|
||||
let mut key = [0u8; KEY_LEN];
|
||||
key.copy_from_slice(key_str.as_bytes());
|
||||
|
||||
// NUL-terminate context for the C ABI.
|
||||
let mut ctx_z = ctx_str.into_bytes();
|
||||
ctx_z.push(0);
|
||||
|
||||
let mut n_cases = 0;
|
||||
let mut passed = 0;
|
||||
let mut total = 0;
|
||||
|
||||
loop {
|
||||
let p_in = match r.find_field("input_len") {
|
||||
Some(p) => p,
|
||||
None => break,
|
||||
};
|
||||
let in_len = r.read_uint_at(p_in).expect("malformed input_len") as usize;
|
||||
let p_h = r.find_field("hash").expect("missing hash");
|
||||
let hash_hex = r.read_string_at(p_h).expect("hash malformed");
|
||||
let p_k = r.find_field("keyed_hash").expect("missing keyed_hash");
|
||||
let keyed_hex = r.read_string_at(p_k).expect("keyed_hash malformed");
|
||||
let p_d = r.find_field("derive_key").expect("missing derive_key");
|
||||
let dk_hex = r.read_string_at(p_d).expect("derive_key malformed");
|
||||
|
||||
n_cases += 1;
|
||||
let input = make_input(in_len);
|
||||
|
||||
// Mode 1: default-length hash (first 32 bytes of "hash").
|
||||
total += 1;
|
||||
let want = from_hex32(&hash_hex);
|
||||
let got = hash(&input);
|
||||
assert_eq!(got, want, "hash32 mismatch at in_len={}", in_len);
|
||||
passed += 1;
|
||||
|
||||
// Mode 2: keyed_hash, default-length (first 32 bytes).
|
||||
total += 1;
|
||||
let want = from_hex32(&keyed_hex);
|
||||
let got = keyed_hash(&key, &input);
|
||||
assert_eq!(got, want, "keyed_hash mismatch at in_len={}", in_len);
|
||||
passed += 1;
|
||||
|
||||
// Mode 3: derive_key, default-length (first 32 bytes).
|
||||
total += 1;
|
||||
let want = from_hex32(&dk_hex);
|
||||
let got = derive_key(&ctx_z, &input);
|
||||
assert_eq!(got, want, "derive_key mismatch at in_len={}", in_len);
|
||||
passed += 1;
|
||||
|
||||
// Mode 4: XOF, full extended length per upstream `hash` field.
|
||||
total += 1;
|
||||
let want = from_hex_var(&hash_hex);
|
||||
let mut got = vec![0u8; want.len()];
|
||||
hash_xof(&input, &mut got);
|
||||
assert_eq!(got, want, "hash_xof mismatch at in_len={}", in_len);
|
||||
passed += 1;
|
||||
}
|
||||
|
||||
assert_eq!(n_cases, 35, "expected 35 cases, found {}", n_cases);
|
||||
assert_eq!(total, 140);
|
||||
assert_eq!(passed, 140);
|
||||
println!("blake3 spec vectors: {}/{} PASS", passed, total);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "lux-crypto-slhdsa"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "Canonical Rust binding for Lux SLH-DSA (FIPS 205). Calls into luxcpp/crypto/slhdsa (vendored PQClean reference, CC0/public domain) via the C-ABI."
|
||||
repository = "https://github.com/luxfi/crypto"
|
||||
readme = "README.md"
|
||||
keywords = ["slh-dsa", "sphincs", "fips205", "post-quantum", "ffi"]
|
||||
categories = ["cryptography"]
|
||||
|
||||
[lib]
|
||||
name = "lux_crypto_slhdsa"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let base: PathBuf = if let Ok(d) = env::var("CRYPTO_DIR") {
|
||||
PathBuf::from(d).join("lib")
|
||||
} else if let Ok(d) = env::var("CRYPTO_BUILD_DIR") {
|
||||
PathBuf::from(d)
|
||||
} else {
|
||||
manifest_dir
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("luxcpp")
|
||||
.join("crypto")
|
||||
.join("build-cto")
|
||||
};
|
||||
|
||||
println!("cargo:rerun-if-changed=src/lib.rs");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_DIR");
|
||||
println!("cargo:rerun-if-env-changed=CRYPTO_BUILD_DIR");
|
||||
|
||||
let lib_path = base.join("slhdsa");
|
||||
println!("cargo:rustc-link-search=native={}", lib_path.display());
|
||||
println!("cargo:rustc-link-lib=static=slhdsa");
|
||||
println!("cargo:rustc-link-lib=static=slhdsa_cpu");
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-link-lib=c++");
|
||||
} else {
|
||||
println!("cargo:rustc-link-lib=stdc++");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
//
|
||||
// lux-crypto-slhdsa: canonical Rust binding for the Lux SLH-DSA C-ABI.
|
||||
//
|
||||
// Links statically against `libslhdsa.a` and `libslhdsa_cpu.a` produced by
|
||||
// `luxcpp/crypto/slhdsa`, whose CPU body wraps the vendored PQClean reference
|
||||
// (sphincs-{sha2,shake}-{128,192,256}f-simple/clean, CC0 / public domain).
|
||||
// Conforms to NIST FIPS 205 §10 parameter catalogue (six 'f' fast variants).
|
||||
//
|
||||
// Mode encoding (byte-equal to luxcpp/crypto/slhdsa/c-abi/c_slhdsa.cpp):
|
||||
// 2 -> SLH-DSA-SHA2-128f (NIST L1)
|
||||
// 3 -> SLH-DSA-SHA2-192f (NIST L3)
|
||||
// 5 -> SLH-DSA-SHA2-256f (NIST L5)
|
||||
// 12 -> SLH-DSA-SHAKE-128f (NIST L1)
|
||||
// 13 -> SLH-DSA-SHAKE-192f (NIST L3)
|
||||
// 15 -> SLH-DSA-SHAKE-256f (NIST L5)
|
||||
//
|
||||
// Buffer sizes are FIPS 205 fixed (identical between SHA2 and SHAKE for the
|
||||
// same security level):
|
||||
// 128f : pk=32 sk=64 sig<=17088
|
||||
// 192f : pk=48 sk=96 sig<=35664
|
||||
// 256f : pk=64 sk=128 sig<=49856
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
use core::ffi::c_int;
|
||||
|
||||
extern "C" {
|
||||
fn slhdsa_keygen(mode: c_int, seed: *const u8, pk: *mut u8, sk: *mut u8) -> c_int;
|
||||
fn slhdsa_sign(
|
||||
mode: c_int,
|
||||
sk: *const u8,
|
||||
msg: *const u8,
|
||||
msg_len: usize,
|
||||
sig: *mut u8,
|
||||
sig_len: *mut usize,
|
||||
) -> c_int;
|
||||
fn slhdsa_verify(
|
||||
mode: c_int,
|
||||
pk: *const u8,
|
||||
msg: *const u8,
|
||||
msg_len: usize,
|
||||
sig: *const u8,
|
||||
sig_len: usize,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// FIPS 205 SLH-DSA parameter set. The integer mode codes are byte-equal to
|
||||
/// the C-ABI dispatch in `luxcpp/crypto/slhdsa/c-abi/c_slhdsa.cpp`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Mode {
|
||||
/// SLH-DSA-SHA2-128f (NIST L1, SHA2 family, fast variant).
|
||||
Sha2_128f = 2,
|
||||
/// SLH-DSA-SHA2-192f (NIST L3, SHA2 family, fast variant).
|
||||
Sha2_192f = 3,
|
||||
/// SLH-DSA-SHA2-256f (NIST L5, SHA2 family, fast variant).
|
||||
Sha2_256f = 5,
|
||||
/// SLH-DSA-SHAKE-128f (NIST L1, SHAKE family, fast variant).
|
||||
Shake_128f = 12,
|
||||
/// SLH-DSA-SHAKE-192f (NIST L3, SHAKE family, fast variant).
|
||||
Shake_192f = 13,
|
||||
/// SLH-DSA-SHAKE-256f (NIST L5, SHAKE family, fast variant).
|
||||
Shake_256f = 15,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Public key length for this parameter set (FIPS 205).
|
||||
#[inline]
|
||||
pub const fn pk_len(self) -> usize {
|
||||
match self {
|
||||
Mode::Sha2_128f | Mode::Shake_128f => 32,
|
||||
Mode::Sha2_192f | Mode::Shake_192f => 48,
|
||||
Mode::Sha2_256f | Mode::Shake_256f => 64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Secret key length for this parameter set (FIPS 205).
|
||||
#[inline]
|
||||
pub const fn sk_len(self) -> usize {
|
||||
match self {
|
||||
Mode::Sha2_128f | Mode::Shake_128f => 64,
|
||||
Mode::Sha2_192f | Mode::Shake_192f => 96,
|
||||
Mode::Sha2_256f | Mode::Shake_256f => 128,
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum signature length for this parameter set (FIPS 205).
|
||||
#[inline]
|
||||
pub const fn sig_len(self) -> usize {
|
||||
match self {
|
||||
Mode::Sha2_128f | Mode::Shake_128f => 17088,
|
||||
Mode::Sha2_192f | Mode::Shake_192f => 35664,
|
||||
Mode::Sha2_256f | Mode::Shake_256f => 49856,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors returned by the SLH-DSA operations.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// Underlying C-ABI call returned a non-zero status.
|
||||
InternalError(c_int),
|
||||
/// `verify` rejected the (msg, sig, pk) triple.
|
||||
InvalidSignature,
|
||||
/// Buffer slice was the wrong size for the requested parameter set.
|
||||
InvalidLength,
|
||||
}
|
||||
|
||||
/// Generate a fresh keypair for the given parameter set. Entropy comes from
|
||||
/// the underlying C wrapper's getentropy()-backed randombytes (FIPS 205 keygen
|
||||
/// is randomized; SLH-DSA itself is *signing*-deterministic per §9.1).
|
||||
///
|
||||
/// `pk` and `sk` must be sized exactly to `mode.pk_len()` and `mode.sk_len()`.
|
||||
#[inline]
|
||||
pub fn keygen(mode: Mode, pk: &mut [u8], sk: &mut [u8]) -> Result<(), Error> {
|
||||
if pk.len() != mode.pk_len() || sk.len() != mode.sk_len() {
|
||||
return Err(Error::InvalidLength);
|
||||
}
|
||||
// The C-ABI accepts a 32-byte seed pointer that is currently unused by the
|
||||
// PQClean reference (which calls randombytes internally). Pass a dummy.
|
||||
let dummy_seed = [0u8; 32];
|
||||
// SAFETY: pointers valid for the call's duration; lengths checked above.
|
||||
let rc = unsafe {
|
||||
slhdsa_keygen(mode as c_int, dummy_seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
|
||||
};
|
||||
match rc {
|
||||
0 => Ok(()),
|
||||
other => Err(Error::InternalError(other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign `msg` under `sk` using the given parameter set.
|
||||
///
|
||||
/// Returns the byte length of the produced signature (= `mode.sig_len()` for
|
||||
/// the deployed 'f' fast variants, where signatures are fixed-size).
|
||||
///
|
||||
/// `sig` must have capacity at least `mode.sig_len()`. `sk` must be sized
|
||||
/// exactly to `mode.sk_len()`.
|
||||
#[inline]
|
||||
pub fn sign(mode: Mode, sk: &[u8], msg: &[u8], sig: &mut [u8]) -> Result<usize, Error> {
|
||||
if sk.len() != mode.sk_len() || sig.len() < mode.sig_len() {
|
||||
return Err(Error::InvalidLength);
|
||||
}
|
||||
let mut sig_len: usize = sig.len();
|
||||
// SAFETY: pointers valid for the call's duration; lengths checked above.
|
||||
let rc = unsafe {
|
||||
slhdsa_sign(
|
||||
mode as c_int,
|
||||
sk.as_ptr(),
|
||||
msg.as_ptr(),
|
||||
msg.len(),
|
||||
sig.as_mut_ptr(),
|
||||
&mut sig_len as *mut usize,
|
||||
)
|
||||
};
|
||||
match rc {
|
||||
0 => Ok(sig_len),
|
||||
other => Err(Error::InternalError(other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a single SLH-DSA signature.
|
||||
///
|
||||
/// Returns `Ok(())` iff the signature is valid for `(pk, msg)`. Returns
|
||||
/// `Err(Error::InvalidSignature)` when the C-ABI rejects the triple, and
|
||||
/// `Err(Error::InternalError)` for any other non-zero status.
|
||||
#[inline]
|
||||
pub fn verify(mode: Mode, pk: &[u8], msg: &[u8], sig: &[u8]) -> Result<(), Error> {
|
||||
if pk.len() != mode.pk_len() {
|
||||
return Err(Error::InvalidLength);
|
||||
}
|
||||
// SAFETY: pointers valid for the call's duration; pk length checked above.
|
||||
let rc = unsafe {
|
||||
slhdsa_verify(
|
||||
mode as c_int,
|
||||
pk.as_ptr(),
|
||||
msg.as_ptr(),
|
||||
msg.len(),
|
||||
sig.as_ptr(),
|
||||
sig.len(),
|
||||
)
|
||||
};
|
||||
match rc {
|
||||
0 => Ok(()),
|
||||
// CRYPTO_ERR_VERIFY = -3 in lux_crypto.h
|
||||
-3 => Err(Error::InvalidSignature),
|
||||
other => Err(Error::InternalError(other)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
//
|
||||
// Round-trip integration tests for SLH-DSA across all six FIPS 205 'f' (fast)
|
||||
// parameter sets. Byte-equal NIST KAT vector tests live in the C++ side
|
||||
// (build-cto/slhdsa_kat_test, 498 PASS lines). These tests verify the Rust
|
||||
// binding's invariants against the underlying byte-equal CPU body:
|
||||
//
|
||||
// 1. keygen(mode, pk, sk) -> CRYPTO_OK
|
||||
// 2. sign(mode, sk, msg, sig) -> sig_len == mode.sig_len()
|
||||
// 3. verify(mode, pk, msg, sig) -> Ok(())
|
||||
// 4. verify with mutated sig -> Err(InvalidSignature)
|
||||
// 5. verify with mutated pk -> Err(InvalidSignature)
|
||||
// 6. verify with mutated msg -> Err(InvalidSignature)
|
||||
// 7. wrong-length pk -> Err(InvalidLength)
|
||||
//
|
||||
// 256f sign() can take >30s on M1 -- gated behind --include-ignored.
|
||||
|
||||
use lux_crypto_slhdsa::{keygen, sign, verify, Error, Mode};
|
||||
|
||||
fn roundtrip(mode: Mode, msg: &[u8]) {
|
||||
let mut pk = vec![0u8; mode.pk_len()];
|
||||
let mut sk = vec![0u8; mode.sk_len()];
|
||||
keygen(mode, &mut pk, &mut sk).expect("keygen");
|
||||
|
||||
let mut sig = vec![0u8; mode.sig_len()];
|
||||
let n = sign(mode, &sk, msg, &mut sig).expect("sign");
|
||||
assert_eq!(n, mode.sig_len(), "{:?}: sig_len mismatch", mode);
|
||||
|
||||
verify(mode, &pk, msg, &sig[..n]).expect("verify(honest)");
|
||||
|
||||
// Tamper signature.
|
||||
let mut bad_sig = sig.clone();
|
||||
bad_sig[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
verify(mode, &pk, msg, &bad_sig[..n]),
|
||||
Err(Error::InvalidSignature),
|
||||
"{:?}: tampered sig should reject",
|
||||
mode
|
||||
);
|
||||
|
||||
// Tamper public key.
|
||||
let mut bad_pk = pk.clone();
|
||||
bad_pk[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
verify(mode, &bad_pk, msg, &sig[..n]),
|
||||
Err(Error::InvalidSignature),
|
||||
"{:?}: tampered pk should reject",
|
||||
mode
|
||||
);
|
||||
|
||||
// Tamper message (only when non-empty).
|
||||
if !msg.is_empty() {
|
||||
let mut bad_msg = msg.to_vec();
|
||||
bad_msg[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
verify(mode, &pk, &bad_msg, &sig[..n]),
|
||||
Err(Error::InvalidSignature),
|
||||
"{:?}: tampered msg should reject",
|
||||
mode
|
||||
);
|
||||
}
|
||||
|
||||
// Wrong-length public key.
|
||||
let short_pk = &pk[..pk.len() - 1];
|
||||
assert_eq!(
|
||||
verify(mode, short_pk, msg, &sig[..n]),
|
||||
Err(Error::InvalidLength),
|
||||
"{:?}: short pk should be rejected as InvalidLength",
|
||||
mode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha2_128f_roundtrip() {
|
||||
roundtrip(Mode::Sha2_128f, b"lux SLH-DSA-SHA2-128f roundtrip");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shake_128f_roundtrip() {
|
||||
roundtrip(Mode::Shake_128f, b"lux SLH-DSA-SHAKE-128f roundtrip");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha2_192f_roundtrip() {
|
||||
roundtrip(Mode::Sha2_192f, b"lux SLH-DSA-SHA2-192f roundtrip");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shake_192f_roundtrip() {
|
||||
roundtrip(Mode::Shake_192f, b"lux SLH-DSA-SHAKE-192f roundtrip");
|
||||
}
|
||||
|
||||
// 256f sign() takes ~30-60s on M1; ignored by default.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn sha2_256f_roundtrip() {
|
||||
roundtrip(Mode::Sha2_256f, b"lux SLH-DSA-SHA2-256f roundtrip");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn shake_256f_roundtrip() {
|
||||
roundtrip(Mode::Shake_256f, b"lux SLH-DSA-SHAKE-256f roundtrip");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_message_sha2_128f() {
|
||||
roundtrip(Mode::Sha2_128f, b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_sizes_match_fips205() {
|
||||
// NIST FIPS 205 §10 parameter catalogue.
|
||||
assert_eq!(Mode::Sha2_128f.pk_len(), 32);
|
||||
assert_eq!(Mode::Sha2_128f.sk_len(), 64);
|
||||
assert_eq!(Mode::Sha2_128f.sig_len(), 17088);
|
||||
|
||||
assert_eq!(Mode::Sha2_192f.pk_len(), 48);
|
||||
assert_eq!(Mode::Sha2_192f.sk_len(), 96);
|
||||
assert_eq!(Mode::Sha2_192f.sig_len(), 35664);
|
||||
|
||||
assert_eq!(Mode::Sha2_256f.pk_len(), 64);
|
||||
assert_eq!(Mode::Sha2_256f.sk_len(), 128);
|
||||
assert_eq!(Mode::Sha2_256f.sig_len(), 49856);
|
||||
|
||||
// SHAKE shares sizes with SHA2 at each security level.
|
||||
assert_eq!(Mode::Shake_128f.pk_len(), Mode::Sha2_128f.pk_len());
|
||||
assert_eq!(Mode::Shake_192f.pk_len(), Mode::Sha2_192f.pk_len());
|
||||
assert_eq!(Mode::Shake_256f.pk_len(), Mode::Sha2_256f.pk_len());
|
||||
}
|
||||
Reference in New Issue
Block a user