mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
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:
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user