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:
Hanzo AI
2025-12-28 04:11:19 -08:00
parent e8f6a648da
commit 5575988f35
10 changed files with 754 additions and 0 deletions
+8
View File
@@ -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"
+2
View File
@@ -18,6 +18,8 @@ members = [
"lux-crypto-keccak",
"lux-crypto-secp256k1",
"lux-crypto-ed25519",
"lux-crypto-blake3",
"lux-crypto-slhdsa",
]
[workspace.package]
+17
View File
@@ -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
+35
View File
@@ -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++");
}
}
+120
View File
@@ -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);
}
+17
View File
@@ -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
+35
View File
@@ -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++");
}
}
+193
View File
@@ -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)),
}
}
+131
View File
@@ -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());
}