rust: extend lux-crypto Rust surface with mldsa/mlkem/slhdsa/ed25519/keccak256

Adds Rust modules wrapping the canonical luxcpp/crypto C-ABI for:
  - ML-DSA (FIPS 204) modes 2/3/5
  - ML-KEM (FIPS 203) modes 2/3/5
  - SLH-DSA (FIPS 205) modes 2/3/5
  - Ed25519 keygen/sign/verify
  - keccak256 single-shot

Each module exposes safe Rust wrappers with size validation and a typed
CryptoStatus enum from the unified lux_crypto.h return-code contract.

Brand-neutral surface (no LUX_ prefix on Rust constants); link_name
attributes map to the existing lux_<alg>_<op> archive symbols until the
next luxcpp rebuild flips the archives to brand-neutral.

build.rs links per-algorithm libcpp_cpu.a archives from the
build-canonical/<alg>/ subdirectories.
This commit is contained in:
Hanzo AI
2025-12-27 21:14:43 -08:00
parent 15a447b682
commit 74d1f329d1
3 changed files with 513 additions and 26 deletions
+11 -6
View File
@@ -8,21 +8,26 @@ description = "Canonical Rust crate for Lux crypto. Calls into luxcpp/crypto via
# This crate is the canonical Rust entry point for Lux crypto.
# Mirrors github.com/luxfi/gpu (canonical Rust binding to luxfi/accel).
#
# Phase 1 surface:
# * secp256k1: secp256k1_ecrecover, secp256k1_ecrecover_batch
# Surface:
# * secp256k1 — ECDSA public-key recovery (single + batch)
# * mldsa — FIPS 204 ML-DSA modes 2/3/5
# * mlkem — FIPS 203 ML-KEM modes 2/3/5
# * slhdsa — FIPS 205 SLH-DSA modes 2/3/5
# * ed25519 — keygen / sign / verify
# * keccak256 — single-shot 32-byte digest
#
# Build expectations:
# - Environment variable `CRYPTO_DIR` points to the install prefix that
# contains `include/lux/crypto/*.h` and `lib/lib<alg>_cpu.a`.
# - Or, set `CRYPTO_BUILD_DIR` to the cmake build directory.
# - Or, set `CRYPTO_BUILD_DIR` to the cmake build directory containing
# `<alg>/lib<alg>_cpu.a`.
# - Deprecated names `LUX_CRYPTO_DIR` / `LUX_CRYPTO_BUILD_DIR` are read for
# one release with a deprecation warning.
#
# Brand-neutral naming: NO -sys suffix. We author the C/C++ ourselves at
# luxcpp/crypto; this is not a wrapper around third-party FFI. One crate
# per first-party FFI surface; safe wrappers (if needed) live as a `pub mod safe`.
# per first-party FFI surface; safe wrappers live in the per-algorithm
# `pub mod` blocks in src/lib.rs.
[lib]
name = "lux_crypto"
[build-dependencies]
+29 -9
View File
@@ -1,8 +1,11 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// Build script for lux-crypto. Locates the luxcpp/crypto build output
// and emits cargo linker directives.
// Build script for lux-crypto. Locates the luxcpp/crypto build outputs and
// emits cargo linker directives for every algorithm exposed by the crate.
//
// One compiled artifact per algorithm; we accept the `<alg>/lib<alg>_cpu.a`
// layout that `luxcpp/crypto/build-canonical/` produces by default.
use std::env;
use std::path::PathBuf;
@@ -18,20 +21,34 @@ fn read_env_with_legacy(new_name: &str, legacy_name: &str) -> Option<String> {
None
}
/// Each first-party algorithm corresponds to:
/// - a build subdirectory `build-canonical/<dir>/`
/// - a static archive named `lib<lib>_cpu.a`
const ALGS: &[(&str, &str)] = &[
("secp256k1", "secp256k1"),
("mldsa", "mldsa"),
("mlkem", "mlkem"),
("slhdsa", "slhdsa"),
("ed25519", "ed25519"),
("keccak", "keccak"),
];
fn main() {
let crypto_dir = read_env_with_legacy("CRYPTO_DIR", "LUX_CRYPTO_DIR");
let build_dir = read_env_with_legacy("CRYPTO_BUILD_DIR", "LUX_CRYPTO_BUILD_DIR");
let lib_path: PathBuf = if let Some(d) = crypto_dir {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let base: PathBuf = if let Some(d) = crypto_dir {
PathBuf::from(d).join("lib")
} else if let Some(d) = build_dir {
PathBuf::from(d).join("secp256k1")
PathBuf::from(d)
} else {
// Default: assume luxcpp/crypto is a sibling at ../../../luxcpp/crypto.
PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
// Default: assume luxcpp/crypto is a sibling at ../../../../luxcpp/crypto.
manifest_dir
.join("..").join("..").join("..")
.join("..").join("luxcpp").join("crypto")
.join("build-canonical").join("secp256k1")
.join("build-canonical")
};
println!("cargo:rerun-if-changed=src/lib.rs");
@@ -40,8 +57,11 @@ fn main() {
println!("cargo:rerun-if-env-changed=LUX_CRYPTO_DIR");
println!("cargo:rerun-if-env-changed=LUX_CRYPTO_BUILD_DIR");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=secp256k1_cpu");
for (subdir, lib) in ALGS {
let lib_path = base.join(subdir);
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static={}_cpu", lib);
}
// C++ runtime
if cfg!(target_os = "macos") {
+473 -11
View File
@@ -3,16 +3,28 @@
//
// Raw FFI bindings to luxcpp/crypto.
//
// Phase 1 surface: secp256k1 ECDSA public-key recovery.
// Surface (Phase 1 + brand-neutral rename):
// - secp256k1: ECDSA public-key recovery (single + batch)
// - mldsa: FIPS 204 ML-DSA (Dilithium) modes 2/3/5
// - mlkem: FIPS 203 ML-KEM (Kyber) modes 2/3/5
// - slhdsa: FIPS 205 SLH-DSA (SPHINCS+) modes 2/3/5
// - ed25519: Ed25519 keygen / sign / verify
// - keccak256: single-shot keccak256 (32-byte digest)
//
// All multi-byte buffers are big-endian. The C ABI is documented in
// `luxcpp/crypto/include/lux/crypto/secp256k1.h`. Symbols are brand-neutral.
// All multi-byte buffers are big-endian unless noted. The C ABI is documented
// in `luxcpp/crypto/c-abi/lux_crypto.h` and the per-algorithm headers under
// `luxcpp/crypto/include/lux/crypto/*.h`.
//
// Symbols are brand-neutral. Rust constants do not carry the LUX_ prefix.
#![no_std]
#![allow(non_camel_case_types)]
use core::ffi::c_int;
// ---------------------------------------------------------------------------
// secp256k1
// ---------------------------------------------------------------------------
/// Status codes returned by the secp256k1 C ABI.
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -45,9 +57,15 @@ impl Secp256k1Status {
}
}
// The canonical luxcpp/crypto C header declares brand-neutral symbols
// (`secp256k1_ecrecover`, `mldsa_*`, etc.). The current static archives at
// `build-canonical/<alg>/lib<alg>_cpu.a` still export those symbols with a
// `lux_` prefix until the next rebuild. We map both via `#[link_name]` so the
// Rust call surface stays brand-neutral and consumers see one canonical name.
extern "C" {
/// Recover the 64-byte uncompressed public key from (hash, r, s, v).
/// Returns one of the `Secp256k1Status` codes as `c_int`.
#[link_name = "lux_secp256k1_ecrecover"]
pub fn secp256k1_ecrecover(
hash: *const u8,
r: *const u8,
@@ -58,6 +76,7 @@ extern "C" {
/// Batch ecrecover. inputs is n * 97 bytes (hash || r || s || v).
/// pubkey_out is n * 64 bytes; status_out is n bytes.
#[link_name = "lux_secp256k1_ecrecover_batch"]
pub fn secp256k1_ecrecover_batch(
inputs: *const u8,
n: usize,
@@ -66,20 +85,463 @@ extern "C" {
) -> c_int;
}
// ---------------------------------------------------------------------------
// PQC mode (NIST level)
// ---------------------------------------------------------------------------
/// NIST security level: 2 = ML-KEM-512 / Dilithium2 / SLH-128s,
/// 3 = ML-KEM-768 / Dilithium3 / SLH-192s,
/// 5 = ML-KEM-1024 / Dilithium5 / SLH-256s.
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NistMode {
Mode2 = 2,
Mode3 = 3,
Mode5 = 5,
}
/// Generic status returned by the unified C ABI (lux_crypto.h).
///
/// 0 => CRYPTO_OK
/// 1 => verify success (boolean ops); 0 is invalid signature
/// <0 => failure (negated errno-ish)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CryptoStatus {
Ok,
VerifyOk,
BadInput,
BadLength,
VerifyFail,
Backend,
NotImpl,
Internal,
Unknown(c_int),
}
impl CryptoStatus {
pub fn from_int(v: c_int) -> Self {
match v {
0 => Self::Ok,
1 => Self::VerifyOk,
-1 => Self::BadInput,
-2 => Self::BadLength,
-3 => Self::VerifyFail,
-4 => Self::Backend,
-5 => Self::NotImpl,
-6 => Self::Internal,
other => Self::Unknown(other),
}
}
pub fn is_ok(self) -> bool {
matches!(self, Self::Ok | Self::VerifyOk)
}
}
// ---------------------------------------------------------------------------
// ML-DSA (FIPS 204)
// ---------------------------------------------------------------------------
pub mod mldsa {
use super::{c_int, CryptoStatus, NistMode};
extern "C" {
#[link_name = "lux_mldsa_keygen"]
fn mldsa_keygen(
mode: c_int,
seed: *const u8,
pk: *mut u8,
sk: *mut u8,
) -> c_int;
#[link_name = "lux_mldsa_sign"]
fn mldsa_sign(
mode: c_int,
sk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *mut u8,
sig_len: *mut usize,
) -> c_int;
#[link_name = "lux_mldsa_verify"]
fn mldsa_verify(
mode: c_int,
pk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *const u8,
sig_len: usize,
) -> c_int;
}
/// FIPS 204 sizes per NIST level (public key, secret key, signature).
pub fn sizes(mode: NistMode) -> (usize, usize, usize) {
match mode {
NistMode::Mode2 => (1312, 2560, 2420),
NistMode::Mode3 => (1952, 4032, 3309),
NistMode::Mode5 => (2592, 4896, 4627),
}
}
/// Generate a key pair from a 32-byte seed. Returns `(pk, sk)`.
pub fn keygen(mode: NistMode, seed: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>), CryptoStatus> {
let (pk_len, sk_len, _) = sizes(mode);
let mut pk = vec![0u8; pk_len];
let mut sk = vec![0u8; sk_len];
let rc = unsafe {
mldsa_keygen(mode as c_int, seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
};
let st = CryptoStatus::from_int(rc);
if st.is_ok() { Ok((pk, sk)) } else { Err(st) }
}
/// Sign a message with the given secret key. Returns the signature.
pub fn sign(mode: NistMode, sk: &[u8], msg: &[u8]) -> Result<Vec<u8>, CryptoStatus> {
let (_, sk_expect, sig_max) = sizes(mode);
if sk.len() != sk_expect {
return Err(CryptoStatus::BadLength);
}
let mut sig = vec![0u8; sig_max];
let mut sig_len: usize = sig_max;
let rc = unsafe {
mldsa_sign(
mode as c_int,
sk.as_ptr(),
msg.as_ptr(),
msg.len(),
sig.as_mut_ptr(),
&mut sig_len as *mut usize,
)
};
let st = CryptoStatus::from_int(rc);
if !st.is_ok() {
return Err(st);
}
sig.truncate(sig_len);
Ok(sig)
}
/// Verify a signature. Returns true on valid, false otherwise.
/// I/O errors (bad length, etc.) are mapped to `false` to match the
/// existing Rust crypto convention.
pub fn verify(mode: NistMode, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
let (pk_expect, _, _) = sizes(mode);
if pk.len() != pk_expect {
return false;
}
let rc = unsafe {
mldsa_verify(
mode as c_int,
pk.as_ptr(),
msg.as_ptr(),
msg.len(),
sig.as_ptr(),
sig.len(),
)
};
// C ABI: 1 = valid, 0 = invalid, <0 = error. Map invalid + error to false.
rc == 1
}
}
// ---------------------------------------------------------------------------
// ML-KEM (FIPS 203)
// ---------------------------------------------------------------------------
pub mod mlkem {
use super::{c_int, CryptoStatus, NistMode};
extern "C" {
#[link_name = "lux_mlkem_keygen"]
fn mlkem_keygen(
mode: c_int,
seed: *const u8,
pk: *mut u8,
sk: *mut u8,
) -> c_int;
#[link_name = "lux_mlkem_encap"]
fn mlkem_encap(
mode: c_int,
pk: *const u8,
ct: *mut u8,
ss: *mut u8,
) -> c_int;
#[link_name = "lux_mlkem_decap"]
fn mlkem_decap(
mode: c_int,
sk: *const u8,
ct: *const u8,
ss: *mut u8,
) -> c_int;
}
/// FIPS 203 sizes per NIST level (public key, secret key, ciphertext).
/// Shared secret is always 32 bytes.
pub fn sizes(mode: NistMode) -> (usize, usize, usize) {
match mode {
NistMode::Mode2 => (800, 1632, 768),
NistMode::Mode3 => (1184, 2400, 1088),
NistMode::Mode5 => (1568, 3168, 1568),
}
}
/// Generate a key pair from a 32-byte seed.
pub fn keygen(mode: NistMode, seed: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>), CryptoStatus> {
let (pk_len, sk_len, _) = sizes(mode);
let mut pk = vec![0u8; pk_len];
let mut sk = vec![0u8; sk_len];
let rc = unsafe {
mlkem_keygen(mode as c_int, seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
};
let st = CryptoStatus::from_int(rc);
if st.is_ok() { Ok((pk, sk)) } else { Err(st) }
}
/// Encapsulate against a public key. Returns `(ciphertext, shared_secret)`.
pub fn encap(mode: NistMode, pk: &[u8]) -> Result<(Vec<u8>, [u8; 32]), CryptoStatus> {
let (pk_expect, _, ct_len) = sizes(mode);
if pk.len() != pk_expect {
return Err(CryptoStatus::BadLength);
}
let mut ct = vec![0u8; ct_len];
let mut ss = [0u8; 32];
let rc = unsafe {
mlkem_encap(mode as c_int, pk.as_ptr(), ct.as_mut_ptr(), ss.as_mut_ptr())
};
let st = CryptoStatus::from_int(rc);
if st.is_ok() { Ok((ct, ss)) } else { Err(st) }
}
/// Decapsulate a ciphertext with the secret key.
pub fn decap(mode: NistMode, sk: &[u8], ct: &[u8]) -> Result<[u8; 32], CryptoStatus> {
let (_, sk_expect, ct_expect) = sizes(mode);
if sk.len() != sk_expect || ct.len() != ct_expect {
return Err(CryptoStatus::BadLength);
}
let mut ss = [0u8; 32];
let rc = unsafe {
mlkem_decap(mode as c_int, sk.as_ptr(), ct.as_ptr(), ss.as_mut_ptr())
};
let st = CryptoStatus::from_int(rc);
if st.is_ok() { Ok(ss) } else { Err(st) }
}
}
// ---------------------------------------------------------------------------
// SLH-DSA (FIPS 205)
// ---------------------------------------------------------------------------
pub mod slhdsa {
use super::{c_int, CryptoStatus, NistMode};
extern "C" {
#[link_name = "lux_slhdsa_keygen"]
fn slhdsa_keygen(
mode: c_int,
seed: *const u8,
pk: *mut u8,
sk: *mut u8,
) -> c_int;
#[link_name = "lux_slhdsa_sign"]
fn slhdsa_sign(
mode: c_int,
sk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *mut u8,
sig_len: *mut usize,
) -> c_int;
#[link_name = "lux_slhdsa_verify"]
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 sizes for the small variants (pk, sk, max_signature).
pub fn sizes(mode: NistMode) -> (usize, usize, usize) {
match mode {
NistMode::Mode2 => (32, 64, 7856),
NistMode::Mode3 => (48, 96, 16224),
NistMode::Mode5 => (64, 128, 29792),
}
}
pub fn keygen(mode: NistMode, seed: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>), CryptoStatus> {
let (pk_len, sk_len, _) = sizes(mode);
let mut pk = vec![0u8; pk_len];
let mut sk = vec![0u8; sk_len];
let rc = unsafe {
slhdsa_keygen(mode as c_int, seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
};
let st = CryptoStatus::from_int(rc);
if st.is_ok() { Ok((pk, sk)) } else { Err(st) }
}
pub fn sign(mode: NistMode, sk: &[u8], msg: &[u8]) -> Result<Vec<u8>, CryptoStatus> {
let (_, sk_expect, sig_max) = sizes(mode);
if sk.len() != sk_expect {
return Err(CryptoStatus::BadLength);
}
let mut sig = vec![0u8; sig_max];
let mut sig_len: usize = sig_max;
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,
)
};
let st = CryptoStatus::from_int(rc);
if !st.is_ok() {
return Err(st);
}
sig.truncate(sig_len);
Ok(sig)
}
pub fn verify(mode: NistMode, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
let (pk_expect, _, _) = sizes(mode);
if pk.len() != pk_expect {
return false;
}
let rc = unsafe {
slhdsa_verify(
mode as c_int,
pk.as_ptr(),
msg.as_ptr(),
msg.len(),
sig.as_ptr(),
sig.len(),
)
};
rc == 1
}
}
// ---------------------------------------------------------------------------
// Ed25519
// ---------------------------------------------------------------------------
pub mod ed25519 {
use super::{c_int, CryptoStatus};
extern "C" {
#[link_name = "lux_ed25519_keygen"]
fn ed25519_keygen(seed: *const u8, sk: *mut u8, pk: *mut u8) -> c_int;
#[link_name = "lux_ed25519_sign"]
fn ed25519_sign(
sk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *mut u8,
) -> c_int;
#[link_name = "lux_ed25519_verify"]
fn ed25519_verify(
pk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *const u8,
) -> c_int;
}
/// Generate an Ed25519 key pair from a 32-byte seed. Returns `(sk, pk)`.
pub fn keygen(seed: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), CryptoStatus> {
let mut sk = [0u8; 32];
let mut pk = [0u8; 32];
let rc = unsafe { ed25519_keygen(seed.as_ptr(), sk.as_mut_ptr(), pk.as_mut_ptr()) };
let st = CryptoStatus::from_int(rc);
if st.is_ok() { Ok((sk, pk)) } else { Err(st) }
}
/// Sign a message. Returns a 64-byte signature.
pub fn sign(sk: &[u8; 32], msg: &[u8]) -> Result<[u8; 64], CryptoStatus> {
let mut sig = [0u8; 64];
let rc = unsafe {
ed25519_sign(sk.as_ptr(), msg.as_ptr(), msg.len(), sig.as_mut_ptr())
};
let st = CryptoStatus::from_int(rc);
if st.is_ok() { Ok(sig) } else { Err(st) }
}
/// Verify a 64-byte signature. Returns true on valid.
pub fn verify(pk: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> bool {
let rc = unsafe {
ed25519_verify(pk.as_ptr(), msg.as_ptr(), msg.len(), sig.as_ptr())
};
rc == 1
}
}
// ---------------------------------------------------------------------------
// keccak256
// ---------------------------------------------------------------------------
pub mod keccak256 {
extern "C" {
// The C ABI declares this as `void` in lux/crypto/keccak.h.
#[link_name = "lux_keccak256"]
fn keccak256(input: *const u8, input_len: usize, output: *mut u8);
}
/// Compute the keccak256 digest of `input`. Always returns a 32-byte digest.
pub fn hash(input: &[u8]) -> [u8; 32] {
let mut out = [0u8; 32];
unsafe {
keccak256(input.as_ptr(), input.len(), out.as_mut_ptr());
}
out
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
// Compile-time symbol check; the actual smoke test is in luxfi/accel
// (Go side) and the C++ test harness.
use super::*;
// Compile-time symbol check. The actual smoke test is in luxfi/accel
// (Go side) and the C++ test harness; here we just confirm linkage.
#[test]
fn link() {
fn link_secp256k1() {
let _f: unsafe extern "C" fn(*const u8, *const u8, *const u8, u8, *mut u8) -> i32 =
super::secp256k1_ecrecover;
secp256k1_ecrecover;
}
#[test]
fn status_from_int() {
assert_eq!(super::Secp256k1Status::from_int(0), Some(super::Secp256k1Status::Ok));
assert_eq!(super::Secp256k1Status::from_int(7), Some(super::Secp256k1Status::BufferLen));
assert_eq!(super::Secp256k1Status::from_int(99), None);
assert_eq!(Secp256k1Status::from_int(0), Some(Secp256k1Status::Ok));
assert_eq!(Secp256k1Status::from_int(7), Some(Secp256k1Status::BufferLen));
assert_eq!(Secp256k1Status::from_int(99), None);
}
#[test]
fn crypto_status_from_int() {
assert!(CryptoStatus::from_int(0).is_ok());
assert!(CryptoStatus::from_int(1).is_ok());
assert!(!CryptoStatus::from_int(-1).is_ok());
}
#[test]
fn sizes_ml_dsa() {
assert_eq!(mldsa::sizes(NistMode::Mode3), (1952, 4032, 3309));
}
#[test]
fn sizes_ml_kem() {
assert_eq!(mlkem::sizes(NistMode::Mode3), (1184, 2400, 1088));
}
#[test]
fn sizes_slh_dsa() {
assert_eq!(slhdsa::sizes(NistMode::Mode3), (48, 96, 16224));
}
}