mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
merge: c-abi-prefix-uniform-2026-04-27
This commit is contained in:
Generated
+6
-16
@@ -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
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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++");
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))));
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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++");
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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++");
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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++");
|
||||
}
|
||||
}
|
||||
@@ -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)) }
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user