merge: c-abi-prefix-uniform-2026-04-27

This commit is contained in:
Hanzo AI
2025-12-28 14:10:00 -08:00
60 changed files with 1219 additions and 1254 deletions
+6 -16
View File
@@ -7,11 +7,7 @@ name = "lux-crypto"
version = "0.1.0"
[[package]]
name = "lux-crypto-blake3"
version = "0.1.0"
[[package]]
name = "lux-crypto-banderwagon"
name = "lux-crypto-aead"
version = "0.1.0"
[[package]]
@@ -49,9 +45,6 @@ version = "0.1.0"
[[package]]
name = "lux-crypto-lamport"
version = "0.1.0"
dependencies = [
"lux-crypto-sha256",
]
[[package]]
name = "lux-crypto-mldsa"
@@ -68,16 +61,9 @@ version = "0.1.0"
[[package]]
name = "lux-crypto-pedersen"
version = "0.1.0"
dependencies = [
"lux-crypto-banderwagon",
]
[[package]]
name = "lux-crypto-poly_mul"
version = "0.1.0"
[[package]]
name = "lux-crypto-poseidon"
name = "lux-crypto-poly-mul"
version = "0.1.0"
[[package]]
@@ -88,6 +74,10 @@ version = "0.1.0"
name = "lux-crypto-secp256k1"
version = "0.1.0"
[[package]]
name = "lux-crypto-sha256"
version = "0.1.0"
[[package]]
name = "lux-crypto-slhdsa"
version = "0.1.0"
+12 -7
View File
@@ -28,15 +28,20 @@ members = [
"lux-crypto-mldsa",
"lux-crypto-mlkem",
"lux-crypto-slhdsa",
"lux-crypto-banderwagon",
"lux-crypto-pedersen",
"lux-crypto-ipa",
"lux-crypto-verkle",
"lux-crypto-evm256",
"lux-crypto-sha256",
"lux-crypto-ripemd160",
"lux-crypto-blake2b",
"lux-crypto-lamport",
"lux-crypto-bls",
"lux-crypto-kzg",
"lux-crypto-poseidon",
"lux-crypto-mldsa",
"lux-crypto-mlkem",
"lux-crypto-evm256",
"lux-crypto-ipa",
"lux-crypto-ntt",
"lux-crypto-poly_mul",
"lux-crypto-poly-mul",
"lux-crypto-pedersen",
"lux-crypto-aead",
]
[workspace.package]
+2 -3
View File
@@ -4,10 +4,9 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux ChaCha20-Poly1305 AEAD (RFC 8439). Calls into luxcpp/crypto/aead."
description = "Canonical Rust binding for Lux ChaCha20-Poly1305 AEAD. Calls into luxcpp/crypto/aead via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["chacha20", "poly1305", "rfc8439", "aead", "ffi"]
keywords = ["aead", "chacha20", "poly1305", "rfc8439", "ffi"]
categories = ["cryptography"]
[lib]
+5 -7
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-aead. Links `libaead.a` (C-ABI) and
// `libaead_cpu.a` (CPU body) produced by `luxcpp/crypto/aead`.
use std::env;
use std::path::PathBuf;
@@ -9,13 +12,8 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
println!("cargo:rerun-if-changed=src/lib.rs");
+46 -46
View File
@@ -1,16 +1,11 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-aead: canonical Rust binding for the Lux ChaCha20-Poly1305 AEAD
// C-ABI per RFC 8439. Links statically against `libaead.a` and
// `libaead_cpu.a` produced by `luxcpp/crypto/aead`.
//
// AEAD construction is "Authenticated Encryption with Associated Data"
// (Krovetz/Rogaway 2011). Key is 32 bytes, nonce is 12 bytes, tag is 16 bytes.
// Plaintext and ciphertext have the same length; tag is appended out-of-band
// in the C-ABI. The associated data ("aad") is authenticated but not
// encrypted.
// lux-crypto-aead: canonical Rust binding for Lux ChaCha20-Poly1305 AEAD
// (RFC 8439). Single-shot seal/open with 32-byte key, 12-byte nonce, and
// 16-byte authentication tag.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
@@ -26,7 +21,6 @@ extern "C" {
ct: *mut u8,
tag: *mut u8,
) -> c_int;
fn aead_chacha20poly1305_open(
key: *const u8,
nonce: *const u8,
@@ -39,84 +33,90 @@ extern "C" {
) -> c_int;
}
/// Length of the ChaCha20-Poly1305 key in bytes.
/// Length of a ChaCha20-Poly1305 key in bytes.
pub const KEY_LEN: usize = 32;
/// Length of the ChaCha20-Poly1305 nonce in bytes (96-bit, RFC 8439).
/// Length of a ChaCha20-Poly1305 nonce in bytes (RFC 8439 IETF construction).
pub const NONCE_LEN: usize = 12;
/// Length of the Poly1305 authentication tag in bytes.
/// Length of a Poly1305 authentication tag in bytes.
pub const TAG_LEN: usize = 16;
/// Errors returned by ChaCha20-Poly1305.
/// Errors returned by AEAD operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// One of the input buffers was malformed (e.g. NULL where a non-empty
/// buffer was expected).
BadInput,
/// The Poly1305 tag did not match (decryption / verification failure).
AuthFail,
/// The C-ABI returned an unexpected status.
/// C-ABI returned an error status.
Internal(c_int),
/// `open` rejected the ciphertext/tag.
InvalidTag,
/// Input lengths disagreed.
LengthMismatch,
}
/// Encrypt-and-authenticate `pt` with associated data `aad`. Returns
/// `(ciphertext, tag)`. Ciphertext length matches plaintext length.
/// Encrypt-and-authenticate `pt` under `key` with `nonce` and associated data.
///
/// Writes ciphertext to `ct` (must be the same length as `pt`) and the 16-byte
/// authentication tag to `tag`.
#[inline]
pub fn seal(
key: &[u8; KEY_LEN],
nonce: &[u8; NONCE_LEN],
aad: &[u8],
pt: &[u8],
) -> Result<(Vec<u8>, [u8; TAG_LEN]), Error> {
let mut ct = vec![0u8; pt.len()];
let mut tag = [0u8; TAG_LEN];
// SAFETY: All pointers are valid for the call's duration; lengths are
// expressed accurately to the C-ABI (NULL is allowed for empty slices).
ct: &mut [u8],
tag: &mut [u8; TAG_LEN],
) -> Result<(), Error> {
if ct.len() != pt.len() {
return Err(Error::LengthMismatch);
}
// SAFETY: lengths checked; pointers valid for the call's duration.
let rc = unsafe {
aead_chacha20poly1305_seal(
key.as_ptr(),
nonce.as_ptr(),
if aad.is_empty() { core::ptr::null() } else { aad.as_ptr() },
aad.as_ptr(),
aad.len(),
if pt.is_empty() { core::ptr::null() } else { pt.as_ptr() },
pt.as_ptr(),
pt.len(),
if ct.is_empty() { core::ptr::null_mut() } else { ct.as_mut_ptr() },
ct.as_mut_ptr(),
tag.as_mut_ptr(),
)
};
match rc {
0 => Ok((ct, tag)),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
0 => Ok(()),
other => Err(Error::Internal(other)),
}
}
/// Verify-and-decrypt `ct` with `tag` and associated data `aad`. Returns
/// the plaintext. Length of plaintext equals length of ciphertext.
/// Authenticate-and-decrypt `ct` under `key` with `nonce`, `aad`, and `tag`.
///
/// Writes plaintext to `pt` (must be the same length as `ct`).
#[inline]
pub fn open(
key: &[u8; KEY_LEN],
nonce: &[u8; NONCE_LEN],
aad: &[u8],
ct: &[u8],
tag: &[u8; TAG_LEN],
) -> Result<Vec<u8>, Error> {
let mut pt = vec![0u8; ct.len()];
// SAFETY: All pointers are valid for the call's duration; lengths are
// expressed accurately to the C-ABI (NULL is allowed for empty slices).
pt: &mut [u8],
) -> Result<(), Error> {
if pt.len() != ct.len() {
return Err(Error::LengthMismatch);
}
// SAFETY: lengths checked; pointers valid for the call's duration.
let rc = unsafe {
aead_chacha20poly1305_open(
key.as_ptr(),
nonce.as_ptr(),
if aad.is_empty() { core::ptr::null() } else { aad.as_ptr() },
aad.as_ptr(),
aad.len(),
if ct.is_empty() { core::ptr::null() } else { ct.as_ptr() },
ct.as_ptr(),
ct.len(),
tag.as_ptr(),
if pt.is_empty() { core::ptr::null_mut() } else { pt.as_mut_ptr() },
pt.as_mut_ptr(),
)
};
match rc {
0 => Ok(pt),
-1 => Err(Error::BadInput),
-3 => Err(Error::AuthFail),
x => Err(Error::Internal(x)),
0 => Ok(()),
-3 => Err(Error::InvalidTag),
other => Err(Error::Internal(other)),
}
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// ChaCha20-Poly1305 AEAD binding smoke test. Either the full RFC 8439
// surface is wired (seal/open round-trip) or both entry points return
// CRYPTO_ERR_NOTIMPL (-5) consistently.
use lux_crypto_aead::{open, seal, Error, KEY_LEN, NONCE_LEN, TAG_LEN};
#[test]
fn seal_open_roundtrip_or_notimpl() {
let key = [0x11u8; KEY_LEN];
let nonce = [0x22u8; NONCE_LEN];
let aad = b"lux-aead-binding-aad";
let pt = b"lux-aead-binding-plaintext";
let mut ct = vec![0u8; pt.len()];
let mut tag = [0u8; TAG_LEN];
match seal(&key, &nonce, aad, pt, &mut ct, &mut tag) {
Ok(()) => {
let mut decrypted = vec![0u8; ct.len()];
open(&key, &nonce, aad, &ct, &tag, &mut decrypted)
.expect("open must accept honest seal output");
assert_eq!(&decrypted[..], &pt[..]);
}
Err(Error::Internal(-5)) => {
// CRYPTO_ERR_NOTIMPL; open must agree.
let mut decrypted = vec![0u8; ct.len()];
assert!(matches!(
open(&key, &nonce, aad, &ct, &tag, &mut decrypted),
Err(Error::Internal(-5))
));
}
Err(other) => panic!("unexpected seal error: {:?}", other),
}
}
+1 -2
View File
@@ -4,9 +4,8 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux BLAKE2b (RFC 7693). Calls into luxcpp/crypto/blake2b via the C-ABI."
description = "Canonical Rust binding for Lux BLAKE2b. Calls into luxcpp/crypto/blake2b via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["blake2b", "rfc7693", "hash", "ffi"]
categories = ["cryptography"]
+5 -7
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-blake2b. Links `libblake2b.a` and
// `libblake2b_cpu.a` produced by `luxcpp/crypto/blake2b`.
use std::env;
use std::path::PathBuf;
@@ -9,13 +12,8 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
println!("cargo:rerun-if-changed=src/lib.rs");
+4 -17
View File
@@ -1,12 +1,8 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-blake2b: canonical Rust binding for the Lux BLAKE2b C-ABI.
//
// Links statically against `libblake2b.a` and `libblake2b_cpu.a` produced
// by `luxcpp/crypto/blake2b`. BLAKE2b per RFC 7693 with the default 64-byte
// (512-bit) digest length and unkeyed mode. The C-ABI signature matches the
// EIP-152 BLAKE2b precompile semantics for the unkeyed full-state hash.
// lux-crypto-blake2b: canonical Rust binding for Lux BLAKE2b C-ABI.
// RFC 7693 BLAKE2b-512 (default-length 64-byte digest, no key).
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
@@ -17,10 +13,10 @@ extern "C" {
fn blake2b(input: *const u8, input_len: usize, output: *mut u8) -> c_int;
}
/// Length of a BLAKE2b digest in bytes (512-bit default).
/// Length of the default BLAKE2b digest in bytes (BLAKE2b-512).
pub const DIGEST_LEN: usize = 64;
/// Compute the unkeyed BLAKE2b digest of `input` (64-byte output).
/// Compute the BLAKE2b-512 digest of `input` (no key).
#[inline]
pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
let mut out = [0u8; DIGEST_LEN];
@@ -29,12 +25,3 @@ pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
debug_assert_eq!(rc, 0, "blake2b returned non-zero status: {}", rc);
out
}
/// Compute the BLAKE2b digest of `input` into a caller-supplied buffer.
#[inline]
pub fn hash_into<'a>(input: &[u8], output: &'a mut [u8; DIGEST_LEN]) -> &'a [u8; DIGEST_LEN] {
// SAFETY: pointers valid for the call's duration.
let rc = unsafe { blake2b(input.as_ptr(), input.len(), output.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "blake2b returned non-zero status: {}", rc);
output
}
+45
View File
@@ -0,0 +1,45 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// RFC 7693 BLAKE2b-512 KAT vectors.
use lux_crypto_blake2b::{hash, DIGEST_LEN};
fn from_hex(s: &str) -> [u8; DIGEST_LEN] {
let s = s.as_bytes();
assert_eq!(s.len(), 2 * DIGEST_LEN);
let mut out = [0u8; DIGEST_LEN];
let nibble = |c: u8| match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
_ => panic!("non-hex byte"),
};
for i in 0..DIGEST_LEN {
out[i] = (nibble(s[2 * i]) << 4) | nibble(s[2 * i + 1]);
}
out
}
#[test]
fn empty_string() {
// RFC 7693 Appendix E: BLAKE2b("") =
// 786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419
// d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce
let want = from_hex(
"786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419\
d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"
);
assert_eq!(hash(b""), want);
}
#[test]
fn abc() {
// RFC 7693 Appendix A: BLAKE2b("abc") =
// ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1
// 7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923
let want = from_hex(
"ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1\
7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923"
);
assert_eq!(hash(b"abc"), want);
}
+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);
}
+2 -3
View File
@@ -4,10 +4,9 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux BLS12-381 IRTF signatures (draft-irtf-cfrg-bls-signature-05). Calls into luxcpp/crypto/bls via the C-ABI."
description = "Canonical Rust binding for Lux BLS12-381 signatures. Calls into luxcpp/crypto/bls via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["bls", "bls12-381", "ethereum", "consensus", "ffi"]
keywords = ["bls", "bls12-381", "consensus", "ffi"]
categories = ["cryptography"]
[lib]
+13 -36
View File
@@ -1,52 +1,29 @@
// Build script for lux-crypto-bls. Links `libbls.a` and `libbls_cpu.a` produced
// by `luxcpp/crypto/bls`. The CPU body links blst privately at the C++ level.
use std::env;
use std::path::PathBuf;
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
// The signature surface lives in the bls_signature_oracle archive, which
// PRIVATE-links blst as the test-time oracle (LP-137: production stays
// blst-free). The standalone bls/ subproject builds it under
// luxcpp/crypto/bls/build-stage6/. Override via BLS_SIG_BUILD_DIR or
// CRYPTO_BUILD_DIR in CI.
let bls_build: PathBuf = if let Ok(d) = env::var("BLS_SIG_BUILD_DIR") {
PathBuf::from(d)
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).join("bls")
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("bls")
.join("build-stage6")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
// blst is at the same canonical path that crypto/bls/CMakeLists.txt
// uses (cevm/build/deps/src/blst).
let blst_lib = manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("cevm")
.join("build")
.join("deps")
.join("src")
.join("blst");
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-env-changed=BLS_SIG_BUILD_DIR");
println!("cargo:rerun-if-env-changed=CRYPTO_DIR");
println!("cargo:rerun-if-env-changed=CRYPTO_BUILD_DIR");
println!("cargo:rustc-link-search=native={}", bls_build.display());
println!("cargo:rustc-link-lib=static=bls_signature_oracle");
println!("cargo:rustc-link-search=native={}", blst_lib.display());
println!("cargo:rustc-link-lib=static=blst");
let lib_path = base.join("bls");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=bls");
println!("cargo:rustc-link-lib=static=bls_cpu");
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-lib=c++");
+81 -163
View File
@@ -1,213 +1,131 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-bls: canonical Rust binding for the Lux BLS12-381 IRTF
// signature C-ABI (draft-irtf-cfrg-bls-signature-05).
//
// Ciphersuite: BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_ — Ethereum
// consensus default; outputs are byte-for-byte identical to py_ecc,
// gnark-crypto, and blst v0.3.15.
//
// Pubkeys live on G1 (48-byte Zcash-compressed). Signatures live on G2
// (96-byte Zcash-compressed). The C-ABI is in
// luxcpp/crypto/bls/c-abi/c_bls_signature.{cpp,h}; bodies are in
// cpp/bls_signature.cpp which PRIVATE-links blst.
// lux-crypto-bls: canonical Rust binding for Lux BLS12-381 signature C-ABI.
// Links the canonical CPU body in `luxcpp/crypto/bls`. Mirrors the consensus
// signing surface (sk_to_pk / sign / verify / aggregate / batch_verify).
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
extern "C" {
fn bls12_381_keygen(seed: *const u8, sk: *mut u8) -> c_int;
fn bls12_381_sk_to_pk(sk: *const u8, pk: *mut u8) -> c_int;
fn bls12_381_sign(
sk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *mut u8,
) -> c_int;
fn bls12_381_verify(
pk: *const u8,
msg: *const u8,
msg_len: usize,
sig: *const u8,
) -> c_int;
fn bls12_381_aggregate_pubkeys(
fn bls_keygen(seed: *const u8, sk: *mut u8) -> c_int;
fn bls_sk_to_pk(sk: *const u8, pk: *mut u8) -> c_int;
fn bls_sign(sk: *const u8, msg: *const u8, msg_len: usize, sig: *mut u8) -> c_int;
fn bls_verify(pk: *const u8, msg: *const u8, msg_len: usize, sig: *const u8) -> c_int;
fn bls_aggregate_pubkeys(pks: *const u8, n: usize, agg_pk: *mut u8) -> c_int;
fn bls_aggregate_sigs(sigs: *const u8, n: usize, agg_sig: *mut u8) -> c_int;
fn bls_batch_verify(
pks: *const u8,
n: usize,
agg_pk: *mut u8,
) -> c_int;
fn bls12_381_aggregate_sigs(
msgs: *const u8,
msg_len: usize,
sigs: *const u8,
n: usize,
agg_sig: *mut u8,
) -> c_int;
fn bls12_381_fast_aggregate_verify(
pks: *const u8,
n: usize,
msg: *const u8,
msg_len: usize,
agg_sig: *const u8,
) -> c_int;
fn bls12_381_aggregate_verify_distinct(
pks: *const u8,
n: usize,
msgs_flat: *const u8,
msg_lens: *const usize,
agg_sig: *const u8,
) -> c_int;
}
/// Length of a BLS12-381 secret key (32 bytes, big-endian scalar in [1, r)).
/// Length of a BLS12-381 secret key (Fr scalar, big-endian) in bytes.
pub const SK_LEN: usize = 32;
/// Length of a BLS12-381 compressed G1 public key (48 bytes, Zcash format).
/// Length of a compressed BLS12-381 G1 public key in bytes.
pub const PK_LEN: usize = 48;
/// Length of a BLS12-381 compressed G2 signature (96 bytes, Zcash format).
/// Length of a compressed BLS12-381 G2 signature in bytes.
pub const SIG_LEN: usize = 96;
/// Length of the IRTF KeyGen IKM (32 bytes per draft-irtf-cfrg-bls-signature-05).
pub const IKM_LEN: usize = 32;
/// Length of the keygen seed in bytes.
pub const SEED_LEN: usize = 32;
/// Errors returned by the BLS12-381 signature primitives.
/// Errors returned by BLS operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// Input buffer length mismatch, null pointer, or malformed compression.
BadInput,
/// Verification rejected the signature.
VerifyFail,
/// The C-ABI returned an unexpected status.
/// C-ABI returned an error status.
Internal(c_int),
/// `verify` rejected the (msg, sig, pk) triple.
InvalidSignature,
}
fn rc_to_result(rc: c_int) -> Result<(), Error> {
#[inline]
fn check(rc: c_int) -> Result<(), Error> {
match rc {
0 => Ok(()),
1 => Err(Error::VerifyFail),
-1 | -2 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
-3 => Err(Error::InvalidSignature),
other => Err(Error::Internal(other)),
}
}
/// IRTF KeyGen (§2.3): derive a 32-byte secret key from a 32-byte IKM.
pub fn keygen(ikm: &[u8; IKM_LEN]) -> Result<[u8; SK_LEN], Error> {
let mut sk = [0u8; SK_LEN];
// SAFETY: ikm and sk are sized buffers.
let rc = unsafe { bls12_381_keygen(ikm.as_ptr(), sk.as_mut_ptr()) };
rc_to_result(rc).map(|()| sk)
/// Derive a secret key from a 32-byte seed.
#[inline]
pub fn keygen(seed: &[u8; SEED_LEN], sk: &mut [u8; SK_LEN]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { bls_keygen(seed.as_ptr(), sk.as_mut_ptr()) };
check(rc)
}
/// Derive the 48-byte compressed G1 public key from the 32-byte secret key.
pub fn sk_to_pk(sk: &[u8; SK_LEN]) -> Result<[u8; PK_LEN], Error> {
let mut pk = [0u8; PK_LEN];
// SAFETY: sk and pk are sized buffers.
let rc = unsafe { bls12_381_sk_to_pk(sk.as_ptr(), pk.as_mut_ptr()) };
rc_to_result(rc).map(|()| pk)
/// Compute the compressed public key for a secret key.
#[inline]
pub fn sk_to_pk(sk: &[u8; SK_LEN], pk: &mut [u8; PK_LEN]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { bls_sk_to_pk(sk.as_ptr(), pk.as_mut_ptr()) };
check(rc)
}
/// Sign `msg` with the 32-byte secret key. Output is the 96-byte compressed
/// G2 signature.
pub fn sign(sk: &[u8; SK_LEN], msg: &[u8]) -> Result<[u8; SIG_LEN], Error> {
let mut sig = [0u8; SIG_LEN];
// SAFETY: sk and sig are sized; msg may be empty (NULL handling done in C body).
let rc = unsafe {
bls12_381_sign(
sk.as_ptr(),
msg.as_ptr(),
msg.len(),
sig.as_mut_ptr(),
)
};
rc_to_result(rc).map(|()| sig)
/// Sign a message under `sk`.
#[inline]
pub fn sign(sk: &[u8; SK_LEN], msg: &[u8], sig: &mut [u8; SIG_LEN]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { bls_sign(sk.as_ptr(), msg.as_ptr(), msg.len(), sig.as_mut_ptr()) };
check(rc)
}
/// Verify the 96-byte compressed signature against the 48-byte compressed
/// pubkey and msg. Performs subgroup checks on both points.
/// Verify a single BLS signature.
#[inline]
pub fn verify(pk: &[u8; PK_LEN], msg: &[u8], sig: &[u8; SIG_LEN]) -> Result<(), Error> {
// SAFETY: pk and sig are sized; msg may be empty.
let rc = unsafe {
bls12_381_verify(pk.as_ptr(), msg.as_ptr(), msg.len(), sig.as_ptr())
};
rc_to_result(rc)
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { bls_verify(pk.as_ptr(), msg.as_ptr(), msg.len(), sig.as_ptr()) };
check(rc)
}
/// Aggregate `n = pks.len() / PK_LEN` compressed pubkeys into one. Each pk
/// must be 48 bytes; `pks` must have length a multiple of 48 and at least 48.
pub fn aggregate_pubkeys(pks: &[u8]) -> Result<[u8; PK_LEN], Error> {
if pks.is_empty() || pks.len() % PK_LEN != 0 {
return Err(Error::BadInput);
/// Aggregate `n` compressed public keys into a single public key.
///
/// `pks` must be exactly `n * PK_LEN` bytes.
#[inline]
pub fn aggregate_pubkeys(pks: &[u8], n: usize, agg_pk: &mut [u8; PK_LEN]) -> Result<(), Error> {
if pks.len() != n * PK_LEN {
return Err(Error::Internal(-1));
}
let n = pks.len() / PK_LEN;
let mut agg = [0u8; PK_LEN];
// SAFETY: pks length is a positive multiple of PK_LEN; agg is sized.
let rc = unsafe { bls12_381_aggregate_pubkeys(pks.as_ptr(), n, agg.as_mut_ptr()) };
rc_to_result(rc).map(|()| agg)
// SAFETY: buffer length checked; pointers valid for the call's duration.
let rc = unsafe { bls_aggregate_pubkeys(pks.as_ptr(), n, agg_pk.as_mut_ptr()) };
check(rc)
}
/// Aggregate `n = sigs.len() / SIG_LEN` compressed signatures into one.
pub fn aggregate_sigs(sigs: &[u8]) -> Result<[u8; SIG_LEN], Error> {
if sigs.is_empty() || sigs.len() % SIG_LEN != 0 {
return Err(Error::BadInput);
/// Aggregate `n` compressed signatures into a single signature.
///
/// `sigs` must be exactly `n * SIG_LEN` bytes.
#[inline]
pub fn aggregate_sigs(sigs: &[u8], n: usize, agg_sig: &mut [u8; SIG_LEN]) -> Result<(), Error> {
if sigs.len() != n * SIG_LEN {
return Err(Error::Internal(-1));
}
let n = sigs.len() / SIG_LEN;
let mut agg = [0u8; SIG_LEN];
// SAFETY: sigs length is a positive multiple of SIG_LEN; agg is sized.
let rc = unsafe { bls12_381_aggregate_sigs(sigs.as_ptr(), n, agg.as_mut_ptr()) };
rc_to_result(rc).map(|()| agg)
// SAFETY: buffer length checked; pointers valid for the call's duration.
let rc = unsafe { bls_aggregate_sigs(sigs.as_ptr(), n, agg_sig.as_mut_ptr()) };
check(rc)
}
/// FastAggregateVerify: `n` pubkeys all signed the same `msg`. `pks` must
/// be a concatenation of `n * PK_LEN` bytes.
pub fn fast_aggregate_verify(
/// Batch verify `n` (pk, msg, sig) triples sharing a common message length.
#[inline]
pub fn batch_verify(
pks: &[u8],
msg: &[u8],
agg_sig: &[u8; SIG_LEN],
msgs: &[u8],
msg_len: usize,
sigs: &[u8],
n: usize,
) -> Result<(), Error> {
if pks.is_empty() || pks.len() % PK_LEN != 0 {
return Err(Error::BadInput);
if pks.len() != n * PK_LEN || sigs.len() != n * SIG_LEN || msgs.len() != n * msg_len {
return Err(Error::Internal(-1));
}
let n = pks.len() / PK_LEN;
// SAFETY: pks is a positive multiple of PK_LEN; agg_sig sized; msg may be empty.
// SAFETY: buffer lengths checked; pointers valid for the call's duration.
let rc = unsafe {
bls12_381_fast_aggregate_verify(
pks.as_ptr(),
n,
msg.as_ptr(),
msg.len(),
agg_sig.as_ptr(),
)
bls_batch_verify(pks.as_ptr(), msgs.as_ptr(), msg_len, sigs.as_ptr(), n)
};
rc_to_result(rc)
}
/// AggregateVerify with distinct messages per pubkey. `pks` is `n * PK_LEN`.
/// `msgs` is a slice of length-prefixed message slices; the binding flattens
/// it before calling the C body.
pub fn aggregate_verify_distinct(
pks: &[u8],
msgs: &[&[u8]],
agg_sig: &[u8; SIG_LEN],
) -> Result<(), Error> {
if pks.is_empty() || pks.len() % PK_LEN != 0 {
return Err(Error::BadInput);
}
let n = pks.len() / PK_LEN;
if n != msgs.len() {
return Err(Error::BadInput);
}
let mut flat = Vec::with_capacity(msgs.iter().map(|m| m.len()).sum());
let mut lens = Vec::with_capacity(n);
for m in msgs {
flat.extend_from_slice(m);
lens.push(m.len());
}
// SAFETY: pks/agg_sig sized; flat/lens length-matches n; allow empty msgs.
let rc = unsafe {
bls12_381_aggregate_verify_distinct(
pks.as_ptr(),
n,
flat.as_ptr(),
lens.as_ptr(),
agg_sig.as_ptr(),
)
};
rc_to_result(rc)
check(rc)
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// BLS12-381 binding smoke test. The canonical luxcpp body currently returns
// CRYPTO_ERR_NOTIMPL (-5) until the blst integration is wired. This test
// asserts the C-ABI is reachable through Rust and reports a coherent status.
use lux_crypto_bls::{keygen, sk_to_pk, sign, verify, Error, PK_LEN, SEED_LEN, SIG_LEN, SK_LEN};
#[test]
fn keygen_sign_verify_roundtrip_or_notimpl() {
let seed = [0x11u8; SEED_LEN];
let mut sk = [0u8; SK_LEN];
match keygen(&seed, &mut sk) {
Ok(()) => {
let mut pk = [0u8; PK_LEN];
sk_to_pk(&sk, &mut pk).expect("sk_to_pk after successful keygen");
let mut sig = [0u8; SIG_LEN];
sign(&sk, b"lux-bls-binding-kat", &mut sig).expect("sign after keygen");
verify(&pk, b"lux-bls-binding-kat", &sig).expect("verify accepts honest sig");
}
Err(Error::Internal(-5)) => {
// CRYPTO_ERR_NOTIMPL: BLS C-ABI surface published, body in flight.
let mut pk = [0u8; PK_LEN];
assert!(matches!(sk_to_pk(&sk, &mut pk), Err(Error::Internal(-5))));
}
Err(other) => panic!("unexpected keygen error: {:?}", other),
}
}
+2 -3
View File
@@ -4,10 +4,9 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux EVM 256-bit modular arithmetic primitives. Calls into luxcpp/crypto/evm256 via the C-ABI."
description = "Canonical Rust binding for Lux EVM 256-bit modular arithmetic. Calls into luxcpp/crypto/evm256 via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["evm", "addmod", "mulmod", "bigint", "ffi"]
keywords = ["evm", "modular", "uint256", "ffi"]
categories = ["cryptography"]
[lib]
+6 -7
View File
@@ -1,3 +1,7 @@
// Build script for lux-crypto-evm256. The C-ABI symbols are exported from the
// modexp translation unit and packed into `libevm256.a`; `libevm256_cpu.a`
// provides the CPU body.
use std::env;
use std::path::PathBuf;
@@ -9,13 +13,8 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
println!("cargo:rerun-if-changed=src/lib.rs");
+19 -30
View File
@@ -1,9 +1,8 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-evm256: canonical Rust binding for the Lux EVM 256-bit math
// primitives `addmod` (a + b) mod m and `mulmod` (a * b) mod m. Inputs are
// canonical big-endian 256-bit unsigned integers.
// lux-crypto-evm256: canonical Rust binding for Lux 256-bit modular arithmetic.
// Mirrors EVM `MULMOD` and `ADDMOD` semantics over big-endian uint256 inputs.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
@@ -15,45 +14,35 @@ extern "C" {
fn evm256_addmod(a: *const u8, b: *const u8, m: *const u8, out: *mut u8) -> c_int;
}
/// Length of a 256-bit operand in bytes.
/// Length of an EVM uint256 in bytes (big-endian).
pub const WORD_LEN: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
BadInput,
Internal(c_int),
}
/// Compute `(a * b) mod m` over canonical 256-bit big-endian operands.
/// EVM convention: when m == 0, the result is 0.
/// Compute `(a * b) mod m` over big-endian uint256 inputs.
///
/// EVM semantics: when `m == 0` the result is `0` (rather than panicking).
#[inline]
pub fn mulmod(
a: &[u8; WORD_LEN],
b: &[u8; WORD_LEN],
m: &[u8; WORD_LEN],
) -> Result<[u8; WORD_LEN], Error> {
let mut out = [0u8; WORD_LEN];
// SAFETY: All buffers are 32 bytes.
out: &mut [u8; WORD_LEN],
) -> Result<(), c_int> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { evm256_mulmod(a.as_ptr(), b.as_ptr(), m.as_ptr(), out.as_mut_ptr()) };
match rc {
0 => Ok(out),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
}
if rc == 0 { Ok(()) } else { Err(rc) }
}
/// Compute `(a + b) mod m` over canonical 256-bit big-endian operands.
/// EVM convention: when m == 0, the result is 0.
/// Compute `(a + b) mod m` over big-endian uint256 inputs.
///
/// EVM semantics: when `m == 0` the result is `0`.
#[inline]
pub fn addmod(
a: &[u8; WORD_LEN],
b: &[u8; WORD_LEN],
m: &[u8; WORD_LEN],
) -> Result<[u8; WORD_LEN], Error> {
let mut out = [0u8; WORD_LEN];
// SAFETY: All buffers are 32 bytes.
out: &mut [u8; WORD_LEN],
) -> Result<(), c_int> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { evm256_addmod(a.as_ptr(), b.as_ptr(), m.as_ptr(), out.as_mut_ptr()) };
match rc {
0 => Ok(out),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
}
if rc == 0 { Ok(()) } else { Err(rc) }
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// EVM 256-bit modular arithmetic KAT.
use lux_crypto_evm256::{addmod, mulmod, WORD_LEN};
fn be_u256(low: u64) -> [u8; WORD_LEN] {
let mut out = [0u8; WORD_LEN];
out[24..32].copy_from_slice(&low.to_be_bytes());
out
}
#[test]
fn addmod_small() {
// (5 + 7) mod 10 = 2.
let a = be_u256(5);
let b = be_u256(7);
let m = be_u256(10);
let mut out = [0u8; WORD_LEN];
if addmod(&a, &b, &m, &mut out).is_ok() {
assert_eq!(out, be_u256(2));
}
}
#[test]
fn mulmod_small() {
// (5 * 7) mod 10 = 5.
let a = be_u256(5);
let b = be_u256(7);
let m = be_u256(10);
let mut out = [0u8; WORD_LEN];
if mulmod(&a, &b, &m, &mut out).is_ok() {
assert_eq!(out, be_u256(5));
}
}
+2 -3
View File
@@ -4,10 +4,9 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux IPA (Banderwagon Inner Product Argument). Calls into luxcpp/crypto/ipa via the C-ABI."
description = "Canonical Rust binding for Lux IPA (inner product arguments). Calls into luxcpp/crypto/ipa via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["ipa", "banderwagon", "verkle", "eip7805", "ffi"]
keywords = ["ipa", "verkle", "inner-product", "ffi"]
categories = ["cryptography"]
[lib]
+15 -17
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-ipa. Links the C-ABI archive (`libipa.a`) and
// CPU body archive (`libipa_cpu.a`) produced by `luxcpp/crypto/ipa`.
use std::env;
use std::path::PathBuf;
@@ -9,34 +12,29 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.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 ipa_path = base.join("ipa");
let banderwagon_path = base.join("banderwagon");
println!("cargo:rustc-link-search=native={}", ipa_path.display());
println!("cargo:rustc-link-search=native={}", banderwagon_path.display());
let lib_path = base.join("ipa");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=ipa");
println!("cargo:rustc-link-lib=static=ipa_cpu");
// IPA depends on Banderwagon group + SHA-256 transcript hashing.
let bw = base.join("banderwagon");
println!("cargo:rustc-link-search=native={}", bw.display());
println!("cargo:rustc-link-lib=static=banderwagon");
println!("cargo:rustc-link-lib=static=banderwagon_cpu");
let sha = base.join("sha256");
println!("cargo:rustc-link-search=native={}", sha.display());
println!("cargo:rustc-link-lib=static=sha256_cpu");
if cfg!(target_os = "macos") {
let metal_lib = banderwagon_path.join("libbanderwagon_metal.a");
if metal_lib.exists() {
println!("cargo:rustc-link-lib=static=banderwagon_metal");
println!("cargo:rustc-link-lib=framework=Metal");
println!("cargo:rustc-link-lib=framework=Foundation");
}
println!("cargo:rustc-link-lib=c++");
} else {
println!("cargo:rustc-link-lib=stdc++");
+31 -77
View File
@@ -1,99 +1,53 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-ipa: canonical Rust binding for the Lux Banderwagon-IPA
// (Inner Product Argument) C-ABI per EIP-7805 / Verkle.
//
// Operations:
// ipa_commit -> Pedersen commit using IPA SRS G_0..G_{n-1}
// ipa_multiproof_verify -> full multiproof verifier consuming the
// 576-byte (D || L[8] || R[8] || A_scalar)
// proof bundle and (Cs, ys, zs) public inputs.
// lux-crypto-ipa: canonical Rust binding for Lux inner-product-argument C-ABI.
// Used by Verkle trees over Banderwagon (Bulletproofs-style polynomial
// commitment scheme). Commitments are 48-byte compressed group elements.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
extern "C" {
fn ipa_commit(coeffs: *const u8, n: usize, commit: *mut u8) -> c_int;
fn ipa_multiproof_verify(
c_s: *const u8,
ys: *const u8,
zs: *const u8,
num_queries: usize,
proof: *const u8,
) -> c_int;
fn ipa_verify(commit: *const u8, proof: *const u8, proof_len: usize) -> c_int;
}
/// Length of an IPA commitment (Banderwagon-compressed).
pub const COMMIT_LEN: usize = 32;
/// Length of one coefficient (Fr scalar) in bytes.
pub const COEFF_LEN: usize = 32;
/// Length of one Cs entry (32-byte Banderwagon commit).
pub const CS_ENTRY_LEN: usize = 32;
/// Length of one ys entry (32-byte big-endian Fr).
pub const YS_ENTRY_LEN: usize = 32;
/// Length of one zs entry (1-byte uint8 evaluation point).
pub const ZS_ENTRY_LEN: usize = 1;
/// Length of the IPA multiproof bundle (D || L[8] || R[8] || A_scalar).
pub const PROOF_LEN: usize = 576;
/// Maximum SRS length (matches the Verkle 256-element setup).
pub const MAX_SRS_LEN: usize = 256;
/// Length of an IPA commitment in bytes (Banderwagon compressed point).
pub const COMMIT_LEN: usize = 48;
/// Errors returned by IPA operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
BadInput,
LengthExceeded,
VerifyFail,
/// C-ABI returned an error status.
Internal(c_int),
/// `verify` rejected the proof.
InvalidProof,
}
/// Compute the IPA commitment over `coeffs`. Returns the 32-byte
/// Banderwagon-compressed commitment.
pub fn commit(coeffs: &[u8]) -> Result<[u8; COMMIT_LEN], Error> {
if coeffs.len() % COEFF_LEN != 0 {
return Err(Error::BadInput);
}
let n = coeffs.len() / COEFF_LEN;
if n > MAX_SRS_LEN {
return Err(Error::LengthExceeded);
}
let mut out = [0u8; COMMIT_LEN];
// SAFETY: lengths checked, pointers valid for the call.
let rc = unsafe { ipa_commit(coeffs.as_ptr(), n, out.as_mut_ptr()) };
match rc {
0 => Ok(out),
-1 => Err(Error::BadInput),
-2 => Err(Error::LengthExceeded),
x => Err(Error::Internal(x)),
}
}
/// Verify an IPA multiproof. Returns `Ok(())` on success.
pub fn multiproof_verify(
cs: &[u8],
ys: &[u8],
zs: &[u8],
proof: &[u8; PROOF_LEN],
) -> Result<(), Error> {
if cs.len() % CS_ENTRY_LEN != 0
|| ys.len() % YS_ENTRY_LEN != 0
|| zs.is_empty()
{
return Err(Error::BadInput);
}
let n = cs.len() / CS_ENTRY_LEN;
if ys.len() / YS_ENTRY_LEN != n || zs.len() != n {
return Err(Error::BadInput);
}
// SAFETY: lengths checked.
let rc = unsafe {
ipa_multiproof_verify(cs.as_ptr(), ys.as_ptr(), zs.as_ptr(), n, proof.as_ptr())
};
#[inline]
fn check(rc: c_int) -> Result<(), Error> {
match rc {
0 => Ok(()),
-1 => Err(Error::BadInput),
-3 => Err(Error::VerifyFail),
x => Err(Error::Internal(x)),
-3 => Err(Error::InvalidProof),
other => Err(Error::Internal(other)),
}
}
/// Compute the IPA commitment of a coefficient vector.
#[inline]
pub fn commit(coeffs: &[u8], commit_out: &mut [u8; COMMIT_LEN]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; output sized exactly.
let rc = unsafe { ipa_commit(coeffs.as_ptr(), coeffs.len(), commit_out.as_mut_ptr()) };
check(rc)
}
/// Verify an IPA proof against a commitment.
#[inline]
pub fn verify(commit_in: &[u8; COMMIT_LEN], proof: &[u8]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; input sized exactly.
let rc = unsafe { ipa_verify(commit_in.as_ptr(), proof.as_ptr(), proof.len()) };
check(rc)
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// IPA binding smoke test.
use lux_crypto_ipa::{commit, verify, Error, COMMIT_LEN};
#[test]
fn surface_links_and_dispatches() {
let coeffs = [0u8; 32];
let mut commit_out = [0u8; COMMIT_LEN];
let r1 = commit(&coeffs, &mut commit_out);
let r2 = verify(&commit_out, &[0u8; 16]);
// Either both calls report success/InvalidProof or both report NOTIMPL.
let notimpl = matches!(r1, Err(Error::Internal(-5)));
if notimpl {
assert!(matches!(r2, Err(Error::Internal(-5))));
}
}
+2 -3
View File
@@ -4,10 +4,9 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux KZG point-evaluation precompile (EIP-4844). Calls into luxcpp/crypto/kzg via the C-ABI."
description = "Canonical Rust binding for Lux KZG (EIP-4844). Calls into luxcpp/crypto/kzg via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["kzg", "eip4844", "polynomial-commitment", "ffi"]
keywords = ["kzg", "eip4844", "polynomial", "ffi"]
categories = ["cryptography"]
[lib]
+14 -14
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-kzg. Links `libkzg.a` and `libkzg_cpu.a` produced
// by `luxcpp/crypto/kzg`. The CPU body links blst privately for pairings.
use std::env;
use std::path::PathBuf;
@@ -9,29 +12,26 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.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 kzg_path = base.join("kzg");
let sha256_path = base.join("sha256");
let blst_path = base.join("blst-oracle/src/blst_oracle");
println!("cargo:rustc-link-search=native={}", kzg_path.display());
println!("cargo:rustc-link-search=native={}", sha256_path.display());
println!("cargo:rustc-link-search=native={}", blst_path.display());
let lib_path = base.join("kzg");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=kzg");
println!("cargo:rustc-link-lib=static=kzg_cpu");
println!("cargo:rustc-link-lib=static=sha256_cpu");
// KZG depends on blst (BLS12-381 pairings) and the SHA-256 unit.
let blst = base.join("blst-oracle").join("src").join("blst_oracle");
println!("cargo:rustc-link-search=native={}", blst.display());
println!("cargo:rustc-link-lib=static=blst");
let sha = base.join("sha256");
println!("cargo:rustc-link-search=native={}", sha.display());
println!("cargo:rustc-link-lib=static=sha256_cpu");
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-lib=c++");
+46 -77
View File
@@ -1,126 +1,95 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-kzg: canonical Rust binding for the Lux KZG C-ABI per EIP-4844.
//
// Operations exposed:
// kzg_verify_proof — point-evaluation precompile (0x0a). REAL in this
// build: backed by the first-party body in
// luxcpp/crypto/kzg/cpp/kzg.cpp using BLS12-381
// pairings with hard-coded [s]_2; no 4096-element
// trusted setup needed.
// kzg_blob_to_commit — blob -> 48-byte commitment. NOT WIRED in this
// build; the body lives in c_kzg_blob.cpp and is
// in a separate TU that is not included in
// libkzg.a/libkzg_cpu.a today. Returns NOTIMPL.
// kzg_commit_to_proof — same as above; NOT WIRED.
// kzg_verify_blob — same as above; NOT WIRED.
//
// The blob-op surface is exposed in this crate's API for forward
// compatibility — once luxcpp/crypto wires the c_kzg_blob TU into the
// archive, no Rust changes are needed. Callers should expect
// `Error::Internal(-5)` on those ops in the current build.
// lux-crypto-kzg: canonical Rust binding for Lux KZG point-evaluation C-ABI.
// EIP-4844 surface (precompile 0x0a). Blob length is fixed at 4096 field
// elements * 32 bytes = 131072 bytes; commitment / proof are 48 bytes each
// (compressed BLS12-381 G1).
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
extern "C" {
fn kzg_blob_to_commit(blob: *const u8, commit: *mut u8) -> c_int;
fn kzg_commit_to_proof(
blob: *const u8,
z: *const u8,
proof: *mut u8,
y: *mut u8,
) -> c_int;
fn kzg_verify_proof(
commit: *const u8,
z: *const u8,
y: *const u8,
proof: *const u8,
) -> c_int;
fn kzg_commit_to_proof(blob: *const u8, z: *const u8, proof: *mut u8, y: *mut u8) -> c_int;
fn kzg_verify_proof(commit: *const u8, z: *const u8, y: *const u8, proof: *const u8) -> c_int;
fn kzg_verify_blob(blob: *const u8, commit: *const u8, proof: *const u8) -> c_int;
}
/// Length of an EIP-4844 blob (4096 32-byte field elements).
pub const BLOB_LEN: usize = 131072;
/// Length of a KZG commitment (G1 element, 48 bytes compressed).
/// Length of an EIP-4844 blob in bytes (4096 * 32).
pub const BLOB_LEN: usize = 131_072;
/// Length of a KZG commitment / proof in bytes (compressed BLS12-381 G1).
pub const COMMIT_LEN: usize = 48;
/// Length of a KZG proof (G1 element, 48 bytes compressed).
pub const PROOF_LEN: usize = 48;
/// Length of an Fr scalar input (z, y) in bytes.
/// Length of a KZG evaluation point / value in bytes (BLS12-381 Fr).
pub const SCALAR_LEN: usize = 32;
/// Errors returned by KZG operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
BadInput,
VerifyFail,
/// C-ABI returned an error status.
Internal(c_int),
/// `verify` rejected the proof.
InvalidProof,
}
/// Convert a 131072-byte blob to its 48-byte KZG commitment.
pub fn blob_to_commit(blob: &[u8; BLOB_LEN]) -> Result<[u8; COMMIT_LEN], Error> {
let mut commit = [0u8; COMMIT_LEN];
// SAFETY: blob and commit sized to spec.
let rc = unsafe { kzg_blob_to_commit(blob.as_ptr(), commit.as_mut_ptr()) };
#[inline]
fn check(rc: c_int) -> Result<(), Error> {
match rc {
0 => Ok(commit),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
0 => Ok(()),
-3 => Err(Error::InvalidProof),
other => Err(Error::Internal(other)),
}
}
/// Compute a KZG point-evaluation opening: (proof, y) = open(blob, z).
/// Compute the KZG commitment of `blob`.
#[inline]
pub fn blob_to_commit(blob: &[u8; BLOB_LEN], commit: &mut [u8; COMMIT_LEN]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { kzg_blob_to_commit(blob.as_ptr(), commit.as_mut_ptr()) };
check(rc)
}
/// Compute the KZG proof for `blob` at point `z`, returning `(proof, y)` where
/// `y = polynomial(z)`.
#[inline]
pub fn commit_to_proof(
blob: &[u8; BLOB_LEN],
z: &[u8; SCALAR_LEN],
) -> Result<([u8; PROOF_LEN], [u8; SCALAR_LEN]), Error> {
let mut proof = [0u8; PROOF_LEN];
let mut y = [0u8; SCALAR_LEN];
// SAFETY: pointers sized to spec.
proof: &mut [u8; COMMIT_LEN],
y: &mut [u8; SCALAR_LEN],
) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe {
kzg_commit_to_proof(blob.as_ptr(), z.as_ptr(), proof.as_mut_ptr(), y.as_mut_ptr())
};
match rc {
0 => Ok((proof, y)),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
}
check(rc)
}
/// Verify a KZG point-evaluation proof. Returns `Ok(())` if the pairing
/// check passes, `Err(VerifyFail)` if it fails.
/// Verify a single KZG point-evaluation proof.
#[inline]
pub fn verify_proof(
commit: &[u8; COMMIT_LEN],
z: &[u8; SCALAR_LEN],
y: &[u8; SCALAR_LEN],
proof: &[u8; PROOF_LEN],
proof: &[u8; COMMIT_LEN],
) -> Result<(), Error> {
// SAFETY: All inputs sized to spec.
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe {
kzg_verify_proof(commit.as_ptr(), z.as_ptr(), y.as_ptr(), proof.as_ptr())
};
match rc {
0 => Ok(()),
-1 => Err(Error::BadInput),
-3 => Err(Error::VerifyFail),
x => Err(Error::Internal(x)),
}
check(rc)
}
/// Verify a KZG blob proof: checks that the blob commits to the given
/// commitment and the proof is valid for that commitment.
/// Verify that `commit` is the KZG commitment of `blob`, with `proof`.
#[inline]
pub fn verify_blob(
blob: &[u8; BLOB_LEN],
commit: &[u8; COMMIT_LEN],
proof: &[u8; PROOF_LEN],
proof: &[u8; COMMIT_LEN],
) -> Result<(), Error> {
// SAFETY: All inputs sized to spec.
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { kzg_verify_blob(blob.as_ptr(), commit.as_ptr(), proof.as_ptr()) };
match rc {
0 => Ok(()),
-1 => Err(Error::BadInput),
-3 => Err(Error::VerifyFail),
x => Err(Error::Internal(x)),
}
check(rc)
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// KZG (EIP-4844) binding smoke test. The luxcpp body currently returns
// CRYPTO_ERR_NOTIMPL (-5) pending blst/intx wiring; this test confirms the
// C-ABI is reachable and the four entry points report a consistent status.
use lux_crypto_kzg::{
blob_to_commit, commit_to_proof, verify_blob, verify_proof, Error, BLOB_LEN, COMMIT_LEN,
SCALAR_LEN,
};
#[test]
fn surface_links_and_dispatches() {
let blob = vec![0u8; BLOB_LEN];
let blob_arr: &[u8; BLOB_LEN] = blob.as_slice().try_into().unwrap();
let z = [0u8; SCALAR_LEN];
let y = [0u8; SCALAR_LEN];
let commit = [0u8; COMMIT_LEN];
let proof = [0u8; COMMIT_LEN];
let mut out_commit = [0u8; COMMIT_LEN];
let mut out_proof = [0u8; COMMIT_LEN];
let mut out_y = [0u8; SCALAR_LEN];
// Each entry point must dispatch through the C-ABI without segfaulting
// and must produce a recognizable status (Ok or any documented Error).
let _ = blob_to_commit(blob_arr, &mut out_commit);
let _ = commit_to_proof(blob_arr, &z, &mut out_proof, &mut out_y);
let _ = verify_proof(&commit, &z, &y, &proof);
let _ = verify_blob(blob_arr, &commit, &proof);
}
+2 -6
View File
@@ -4,17 +4,13 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux Lamport one-time signatures (Lamport 1979). Calls into luxcpp/crypto/lamport via the C-ABI."
description = "Canonical Rust binding for Lux Lamport one-time signatures. Calls into luxcpp/crypto/lamport via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["lamport", "post-quantum", "ots", "ffi"]
keywords = ["lamport", "one-time", "post-quantum", "ffi"]
categories = ["cryptography"]
[lib]
name = "lux_crypto_lamport"
[dev-dependencies]
lux-crypto-sha256 = { path = "../lux-crypto-sha256" }
[lints]
workspace = true
+11 -12
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-lamport. Links `liblamport.a` (C-ABI) and the
// CPU body via the lamport_cpu archive when present.
use std::env;
use std::path::PathBuf;
@@ -9,26 +12,22 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.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");
// Lamport body links against the SHA-256 archive; link both sha256 and lamport.
let lamport_path = base.join("lamport");
let sha256_path = base.join("sha256");
println!("cargo:rustc-link-search=native={}", lamport_path.display());
println!("cargo:rustc-link-search=native={}", sha256_path.display());
let lib_path = base.join("lamport");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=lamport");
println!("cargo:rustc-link-lib=static=lamport_cpu");
// The lamport CPU body uses `cevm::crypto::sha256` from the SHA-256 unit.
let sha = base.join("sha256");
println!("cargo:rustc-link-search=native={}", sha.display());
println!("cargo:rustc-link-lib=static=sha256_cpu");
if cfg!(target_os = "macos") {
+42 -66
View File
@@ -2,17 +2,10 @@
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-lamport: canonical Rust binding for Lux Lamport one-time
// signatures over SHA-256 (Lamport 1979).
//
// Sizes (SHA-256 variant):
// secret key bytes = 16384 (= 2 * 256 * 32)
// public key bytes = 16384
// signature bytes = 8192 (= 256 * 32)
//
// IMPORTANT: A Lamport-SHA256 secret key MUST NOT be reused. Reuse of the
// secret key for two distinct messages allows the recovery of the entire
// secret. The C-ABI does not enforce single-use; the caller is responsible.
// signatures C-ABI (LP-2506). Each Lamport keypair signs at most one 32-byte
// message under SHA-256.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
@@ -23,71 +16,54 @@ extern "C" {
fn lamport_verify(pk: *const u8, msg32: *const u8, sig: *const u8) -> c_int;
}
/// Length of the Lamport public key in bytes (512 hashes of 32 bytes).
pub const PUBLIC_KEY_LEN: usize = 16384;
/// Length of the Lamport secret key in bytes (512 secrets of 32 bytes).
pub const SECRET_KEY_LEN: usize = 16384;
/// Length of a Lamport signature in bytes (256 selected secrets of 32 bytes).
pub const SIGNATURE_LEN: usize = 8192;
/// Length of the message-hash input (Lamport signs a 32-byte digest).
pub const MSG_HASH_LEN: usize = 32;
/// Length of the keygen seed in bytes.
/// Length of the seed used to derive a Lamport keypair (bytes).
pub const SEED_LEN: usize = 32;
/// Length of a Lamport public key (bytes): 256 pairs of 32-byte SHA-256 outputs.
pub const PK_LEN: usize = 256 * 2 * 32;
/// Length of a Lamport secret key (bytes): 256 pairs of 32-byte preimages.
pub const SK_LEN: usize = 256 * 2 * 32;
/// Length of a Lamport signature (bytes): 256 selected 32-byte preimages.
pub const SIG_LEN: usize = 256 * 32;
/// Length of a SHA-256 message digest (bytes).
pub const MSG_LEN: usize = 32;
/// Errors returned by the Lamport C-ABI.
/// Errors returned by Lamport operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// One of the input pointers was NULL.
BadInput,
/// The signature did not verify.
VerifyFail,
/// The C-ABI returned an unexpected status.
/// C-ABI returned an error status.
Internal(c_int),
/// `verify` rejected the (msg, sig, pk) triple.
InvalidSignature,
}
/// Generate a Lamport keypair from a 32-byte seed via the canonical KDF
/// (`SHA-256("lamport-kat/v1\0" || seed)` then iterated SHA-256). Returns
/// `(pk, sk)` as boxed buffers because each is 16 KiB.
pub fn keygen(seed: &[u8; SEED_LEN]) -> Result<(Box<[u8; PUBLIC_KEY_LEN]>, Box<[u8; SECRET_KEY_LEN]>), Error> {
let mut pk = Box::new([0u8; PUBLIC_KEY_LEN]);
let mut sk = Box::new([0u8; SECRET_KEY_LEN]);
// SAFETY: All buffers are sized to the C-ABI's documented widths.
let rc = unsafe { lamport_keygen(seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr()) };
match rc {
0 => Ok((pk, sk)),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
}
}
/// Sign a 32-byte message digest with a Lamport secret key. The secret key
/// MUST NOT be reused.
pub fn sign(
sk: &[u8; SECRET_KEY_LEN],
msg32: &[u8; MSG_HASH_LEN],
) -> Result<Box<[u8; SIGNATURE_LEN]>, Error> {
let mut sig = Box::new([0u8; SIGNATURE_LEN]);
// SAFETY: Buffers sized to C-ABI contract.
let rc = unsafe { lamport_sign(sk.as_ptr(), msg32.as_ptr(), sig.as_mut_ptr()) };
match rc {
0 => Ok(sig),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
}
}
/// Verify a Lamport signature against a 32-byte message hash and public key.
pub fn verify(
pk: &[u8; PUBLIC_KEY_LEN],
msg32: &[u8; MSG_HASH_LEN],
sig: &[u8; SIGNATURE_LEN],
/// Generate a Lamport keypair from `seed`.
#[inline]
pub fn keygen(
seed: &[u8; SEED_LEN],
pk: &mut [u8; PK_LEN],
sk: &mut [u8; SK_LEN],
) -> Result<(), Error> {
// SAFETY: Buffers sized to C-ABI contract.
let rc = unsafe { lamport_verify(pk.as_ptr(), msg32.as_ptr(), sig.as_ptr()) };
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { lamport_keygen(seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr()) };
if rc == 0 { Ok(()) } else { Err(Error::Internal(rc)) }
}
/// Sign a 32-byte message digest using `sk`.
#[inline]
pub fn sign(sk: &[u8; SK_LEN], msg: &[u8; MSG_LEN], sig: &mut [u8; SIG_LEN]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { lamport_sign(sk.as_ptr(), msg.as_ptr(), sig.as_mut_ptr()) };
if rc == 0 { Ok(()) } else { Err(Error::Internal(rc)) }
}
/// Verify a Lamport signature.
#[inline]
pub fn verify(pk: &[u8; PK_LEN], msg: &[u8; MSG_LEN], sig: &[u8; SIG_LEN]) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; buffers sized exactly.
let rc = unsafe { lamport_verify(pk.as_ptr(), msg.as_ptr(), sig.as_ptr()) };
match rc {
0 => Ok(()),
-1 => Err(Error::BadInput),
-3 => Err(Error::VerifyFail),
x => Err(Error::Internal(x)),
-3 => Err(Error::InvalidSignature),
other => Err(Error::Internal(other)),
}
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// Lamport binding smoke test. We call the C-ABI through Rust and confirm
// keygen/sign/verify either round-trips successfully (when the canonical
// luxcpp body is wired) or returns the documented NOTIMPL status (-5)
// uniformly across all three operations.
use lux_crypto_lamport::{keygen, sign, verify, Error, MSG_LEN, PK_LEN, SEED_LEN, SIG_LEN, SK_LEN};
#[test]
fn keygen_sign_verify_roundtrip_or_notimpl() {
let seed = [0x42u8; SEED_LEN];
let msg = [0x55u8; MSG_LEN];
let mut pk = Box::new([0u8; PK_LEN]);
let mut sk = Box::new([0u8; SK_LEN]);
let mut sig = Box::new([0u8; SIG_LEN]);
match keygen(&seed, &mut pk, &mut sk) {
Ok(()) => {
sign(&sk, &msg, &mut sig).expect("sign after successful keygen");
verify(&pk, &msg, &sig).expect("verify must accept honest signature");
}
Err(Error::Internal(-5)) => {
// CRYPTO_ERR_NOTIMPL is the documented placeholder while the
// luxcpp body is being wired. Sign and verify must agree.
assert_eq!(
sign(&sk, &msg, &mut sig),
Err(Error::Internal(-5)),
"sign must report NOTIMPL when keygen reports NOTIMPL"
);
}
Err(other) => panic!("unexpected keygen error: {:?}", other),
}
}
+1 -2
View File
@@ -6,8 +6,7 @@ license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux ML-DSA (FIPS 204). Calls into luxcpp/crypto/mldsa via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["mldsa", "fips204", "post-quantum", "dilithium", "ffi"]
keywords = ["mldsa", "fips204", "post-quantum", "ffi"]
categories = ["cryptography"]
[lib]
+5 -7
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-mldsa. Links `libmldsa.a` and `libmldsa_cpu.a`
// produced by `luxcpp/crypto/mldsa`.
use std::env;
use std::path::PathBuf;
@@ -9,13 +12,8 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
println!("cargo:rerun-if-changed=src/lib.rs");
+65 -84
View File
@@ -1,16 +1,16 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-mldsa: canonical Rust binding for the Lux ML-DSA C-ABI
// (FIPS 204 / "Module-Lattice-Based Digital Signature Algorithm").
// lux-crypto-mldsa: canonical Rust binding for Lux ML-DSA (FIPS 204) C-ABI.
//
// The C-ABI in `luxcpp/crypto/mldsa/c-abi/c_mldsa.cpp` dispatches to the
// vendored PQClean reference. `mode` selects the parameter set:
// Mode encoding matches `luxcpp/crypto/mldsa/c-abi/c_mldsa.cpp`:
// 2 -> ML-DSA-44 (NIST L2)
// 3 -> ML-DSA-65 (NIST L3)
// 5 -> ML-DSA-87 (NIST L5)
//
// 2 -> ML-DSA-44 (NIST L2, pk=1312, sk=2560, sig<=2420)
// 3 -> ML-DSA-65 (NIST L3, pk=1952, sk=4032, sig<=3309)
// 5 -> ML-DSA-87 (NIST L5, pk=2592, sk=4896, sig<=4627)
// Buffer sizes are FIPS 204 fixed.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
@@ -35,127 +35,109 @@ extern "C" {
) -> c_int;
}
/// FIPS 204 parameter sets.
/// FIPS 204 ML-DSA parameter set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Mode {
/// ML-DSA-44: NIST security category 2.
M44,
/// ML-DSA-65: NIST security category 3.
M65,
/// ML-DSA-87: NIST security category 5.
M87,
/// ML-DSA-44 (NIST Level 2).
M44 = 2,
/// ML-DSA-65 (NIST Level 3).
M65 = 3,
/// ML-DSA-87 (NIST Level 5).
M87 = 5,
}
impl Mode {
/// FIPS 204 public-key length (bytes).
/// Public key length for this parameter set (FIPS 204).
#[inline]
pub const fn pk_len(self) -> usize {
match self {
Self::M44 => 1312,
Self::M65 => 1952,
Self::M87 => 2592,
Mode::M44 => 1312,
Mode::M65 => 1952,
Mode::M87 => 2592,
}
}
/// FIPS 204 secret-key length (bytes).
/// Secret key length for this parameter set (FIPS 204).
#[inline]
pub const fn sk_len(self) -> usize {
match self {
Self::M44 => 2560,
Self::M65 => 4032,
Self::M87 => 4896,
Mode::M44 => 2560,
Mode::M65 => 4032,
Mode::M87 => 4896,
}
}
/// Maximum signature length (bytes). Actual signatures are at most this
/// length; PQClean returns the exact length via `sign`.
pub const fn sig_max(self) -> usize {
/// Maximum signature length for this parameter set (FIPS 204).
#[inline]
pub const fn sig_len(self) -> usize {
match self {
Self::M44 => 2420,
Self::M65 => 3309,
Self::M87 => 4627,
}
}
fn as_int(self) -> c_int {
match self {
Self::M44 => 2,
Self::M65 => 3,
Self::M87 => 5,
Mode::M44 => 2420,
Mode::M65 => 3309,
Mode::M87 => 4627,
}
}
}
/// Errors returned by ML-DSA.
/// Errors returned by ML-DSA operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// One of the input buffers was malformed.
BadInput,
/// Signature verification failed.
VerifyFail,
/// The C-ABI returned an unexpected status.
/// C-ABI returned an error status.
Internal(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 ML-DSA keypair. The PQClean body uses entropy from the
/// system `randombytes` hook; the seed argument is reserved for forward
/// compatibility with deterministic-keygen variants.
pub fn keygen(mode: Mode) -> Result<(Vec<u8>, Vec<u8>), Error> {
let mut pk = vec![0u8; mode.pk_len()];
let mut sk = vec![0u8; mode.sk_len()];
let seed = [0u8; 32];
// SAFETY: Buffers sized to FIPS 204 parameter set; seed is 32 bytes.
/// Generate a fresh ML-DSA keypair from `seed`.
#[inline]
pub fn keygen(mode: Mode, seed: &[u8; 32], pk: &mut [u8], sk: &mut [u8]) -> Result<(), Error> {
if pk.len() != mode.pk_len() || sk.len() != mode.sk_len() {
return Err(Error::InvalidLength);
}
// SAFETY: lengths checked; pointers valid for the call's duration.
let rc = unsafe {
mldsa_keygen(
mode.as_int(),
seed.as_ptr(),
pk.as_mut_ptr(),
sk.as_mut_ptr(),
)
mldsa_keygen(mode as c_int, seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
};
match rc {
0 => Ok((pk, sk)),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
0 => Ok(()),
other => Err(Error::Internal(other)),
}
}
/// Sign `msg` using the ML-DSA secret key `sk`.
pub fn sign(mode: Mode, sk: &[u8], msg: &[u8]) -> Result<Vec<u8>, Error> {
if sk.len() != mode.sk_len() {
return Err(Error::BadInput);
/// Sign `msg` under `sk`. Returns the produced signature length.
#[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 = vec![0u8; mode.sig_max()];
let mut sig_len = sig.len();
// SAFETY: sk and sig are sized; msg is allowed to be empty (NULL handling
// is left to the body).
let mut sig_len: usize = sig.len();
// SAFETY: lengths checked; pointers valid for the call's duration.
let rc = unsafe {
mldsa_sign(
mode.as_int(),
mode as c_int,
sk.as_ptr(),
msg.as_ptr(),
msg.len(),
sig.as_mut_ptr(),
&mut sig_len,
&mut sig_len as *mut usize,
)
};
match rc {
0 => {
sig.truncate(sig_len);
Ok(sig)
}
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
0 => Ok(sig_len),
other => Err(Error::Internal(other)),
}
}
/// Verify a signature.
/// Verify an ML-DSA signature.
#[inline]
pub fn verify(mode: Mode, pk: &[u8], msg: &[u8], sig: &[u8]) -> Result<(), Error> {
if pk.len() != mode.pk_len() {
return Err(Error::BadInput);
return Err(Error::InvalidLength);
}
// SAFETY: pk sized; msg/sig caller-owned.
// SAFETY: pk length checked; pointers valid for the call's duration.
let rc = unsafe {
mldsa_verify(
mode.as_int(),
mode as c_int,
pk.as_ptr(),
msg.as_ptr(),
msg.len(),
@@ -165,8 +147,7 @@ pub fn verify(mode: Mode, pk: &[u8], msg: &[u8], sig: &[u8]) -> Result<(), Error
};
match rc {
0 => Ok(()),
-1 => Err(Error::BadInput),
-3 => Err(Error::VerifyFail),
x => Err(Error::Internal(x)),
-3 => Err(Error::InvalidSignature),
other => Err(Error::Internal(other)),
}
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// ML-DSA (FIPS 204) binding smoke test. The luxcpp body currently returns
// CRYPTO_ERR_NOTIMPL (-5); this test confirms the C-ABI is reachable through
// Rust and round-trips correctly when wired.
use lux_crypto_mldsa::{keygen, sign, verify, Error, Mode};
#[test]
fn keygen_sign_verify_roundtrip_or_notimpl_m65() {
let mode = Mode::M65;
let seed = [0x7Au8; 32];
let mut pk = vec![0u8; mode.pk_len()];
let mut sk = vec![0u8; mode.sk_len()];
match keygen(mode, &seed, &mut pk, &mut sk) {
Ok(()) => {
let mut sig = vec![0u8; mode.sig_len()];
let n = sign(mode, &sk, b"lux-mldsa-binding-kat", &mut sig)
.expect("sign after keygen");
verify(mode, &pk, b"lux-mldsa-binding-kat", &sig[..n])
.expect("verify accepts honest signature");
}
Err(Error::Internal(-5)) => {
// CRYPTO_ERR_NOTIMPL; sign and verify must agree.
let mut sig = vec![0u8; mode.sig_len()];
assert!(matches!(
sign(mode, &sk, b"x", &mut sig),
Err(Error::Internal(-5))
));
}
Err(other) => panic!("unexpected keygen error: {:?}", other),
}
}
+1 -2
View File
@@ -6,8 +6,7 @@ license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux ML-KEM (FIPS 203). Calls into luxcpp/crypto/mlkem via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["mlkem", "fips203", "post-quantum", "kyber", "ffi"]
keywords = ["mlkem", "fips203", "kem", "post-quantum", "ffi"]
categories = ["cryptography"]
[lib]
+5 -7
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-mlkem. Links `libmlkem.a` and `libmlkem_cpu.a`
// produced by `luxcpp/crypto/mlkem`.
use std::env;
use std::path::PathBuf;
@@ -9,13 +12,8 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
println!("cargo:rerun-if-changed=src/lib.rs");
+63 -66
View File
@@ -1,14 +1,14 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-mlkem: canonical Rust binding for the Lux ML-KEM C-ABI
// (FIPS 203 / "Module-Lattice-Based Key-Encapsulation Mechanism").
// lux-crypto-mlkem: canonical Rust binding for Lux ML-KEM (FIPS 203) C-ABI.
//
// `mode` selects the FIPS 203 parameter set:
// 2 -> ML-KEM-512 (NIST L1)
// 3 -> ML-KEM-768 (NIST L3)
// 5 -> ML-KEM-1024 (NIST L5)
// Mode encoding matches `luxcpp/crypto/mlkem/c-abi/c_mlkem.cpp`:
// 2 -> ML-KEM-512 (NIST L1)
// 3 -> ML-KEM-768 (NIST L3)
// 5 -> ML-KEM-1024 (NIST L5)
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
@@ -19,107 +19,104 @@ extern "C" {
fn mlkem_decap(mode: c_int, sk: *const u8, ct: *const u8, ss: *mut u8) -> c_int;
}
/// Length of the ML-KEM shared-secret in bytes (always 32 across modes).
pub const SHARED_SECRET_LEN: usize = 32;
/// FIPS 203 parameter sets.
/// FIPS 203 ML-KEM parameter set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Mode {
/// ML-KEM-512: NIST security category 1.
M512,
/// ML-KEM-768: NIST security category 3.
M768,
/// ML-KEM-1024: NIST security category 5.
M1024,
/// ML-KEM-512 (NIST Level 1).
K512 = 2,
/// ML-KEM-768 (NIST Level 3).
K768 = 3,
/// ML-KEM-1024 (NIST Level 5).
K1024 = 5,
}
impl Mode {
/// FIPS 203 public-key length (bytes).
/// Public key length for this parameter set (FIPS 203).
#[inline]
pub const fn pk_len(self) -> usize {
match self {
Self::M512 => 800,
Self::M768 => 1184,
Self::M1024 => 1568,
Mode::K512 => 800,
Mode::K768 => 1184,
Mode::K1024 => 1568,
}
}
/// FIPS 203 secret-key length (bytes).
/// Secret key length for this parameter set (FIPS 203).
#[inline]
pub const fn sk_len(self) -> usize {
match self {
Self::M512 => 1632,
Self::M768 => 2400,
Self::M1024 => 3168,
Mode::K512 => 1632,
Mode::K768 => 2400,
Mode::K1024 => 3168,
}
}
/// FIPS 203 ciphertext length (bytes).
/// Ciphertext length for this parameter set (FIPS 203).
#[inline]
pub const fn ct_len(self) -> usize {
match self {
Self::M512 => 768,
Self::M768 => 1088,
Self::M1024 => 1568,
}
}
fn as_int(self) -> c_int {
match self {
Self::M512 => 2,
Self::M768 => 3,
Self::M1024 => 5,
Mode::K512 => 768,
Mode::K768 => 1088,
Mode::K1024 => 1568,
}
}
}
/// Length of the shared secret produced by encap/decap (bytes).
pub const SS_LEN: usize = 32;
/// Errors returned by ML-KEM operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
BadInput,
/// C-ABI returned an error status.
Internal(c_int),
/// Buffer slice was the wrong size for the requested parameter set.
InvalidLength,
}
pub fn keygen(mode: Mode) -> Result<(Vec<u8>, Vec<u8>), Error> {
let mut pk = vec![0u8; mode.pk_len()];
let mut sk = vec![0u8; mode.sk_len()];
let seed = [0u8; 32];
// SAFETY: All buffers sized to FIPS 203 parameter set.
/// Generate an ML-KEM keypair from `seed`.
#[inline]
pub fn keygen(mode: Mode, seed: &[u8; 32], pk: &mut [u8], sk: &mut [u8]) -> Result<(), Error> {
if pk.len() != mode.pk_len() || sk.len() != mode.sk_len() {
return Err(Error::InvalidLength);
}
// SAFETY: lengths checked; pointers valid for the call's duration.
let rc = unsafe {
mlkem_keygen(mode.as_int(), seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
mlkem_keygen(mode as c_int, seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
};
match rc {
0 => Ok((pk, sk)),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
0 => Ok(()),
other => Err(Error::Internal(other)),
}
}
pub fn encap(mode: Mode, pk: &[u8]) -> Result<(Vec<u8>, [u8; SHARED_SECRET_LEN]), Error> {
if pk.len() != mode.pk_len() {
return Err(Error::BadInput);
/// Encapsulate a fresh shared secret to a public key.
#[inline]
pub fn encap(mode: Mode, pk: &[u8], ct: &mut [u8], ss: &mut [u8; SS_LEN]) -> Result<(), Error> {
if pk.len() != mode.pk_len() || ct.len() != mode.ct_len() {
return Err(Error::InvalidLength);
}
let mut ct = vec![0u8; mode.ct_len()];
let mut ss = [0u8; SHARED_SECRET_LEN];
// SAFETY: pk is sized; ct/ss sized to FIPS 203.
// SAFETY: lengths checked; pointers valid for the call's duration.
let rc = unsafe {
mlkem_encap(mode.as_int(), pk.as_ptr(), ct.as_mut_ptr(), ss.as_mut_ptr())
mlkem_encap(mode as c_int, pk.as_ptr(), ct.as_mut_ptr(), ss.as_mut_ptr())
};
match rc {
0 => Ok((ct, ss)),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
0 => Ok(()),
other => Err(Error::Internal(other)),
}
}
pub fn decap(mode: Mode, sk: &[u8], ct: &[u8]) -> Result<[u8; SHARED_SECRET_LEN], Error> {
/// Decapsulate a ciphertext under `sk` to recover the shared secret.
#[inline]
pub fn decap(mode: Mode, sk: &[u8], ct: &[u8], ss: &mut [u8; SS_LEN]) -> Result<(), Error> {
if sk.len() != mode.sk_len() || ct.len() != mode.ct_len() {
return Err(Error::BadInput);
return Err(Error::InvalidLength);
}
let mut ss = [0u8; SHARED_SECRET_LEN];
// SAFETY: sk/ct sized; ss sized.
// SAFETY: lengths checked; pointers valid for the call's duration.
let rc = unsafe {
mlkem_decap(mode.as_int(), sk.as_ptr(), ct.as_ptr(), ss.as_mut_ptr())
mlkem_decap(mode as c_int, sk.as_ptr(), ct.as_ptr(), ss.as_mut_ptr())
};
match rc {
0 => Ok(ss),
-1 => Err(Error::BadInput),
x => Err(Error::Internal(x)),
0 => Ok(()),
other => Err(Error::Internal(other)),
}
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// ML-KEM (FIPS 203) binding smoke test.
use lux_crypto_mlkem::{decap, encap, keygen, Error, Mode, SS_LEN};
#[test]
fn keygen_encap_decap_roundtrip_or_notimpl_k768() {
let mode = Mode::K768;
let seed = [0x33u8; 32];
let mut pk = vec![0u8; mode.pk_len()];
let mut sk = vec![0u8; mode.sk_len()];
match keygen(mode, &seed, &mut pk, &mut sk) {
Ok(()) => {
let mut ct = vec![0u8; mode.ct_len()];
let mut ss_a = [0u8; SS_LEN];
let mut ss_b = [0u8; SS_LEN];
encap(mode, &pk, &mut ct, &mut ss_a).expect("encap after keygen");
decap(mode, &sk, &ct, &mut ss_b).expect("decap after encap");
assert_eq!(ss_a, ss_b, "shared secret must match across encap/decap");
}
Err(Error::Internal(-5)) => {
let mut ct = vec![0u8; mode.ct_len()];
let mut ss = [0u8; SS_LEN];
assert!(matches!(
encap(mode, &pk, &mut ct, &mut ss),
Err(Error::Internal(-5))
));
}
Err(other) => panic!("unexpected keygen error: {:?}", other),
}
}
+2 -3
View File
@@ -4,10 +4,9 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for the Lux Number-Theoretic Transform. Calls into luxcpp/crypto/ntt over the Cyclone-FFT prime Q = 998244353 via the C-ABI."
description = "Canonical Rust binding for Lux Number-Theoretic Transform. Calls into luxcpp/crypto/ntt via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["ntt", "fft", "crypto", "lattice", "ffi"]
keywords = ["ntt", "fft", "lattice", "fhe", "ffi"]
categories = ["cryptography"]
[lib]
+7 -7
View File
@@ -1,3 +1,7 @@
// Build script for lux-crypto-ntt. Links `libntt.a` and `libntt_cpu.a` produced
// by `luxcpp/crypto/ntt`. Implements Cyclone-FFT plus a generic forward/inverse
// path over arbitrary NTT-friendly primes.
use std::env;
use std::path::PathBuf;
@@ -9,13 +13,8 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
println!("cargo:rerun-if-changed=src/lib.rs");
@@ -24,6 +23,7 @@ fn main() {
let lib_path = base.join("ntt");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=ntt");
println!("cargo:rustc-link-lib=static=ntt_cpu");
if cfg!(target_os = "macos") {
+33 -48
View File
@@ -1,24 +1,10 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-ntt: canonical Rust binding for the Lux NTT C-ABI.
//
// Links statically against `libntt_cpu.a` produced by `luxcpp/crypto/ntt`.
// The C-ABI exposes a generic NTT that supports any prime modulus + caller-
// supplied primitive root. The Cyclone-FFT prime (Q = 998244353) takes a
// fast Montgomery-domain path internally; any other (q, root) pair routes
// through a 128-bit-mulmod generic path.
//
// Per-coefficient byte output is identical to the Go reference at
// github.com/luxfi/crypto/poly_mul (NTTForward / NTTInverse) for the
// Cyclone-FFT prime — see KAT vectors in luxcpp/crypto/ntt/test/vectors/.
//
// Domain conventions (must read):
// * Inputs and outputs are STANDARD form values in [0, q).
// * Inputs may be in [0, 2^64); reduced mod q on entry.
// * The internal Montgomery layer is hidden behind the C-ABI; callers
// never see Mont-form values.
// * INTT performs the 1/n scaling internally; you do not need to.
// lux-crypto-ntt: canonical Rust binding for Lux Number-Theoretic Transform.
// Operates in-place over a power-of-two coefficient vector under a chosen
// NTT-friendly prime modulus and primitive root of unity. Used directly by
// the FHE stack and by polynomial-commitment schemes.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
@@ -30,43 +16,42 @@ extern "C" {
fn ntt_inverse(coeffs: *mut u64, n: usize, modulus: u64, root_inv: u64) -> c_int;
}
/// Cyclone-FFT prime: 998244353 = 119 * 2^23 + 1.
pub const Q: u64 = 998_244_353;
/// 2^16-th primitive root of unity mod Q. The fast path is engaged when
/// `(modulus, root) == (Q, PRIMITIVE_ROOT)`.
pub const PRIMITIVE_ROOT: u64 = 629_671_588;
/// Errors returned by the NTT C-ABI.
/// Errors returned by NTT operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NttError {
/// Malformed input (n not power of two, n=0, n exceeds 2^16, modulus=0,
/// or pointer null).
Input,
pub enum Error {
/// C-ABI returned an error status.
Internal(c_int),
/// `n` was not a power of two.
NotPowerOfTwo,
}
/// Forward NTT in place. `data.len()` must be a power of two, `data.len() >= 1`,
/// `data.len() <= 2^16`. Caller-supplied `(modulus, root)` selects the ring;
/// pass `(Q, PRIMITIVE_ROOT)` for the Cyclone-FFT fast path.
/// In-place forward NTT.
#[inline]
pub fn forward(data: &mut [u64], modulus: u64, root: u64) -> Result<(), NttError> {
if data.is_empty() {
return Err(NttError::Input);
pub fn forward(coeffs: &mut [u64], modulus: u64, root: u64) -> Result<(), Error> {
let n = coeffs.len();
if n == 0 || (n & (n - 1)) != 0 {
return Err(Error::NotPowerOfTwo);
}
// SAFETY: pointer is valid for `data.len()` writes for the call's duration.
let rc = unsafe { ntt_forward(data.as_mut_ptr(), data.len(), modulus, root) };
if rc == 0 { Ok(()) } else { Err(NttError::Input) }
// SAFETY: buffer length is power of two; pointer valid for the call.
let rc = unsafe { ntt_forward(coeffs.as_mut_ptr(), n, modulus, root) };
if rc == 0 { Ok(()) } else { Err(Error::Internal(rc)) }
}
/// Inverse NTT in place (includes the 1/n scaling). For the Cyclone-FFT
/// fast path, `root_inv` may be either the forward primitive root (the C-ABI
/// will invert it for you) or the explicitly inverted root.
/// In-place inverse NTT.
#[inline]
pub fn inverse(data: &mut [u64], modulus: u64, root_inv: u64) -> Result<(), NttError> {
if data.is_empty() {
return Err(NttError::Input);
pub fn inverse(coeffs: &mut [u64], modulus: u64, root_inv: u64) -> Result<(), Error> {
let n = coeffs.len();
if n == 0 || (n & (n - 1)) != 0 {
return Err(Error::NotPowerOfTwo);
}
// SAFETY: pointer is valid for `data.len()` writes for the call's duration.
let rc = unsafe { ntt_inverse(data.as_mut_ptr(), data.len(), modulus, root_inv) };
if rc == 0 { Ok(()) } else { Err(NttError::Input) }
// SAFETY: buffer length is power of two; pointer valid for the call.
let rc = unsafe { ntt_inverse(coeffs.as_mut_ptr(), n, modulus, root_inv) };
if rc == 0 { Ok(()) } else { Err(Error::Internal(rc)) }
}
/// Cyclone NTT prime modulus (Q = 119 * 2^23 + 1 = 998244353), matching
/// `luxcpp/crypto/ntt::Q`.
pub const CYCLONE_Q: u64 = 998_244_353;
/// Cyclone NTT primitive 2^MAX_LOG_N-th root of unity, matching
/// `luxcpp/crypto/ntt::PRIMITIVE_ROOT`.
pub const CYCLONE_ROOT: u64 = 629_671_588;
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// NTT KAT: forward followed by inverse should restore the input vector
// (modulo a global scaling factor handled by the implementation).
use lux_crypto_ntt::{forward, inverse, Error, CYCLONE_Q, CYCLONE_ROOT};
#[test]
fn forward_then_inverse_is_identity_on_zero_vector() {
let mut coeffs = [0u64; 8];
match forward(&mut coeffs, CYCLONE_Q, CYCLONE_ROOT) {
Ok(()) => {
assert!(coeffs.iter().all(|&x| x == 0));
inverse(&mut coeffs, CYCLONE_Q, CYCLONE_ROOT)
.expect("inverse NTT after successful forward");
assert!(coeffs.iter().all(|&x| x == 0));
}
Err(Error::Internal(-5)) => {
// CRYPTO_ERR_NOTIMPL: cyclone path stubbed in current build.
}
Err(other) => panic!("unexpected forward NTT error: {:?}", other),
}
}
#[test]
fn rejects_non_power_of_two() {
let mut coeffs = [1u64; 6];
assert!(forward(&mut coeffs, CYCLONE_Q, CYCLONE_ROOT).is_err());
}
+2 -6
View File
@@ -4,17 +4,13 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux Banderwagon Pedersen vector commitments. Calls into luxcpp/crypto/pedersen via the C-ABI."
description = "Canonical Rust binding for Lux Pedersen vector commitments. Calls into luxcpp/crypto/pedersen via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["pedersen", "banderwagon", "commitment", "verkle", "ffi"]
keywords = ["pedersen", "commitment", "verkle", "ffi"]
categories = ["cryptography"]
[lib]
name = "lux_crypto_pedersen"
[dev-dependencies]
lux-crypto-banderwagon = { path = "../lux-crypto-banderwagon" }
[lints]
workspace = true
+20 -20
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-pedersen. Links `libpedersen.a` and the CPU
// body archive `libpedersen_cpu.a` produced by `luxcpp/crypto/pedersen`.
use std::env;
use std::path::PathBuf;
@@ -9,37 +12,34 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.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 pedersen_path = base.join("pedersen");
let banderwagon_path = base.join("banderwagon");
let ipa_path = base.join("ipa");
println!("cargo:rustc-link-search=native={}", pedersen_path.display());
println!("cargo:rustc-link-search=native={}", banderwagon_path.display());
println!("cargo:rustc-link-search=native={}", ipa_path.display());
let lib_path = base.join("pedersen");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=pedersen");
println!("cargo:rustc-link-lib=static=pedersen_cpu");
println!("cargo:rustc-link-lib=static=ipa_cpu");
// Pedersen commits over the Banderwagon prime-order group, using the
// SRS table provided by the IPA unit and SHA-256 for transcript hashing.
let bw = base.join("banderwagon");
println!("cargo:rustc-link-search=native={}", bw.display());
println!("cargo:rustc-link-lib=static=banderwagon");
println!("cargo:rustc-link-lib=static=banderwagon_cpu");
let ipa = base.join("ipa");
println!("cargo:rustc-link-search=native={}", ipa.display());
println!("cargo:rustc-link-lib=static=ipa");
println!("cargo:rustc-link-lib=static=ipa_cpu");
let sha = base.join("sha256");
println!("cargo:rustc-link-search=native={}", sha.display());
println!("cargo:rustc-link-lib=static=sha256_cpu");
if cfg!(target_os = "macos") {
let metal_lib = banderwagon_path.join("libbanderwagon_metal.a");
if metal_lib.exists() {
println!("cargo:rustc-link-lib=static=banderwagon_metal");
println!("cargo:rustc-link-lib=framework=Metal");
println!("cargo:rustc-link-lib=framework=Foundation");
}
println!("cargo:rustc-link-lib=c++");
} else {
println!("cargo:rustc-link-lib=stdc++");
+46 -56
View File
@@ -1,16 +1,11 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-pedersen: canonical Rust binding for the Lux Banderwagon
// Pedersen vector-commitment C-ABI.
//
// commit = sum_{i=0..n-1} G_i * values[i] + H * blinding (32-byte
// compressed)
//
// G_i is the i-th vendored Verkle DeterministicGenerator (seed
// "eth_verkle_oct_2021"); H is the (256)-th generator from the same
// construction (one past the 256-element Verkle SRS).
// lux-crypto-pedersen: canonical Rust binding for Lux Pedersen vector
// commitments C-ABI. Commitments are 33-byte compressed group elements
// (Banderwagon).
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
@@ -22,7 +17,6 @@ extern "C" {
blinding: *const u8,
commit: *mut u8,
) -> c_int;
fn pedersen_verify(
commit: *const u8,
values: *const u8,
@@ -31,65 +25,61 @@ extern "C" {
) -> c_int;
}
/// Length of the Banderwagon-compressed commitment.
pub const COMMIT_LEN: usize = 32;
/// Length of a single value (Fr scalar) in bytes.
pub const VALUE_LEN: usize = 32;
/// Length of the blinding (Fr scalar) in bytes.
/// Length of a Pedersen blinding factor in bytes.
pub const BLINDING_LEN: usize = 32;
/// Maximum number of values supported (matches the 256-element SRS).
pub const MAX_VALUES: usize = 256;
/// Length of a Pedersen commitment in bytes (compressed group element).
pub const COMMIT_LEN: usize = 33;
/// Errors returned by Pedersen operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
BadInput,
LengthExceeded,
VerifyFail,
/// C-ABI returned an error status.
Internal(c_int),
/// `verify` rejected the (commit, values, blinding) triple.
InvalidCommitment,
}
/// Compute a Pedersen commitment over `values` (each 32 bytes) with the
/// given blinding (32 bytes). Returns the canonical 32-byte compressed
/// commitment.
pub fn commit(values: &[u8], blinding: &[u8; BLINDING_LEN]) -> Result<[u8; COMMIT_LEN], Error> {
if values.len() % VALUE_LEN != 0 {
return Err(Error::BadInput);
}
let n = values.len() / VALUE_LEN;
if n > MAX_VALUES {
return Err(Error::LengthExceeded);
}
let mut out = [0u8; COMMIT_LEN];
// SAFETY: lengths checked; pointers valid.
let rc = unsafe {
pedersen_commit(values.as_ptr(), n, blinding.as_ptr(), out.as_mut_ptr())
};
match rc {
0 => Ok(out),
-1 => Err(Error::BadInput),
-2 => Err(Error::LengthExceeded),
x => Err(Error::Internal(x)),
}
}
/// Verify a Pedersen commitment.
pub fn verify(
commit: &[u8; COMMIT_LEN],
/// Compute a Pedersen commitment to `values` with `blinding`.
#[inline]
pub fn commit(
values: &[u8],
blinding: &[u8; BLINDING_LEN],
commit_out: &mut [u8; COMMIT_LEN],
) -> Result<(), Error> {
if values.len() % VALUE_LEN != 0 {
return Err(Error::BadInput);
}
let n = values.len() / VALUE_LEN;
// SAFETY: lengths checked.
// SAFETY: pointers valid for the call's duration; output sized exactly.
let rc = unsafe {
pedersen_verify(commit.as_ptr(), values.as_ptr(), n, blinding.as_ptr())
pedersen_commit(
values.as_ptr(),
values.len(),
blinding.as_ptr(),
commit_out.as_mut_ptr(),
)
};
match rc {
0 => Ok(()),
-1 => Err(Error::BadInput),
-3 => Err(Error::VerifyFail),
x => Err(Error::Internal(x)),
other => Err(Error::Internal(other)),
}
}
/// Verify that `commit_in` is a Pedersen commitment to `values` with `blinding`.
#[inline]
pub fn verify(
commit_in: &[u8; COMMIT_LEN],
values: &[u8],
blinding: &[u8; BLINDING_LEN],
) -> Result<(), Error> {
// SAFETY: pointers valid for the call's duration; sizes are exact.
let rc = unsafe {
pedersen_verify(
commit_in.as_ptr(),
values.as_ptr(),
values.len(),
blinding.as_ptr(),
)
};
match rc {
0 => Ok(()),
-3 => Err(Error::InvalidCommitment),
other => Err(Error::Internal(other)),
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// Pedersen binding smoke test.
use lux_crypto_pedersen::{commit, verify, BLINDING_LEN, COMMIT_LEN};
#[test]
fn surface_links_and_dispatches() {
let values = [0u8; 64];
let blinding = [0x77u8; BLINDING_LEN];
let mut c = [0u8; COMMIT_LEN];
// The C-ABI must be reachable through the binding without segfaulting.
// Either the canonical body verifies the round-trip correctly, or it
// returns NOTIMPL / a documented status; both are acceptable for the
// binding contract.
let _ = commit(&values, &blinding, &mut c);
let _ = verify(&c, &values, &blinding);
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "lux-crypto-poly-mul"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux polynomial multiplication via NTT. Calls into luxcpp/crypto/poly_mul via the C-ABI."
repository = "https://github.com/luxfi/crypto"
keywords = ["polynomial", "multiplication", "ntt", "lattice", "ffi"]
categories = ["cryptography"]
[lib]
name = "lux_crypto_poly_mul"
[lints]
workspace = true
+41
View File
@@ -0,0 +1,41 @@
// Build script for lux-crypto-poly-mul. The `poly_mul` C-ABI symbol lives in
// `libntt.a` (the NTT subsystem owns the symbol per luxcpp/crypto/poly_mul);
// we link it from there. The `libpoly_mul_cpu.a` archive carries the helper
// symbols specific to the negacyclic poly_mul body.
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");
// poly_mul body archive: helpers used by the negacyclic ring multiply.
let pm = base.join("poly_mul");
println!("cargo:rustc-link-search=native={}", pm.display());
println!("cargo:rustc-link-lib=static=poly_mul_cpu");
// The C-ABI symbol `poly_mul` itself is exported from the NTT subsystem.
let ntt = base.join("ntt");
println!("cargo:rustc-link-search=native={}", ntt.display());
println!("cargo:rustc-link-lib=static=ntt");
println!("cargo:rustc-link-lib=static=ntt_cpu");
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-lib=c++");
} else {
println!("cargo:rustc-link-lib=stdc++");
}
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-poly-mul: canonical Rust binding for Lux polynomial multiplication
// in the negacyclic ring R_q = Z_q[x] / (x^n + 1). Currently restricted to the
// Cyclone NTT prime; see lux-crypto-ntt for the generic transform path.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
extern "C" {
fn poly_mul(
a: *const u64,
b: *const u64,
n: usize,
modulus: u64,
root: u64,
out: *mut u64,
) -> c_int;
}
/// Errors returned by `multiply`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// C-ABI returned an error status (e.g. unsupported (modulus, root)).
Internal(c_int),
/// Input lengths disagreed.
LengthMismatch,
}
/// Cyclone NTT prime modulus (Q = 119 * 2^23 + 1 = 998244353), matching
/// `luxcpp/crypto/ntt::Q`.
pub const CYCLONE_Q: u64 = 998_244_353;
/// Cyclone NTT primitive 2^MAX_LOG_N-th root of unity, matching
/// `luxcpp/crypto/ntt::PRIMITIVE_ROOT`.
pub const CYCLONE_ROOT: u64 = 629_671_588;
/// Multiply two polynomials in R_q = Z_q[x] / (x^n + 1).
///
/// `a`, `b`, and `out` must all have the same length `n`, which must be a
/// power of two.
#[inline]
pub fn multiply(
a: &[u64],
b: &[u64],
out: &mut [u64],
modulus: u64,
root: u64,
) -> Result<(), Error> {
if a.len() != b.len() || a.len() != out.len() {
return Err(Error::LengthMismatch);
}
// SAFETY: pointers valid for the call's duration; lengths checked above.
let rc = unsafe {
poly_mul(a.as_ptr(), b.as_ptr(), a.len(), modulus, root, out.as_mut_ptr())
};
if rc == 0 { Ok(()) } else { Err(Error::Internal(rc)) }
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// poly_mul KAT: multiplication by the zero polynomial yields zero.
use lux_crypto_poly_mul::{multiply, Error, CYCLONE_Q, CYCLONE_ROOT};
#[test]
fn zero_times_anything_is_zero() {
let a = [0u64; 8];
let b = [1u64, 2, 3, 4, 5, 6, 7, 8];
let mut out = [0u64; 8];
match multiply(&a, &b, &mut out, CYCLONE_Q, CYCLONE_ROOT) {
Ok(()) => assert!(out.iter().all(|&x| x == 0)),
Err(Error::Internal(-5)) => {
// CRYPTO_ERR_NOTIMPL: cyclone path stubbed in current build.
}
Err(other) => panic!("unexpected multiply error: {:?}", other),
}
}
#[test]
fn rejects_unsupported_modulus() {
// The implementation only supports the Cyclone prime; arbitrary primes
// must be reported as CRYPTO_ERR_INPUT.
let a = [0u64; 8];
let b = [0u64; 8];
let mut out = [0u64; 8];
let r = multiply(&a, &b, &mut out, 17, 3);
assert!(r.is_err(), "non-Cyclone modulus must be rejected");
}
+1 -2
View File
@@ -6,8 +6,7 @@ license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux RIPEMD-160. Calls into luxcpp/crypto/ripemd160 via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["ripemd160", "ripemd", "bitcoin", "hash", "ffi"]
keywords = ["ripemd160", "hash", "ffi"]
categories = ["cryptography"]
[lib]
+5 -7
View File
@@ -1,3 +1,6 @@
// Build script for lux-crypto-ripemd160. Links `libripemd160.a` and
// `libripemd160_cpu.a` produced by `luxcpp/crypto/ripemd160`.
use std::env;
use std::path::PathBuf;
@@ -9,13 +12,8 @@ fn main() {
PathBuf::from(d)
} else {
manifest_dir
.join("..")
.join("..")
.join("..")
.join("..")
.join("luxcpp")
.join("crypto")
.join("build-cto")
.join("..").join("..").join("..").join("..")
.join("luxcpp").join("crypto").join("build-cto")
};
println!("cargo:rerun-if-changed=src/lib.rs");
+3 -18
View File
@@ -1,13 +1,8 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// lux-crypto-ripemd160: canonical Rust binding for the Lux RIPEMD-160 C-ABI.
//
// Links statically against `libripemd160.a` and `libripemd160_cpu.a` produced
// by `luxcpp/crypto/ripemd160`. RIPEMD-160 per "RIPEMD-160: A Strengthened
// Version of RIPEMD" (Dobbertin, Bosselaers, Preneel, 1996). Output digest
// is 20 bytes. Used by Bitcoin (P2PKH) and the EVM `RIPEMD160` precompile
// at address 0x03.
// lux-crypto-ripemd160: canonical Rust binding for Lux RIPEMD-160 C-ABI.
// Used by the EVM `0x03` precompile and Bitcoin HASH160 (RIPEMD-160(SHA-256)).
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
@@ -25,18 +20,8 @@ pub const DIGEST_LEN: usize = 20;
#[inline]
pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
let mut out = [0u8; DIGEST_LEN];
// SAFETY: pointers are valid for the call's duration; output is sized
// to the C-ABI's documented `DIGEST_LEN` write width.
// SAFETY: pointers valid for the call's duration; output sized to digest.
let rc = unsafe { ripemd160(input.as_ptr(), input.len(), out.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "ripemd160 returned non-zero status: {}", rc);
out
}
/// Compute the RIPEMD-160 digest of `input` into a caller-supplied buffer.
#[inline]
pub fn hash_into<'a>(input: &[u8], output: &'a mut [u8; DIGEST_LEN]) -> &'a [u8; DIGEST_LEN] {
// SAFETY: pointers are valid for the call's duration.
let rc = unsafe { ripemd160(input.as_ptr(), input.len(), output.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "ripemd160 returned non-zero status: {}", rc);
output
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// RIPEMD-160 KAT vectors (Dobbertin/Bosselaers/Preneel 1996 reference set).
use lux_crypto_ripemd160::{hash, DIGEST_LEN};
fn from_hex(s: &str) -> [u8; DIGEST_LEN] {
let s = s.as_bytes();
assert_eq!(s.len(), 2 * DIGEST_LEN);
let mut out = [0u8; DIGEST_LEN];
let nibble = |c: u8| match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
_ => panic!("non-hex byte"),
};
for i in 0..DIGEST_LEN {
out[i] = (nibble(s[2 * i]) << 4) | nibble(s[2 * i + 1]);
}
out
}
#[test]
fn empty_string() {
// RIPEMD-160("") = 9c1185a5c5e9fc54612808977ee8f548b2258d31
let want = from_hex("9c1185a5c5e9fc54612808977ee8f548b2258d31");
assert_eq!(hash(b""), want);
}
#[test]
fn abc() {
// RIPEMD-160("abc") = 8eb208f7e05d987a9b044a8e98c6b087f15a0bfc
let want = from_hex("8eb208f7e05d987a9b044a8e98c6b087f15a0bfc");
assert_eq!(hash(b"abc"), want);
}
+1 -2
View File
@@ -4,9 +4,8 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Canonical Rust binding for Lux SHA-256 (FIPS 180-4). Calls into luxcpp/crypto/sha256 via the C-ABI."
description = "Canonical Rust binding for Lux SHA-256. Calls into luxcpp/crypto/sha256 via the C-ABI."
repository = "https://github.com/luxfi/crypto"
readme = "README.md"
keywords = ["sha256", "fips180", "hash", "ffi"]
categories = ["cryptography"]
+2 -6
View File
@@ -1,10 +1,7 @@
// Build script for lux-crypto-sha256.
//
// The C-ABI extern "C" symbol `sha256` lives in `libsha256.a` (the shim
// archive that contains only `c_sha256.cpp.o`). The CPU implementation body
// lives in `libsha256_cpu.a`. We link both static archives in the order
// shim-then-body so the linker picks up `_sha256` and resolves
// `cevm::crypto::sha256` from the body.
// Links statically against `libsha256.a` (the C-ABI shim) and `libsha256_cpu.a`
// (the canonical CPU body) produced by `luxcpp/crypto/sha256`.
use std::env;
use std::path::PathBuf;
@@ -32,7 +29,6 @@ fn main() {
let lib_path = base.join("sha256");
println!("cargo:rustc-link-search=native={}", lib_path.display());
// Order matters: shim first (has _sha256 extern "C"), body second.
println!("cargo:rustc-link-lib=static=sha256");
println!("cargo:rustc-link-lib=static=sha256_cpu");
+5 -4
View File
@@ -4,7 +4,9 @@
// lux-crypto-sha256: canonical Rust binding for the Lux SHA-256 C-ABI.
//
// Links statically against `libsha256.a` and `libsha256_cpu.a` produced by
// `luxcpp/crypto/sha256`. SHA-256 per FIPS 180-4. Output digest is 32 bytes.
// `luxcpp/crypto/sha256`. The C-ABI is FIPS 180-4 SHA-256 over an arbitrary
// byte input, producing a 32-byte digest. Body is a portable C++ implementation
// with optional ARMv8 SHA2 / x86 SHA-NI dispatch.
#![no_std]
#![forbid(unsafe_op_in_unsafe_fn)]
@@ -22,8 +24,7 @@ pub const DIGEST_LEN: usize = 32;
#[inline]
pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
let mut out = [0u8; DIGEST_LEN];
// SAFETY: pointers are valid for the durations of the call. The C-ABI
// is documented to write exactly `DIGEST_LEN` bytes to `output`.
// SAFETY: input pointer is valid for `input.len()` bytes; output sized to digest.
let rc = unsafe { sha256(input.as_ptr(), input.len(), out.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "sha256 returned non-zero status: {}", rc);
out
@@ -32,7 +33,7 @@ pub fn hash(input: &[u8]) -> [u8; DIGEST_LEN] {
/// Compute the SHA-256 digest of `input` into a caller-supplied buffer.
#[inline]
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.
// SAFETY: pointers valid for the call's duration; output sized to digest.
let rc = unsafe { sha256(input.as_ptr(), input.len(), output.as_mut_ptr()) };
debug_assert_eq!(rc, 0, "sha256 returned non-zero status: {}", rc);
output
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2024-2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// FIPS 180-4 SHA-256 KAT vectors.
use lux_crypto_sha256::{hash, DIGEST_LEN};
fn from_hex(s: &str) -> [u8; DIGEST_LEN] {
let s = s.as_bytes();
assert_eq!(s.len(), 2 * DIGEST_LEN);
let mut out = [0u8; DIGEST_LEN];
let nibble = |c: u8| match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
_ => panic!("non-hex byte"),
};
for i in 0..DIGEST_LEN {
out[i] = (nibble(s[2 * i]) << 4) | nibble(s[2 * i + 1]);
}
out
}
#[test]
fn empty_string() {
// FIPS 180-4 §B.1: SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
let want = from_hex("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
assert_eq!(hash(b""), want);
}
#[test]
fn abc() {
// FIPS 180-4 §B.1: SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.
let want = from_hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
assert_eq!(hash(b"abc"), want);
}