rust: align extern "C" decls with bare C-ABI symbol convention

The luxcpp/crypto archives export bare-named C symbols (keccak256,
secp256k1_ecrecover, blake3, ed25519_sign, slhdsa_sign) consistently
across all 30+ algorithms. The keccak/secp256k1/blake3 Rust crates had
drifted to declaring `lux_<alg>_<op>` link names that the archives
never exposed, causing link-time symbol-not-found at workspace test.

- keccak:    drop link_name="lux_keccak256", call bare keccak256
- secp256k1: drop link_name="lux_secp256k1_ecrecover{,_batch}", call bare
- blake3:    archive only exports `blake3` today; remove the unimplemented
             keyed_hash/derive_key/hash_xof Rust facade plus the broken
             spec_vectors test that referenced a non-existent JSON file
- ed25519, slhdsa: unchanged (already matched archive)

cargo test --release --workspace: 0 link errors. The pre-existing
ed25519 RFC8032 failures (returning -5 = CRYPTO_ERR_NOTIMPL) are due
to the C-ABI ed25519 still being a NOTIMPL stub in luxcpp/crypto, not
a symbol-audit issue.
This commit is contained in:
Hanzo AI
2026-04-27 18:24:18 -07:00
parent a049b9e5a1
commit 3795068d44
4 changed files with 19 additions and 295 deletions
+12 -89
View File
@@ -4,48 +4,23 @@
// 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:
// `luxcpp/crypto/blake3`. The C-ABI surface currently exposes the default
// 32-byte hash mode only; keyed_hash, derive_key and the XOF will land
// alongside the first-class CPU body and be exposed here at that time.
//
// * `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.
// Byte-equal to upstream b3sum / blake3-py for the default `hash` mode.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::{c_char, c_int};
use core::ffi::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]
@@ -57,64 +32,12 @@ pub fn hash(input: &[u8]) -> [u8; OUT_LEN] {
out
}
/// Compute the keyed BLAKE3 digest (MAC) of `input` under `key`.
/// Compute the default 32-byte BLAKE3 digest of `input` into a caller-supplied
/// buffer. Returns the same buffer for ergonomic chaining.
#[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);
pub fn hash_into<'a>(input: &[u8], output: &'a mut [u8; OUT_LEN]) -> &'a [u8; OUT_LEN] {
// SAFETY: pointers valid for the call's duration; output sized to digest.
let rc = unsafe { blake3(input.as_ptr(), input.len(), output.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "blake3 returned non-zero status: {}", rc);
output
}
@@ -1,196 +0,0 @@
// 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);
}
+3 -4
View File
@@ -22,8 +22,7 @@ extern "C" {
/// The C-ABI declaration in `lux/crypto/keccak.h` returns `void`; we mirror
/// that exactly. Caller must guarantee `output` points to at least 32
/// writable bytes.
#[link_name = "lux_keccak256"]
fn lux_keccak256(input: *const u8, input_len: usize, output: *mut u8);
fn keccak256(input: *const u8, input_len: usize, output: *mut u8);
}
/// Length of a keccak256 digest in bytes.
@@ -39,7 +38,7 @@ pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
// SAFETY: `input.as_ptr()` is valid for `input.len()` bytes (read-only)
// and `out.as_mut_ptr()` is valid for `DIGEST_LEN` bytes (write).
unsafe {
lux_keccak256(input.as_ptr(), input.len(), out.as_mut_ptr());
keccak256(input.as_ptr(), input.len(), out.as_mut_ptr());
}
out
}
@@ -51,7 +50,7 @@ pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
pub fn hash_into<'a>(input: &[u8], output: &'a mut [u8; DIGEST_LEN]) -> &'a [u8; DIGEST_LEN] {
// SAFETY: pointers are valid for the durations of the call.
unsafe {
lux_keccak256(input.as_ptr(), input.len(), output.as_mut_ptr());
keccak256(input.as_ptr(), input.len(), output.as_mut_ptr());
}
output
}
+4 -6
View File
@@ -92,8 +92,7 @@ impl Error {
extern "C" {
/// Recover the 64-byte uncompressed public key (X || Y, big-endian) from
/// `(hash, r, s, v)` where `v` is the recovery id 0 or 1.
#[link_name = "lux_secp256k1_ecrecover"]
fn lux_secp256k1_ecrecover(
fn secp256k1_ecrecover(
hash: *const u8,
r: *const u8,
s: *const u8,
@@ -103,8 +102,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"]
fn lux_secp256k1_ecrecover_batch(
fn secp256k1_ecrecover_batch(
inputs: *const u8,
n: usize,
pubkey_out: *mut u8,
@@ -135,7 +133,7 @@ pub fn ecrecover(
let mut out = [0u8; PUBKEY_LEN];
// SAFETY: All pointers are valid for the call's duration; v is a copy.
let rc = unsafe {
lux_secp256k1_ecrecover(hash.as_ptr(), r.as_ptr(), s.as_ptr(), v, out.as_mut_ptr())
secp256k1_ecrecover(hash.as_ptr(), r.as_ptr(), s.as_ptr(), v, out.as_mut_ptr())
};
if rc == 0 {
Ok(out)
@@ -162,7 +160,7 @@ pub fn ecrecover_batch(inputs: &[u8]) -> Result<(Vec<u8>, Vec<u8>), Error> {
let mut statuses = vec![0u8; n];
// SAFETY: We have shown the buffers are sized to the C-ABI contract.
let rc = unsafe {
lux_secp256k1_ecrecover_batch(
secp256k1_ecrecover_batch(
inputs.as_ptr(),
n,
pubkeys.as_mut_ptr(),