rust: ship lux-crypto-blake3 with 140-vector spec_vectors KAT

Real Rust binding over the now-real luxcpp/crypto/blake3 C-ABI
(commit c4a1d2e0, "vendor BLAKE3 v1.5.0 reference C"). Exposes all four
canonical BLAKE3 modes:

    hash(input)                          -> [u8; 32]
    keyed_hash(key, input)               -> [u8; 32]
    derive_key(context_z, key_material)  -> [u8; 32]
    hash_xof(input, &mut output)         -> ()

Statically links libblake3.a + libblake3_cpu.a from the build dir
(CRYPTO_BUILD_DIR or the canonical build-cto/ default).

Test (tests/spec_vectors.rs) parses upstream test_vectors.json (vendored
under luxcpp/crypto/blake3/test/vectors/) and asserts byte-equality
across all 35 cases x 4 modes = 140 assertions. cargo test PASS.
This commit is contained in:
Hanzo AI
2026-04-27 13:07:37 -07:00
parent 04b7b49c86
commit 40b6c6e468
6 changed files with 373 additions and 0 deletions
+4
View File
@@ -18,6 +18,10 @@ version = "0.1.0"
name = "lux-crypto-blake2b"
version = "0.1.0"
[[package]]
name = "lux-crypto-blake3"
version = "0.1.0"
[[package]]
name = "lux-crypto-evm256"
version = "0.1.0"
+1
View File
@@ -20,6 +20,7 @@ members = [
"lux-crypto-sha256",
"lux-crypto-ripemd160",
"lux-crypto-blake2b",
"lux-crypto-blake3",
"lux-crypto-aead",
"lux-crypto-lamport",
"lux-crypto-mldsa",
+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);
}