A post-quantum signature scheme that used to demand a guessed 8 MiB of stack “to be safe” now runs on a measured 2 MiB — and two of its sibling crates just got a genuinely heap-free build to go with it.
Today we released OxiCrypto 0.2.1 — a release built around measuring instead of guessing. A new stack_safe module pins ML-DSA-87’s worker-thread stack requirement at 2 MiB instead of a hardcoded 8 MiB. A new default-on alloc Cargo feature makes oxicrypto-core and oxicrypto-hash genuinely buildable against bare core. A new integration test wires a full PQ-KEM → HKDF → AEAD hybrid encryption round trip through the facade crate. And a security fix closes a denial-of-service panic in truncated HMAC verification.
No OpenSSL. No BoringSSL. No ring, no aws-lc-rs in the default closure. No FFI. No -sys crates. And as of 0.2.1, no heap allocator required either, if you don’t want one: oxicrypto-core and oxicrypto-hash now build with cargo build --no-default-features and link only core, no extern crate alloc anywhere in the tree. The rest of the workspace keeps compiling to a single static binary with zero apt-get install and no C toolchain, exactly as it did in 0.2.0.
Why OxiCrypto 0.2.1 is a game changer
Even a Pure Rust crypto stack accumulates its own quiet pain points:
- A post-quantum signature scheme’s stack use is usually a guess — ship a big round number “to be safe” (this crate’s own documentation used to quote 8 MiB, one test even used 16 MiB) and hope it’s enough across every compiler, allocator, and platform.
- A
no_stdfeature flag on a crypto crate is too often a documentation-only promise: the flag exists inCargo.toml, but the crate still linksallocunder the hood, and nobody notices until a real embedded build fails. - Wiring a KEM, a KDF, and an AEAD together into an actual hybrid public-key encryption scheme is exactly the kind of cross-crate integration most libraries leave as “an exercise for the caller” — and getting the HKDF salt/info/extract-then-expand details wrong is how ad hoc constructions get broken.
- A truncated-MAC helper that checks only a lower bound on the caller-supplied tag length is a latent panic — a denial-of-service vector — for whoever controls that length.
OxiCrypto 0.2.1 ends all of that.
- Real measurement, not a guess. A binary-search probe against the live crate pinned ML-DSA-87’s actual keygen+sign+verify worst case at 768 KiB (debug) / 512 KiB (release).
OXICRYPTO_MLDSA_STACKnow ships at 2 MiB — ≈2.7× headroom over the measured worst case, and 4× smaller than the historical 8 MiB figure the docs used to quote. - Genuinely alloc-free builds.
cargo build -p oxicrypto-core --no-default-features(andoxicrypto-hash) now links onlycore— no heap allocator — whileSecretKey<N>,Hash::hash/hash_to_array::<N>(), the constant-time utilities,CryptoError, andAlgorithmIdstay available. - A proven hybrid-encryption path.
crates/oxicrypto/tests/pq_hybrid_encryption.rsexercisesMlKem768andXWing768end to end — encapsulate → HKDF-SHA-256 extract-then-expand → AES-256-GCM seal/open — and confirms a tampered ciphertext is rejected and independent encapsulations derive distinct keys. - A DoS-class panic closed.
HmacSha256/HmacSha384/HmacSha512’sverify_truncatedandmac_truncatednow validate an inclusive length range and returnCryptoError::BadInputinstead of slicing past the digest buffer. - Modernized AEAD internals. The
aead0.5 → 0.6 migration replaces a length-panickingGenericArray::from_slicewith fallibletry_fromacross every AEAD backend in the workspace. - New governance docs.
CONTRIBUTING.mdandSECURITY.mdformalize the build/test/lint bar and a private vulnerability-disclosure channel.
Technical Deep Dive: stack safety, alloc-free builds, and the hybrid path
- Stack safety —
oxicrypto-pq::stack_safe.run_on_large_stack<F: FnOnce() -> T + Send, T: Send>(f: F) -> Result<T, CryptoError>spawnsfon a scoped worker thread sized toOXICRYPTO_MLDSA_STACKviathread::Builder::stack_size+spawn_scoped. The threemldsa87_*_stack_safehelpers wrap keygen/sign/verify around it and exchange owned byte vectors — a 32-byte seed, a 2592-byte verifying key, a 4627-byte signature — rather than the typedSigningKey87/VerifyingKey87/Signature87structs, so the largeml-dsaworking set (theexpand_aNTT matrix plus they/w/cs1/cs2/z/ct0vectors) never has to cross back over the thread boundary.crates/oxicrypto-pq/tests/stack_safe.rsproves the round trip on the default libtest worker-thread stack — no manual high-stack wrapper needed by the caller. - Alloc-free surface —
oxicrypto-core/oxicrypto-hash. The newallocfeature (default-on) gatesVec/String/Boxre-exports,SecretVec, theKeyGeneratortrait, and the*_to_vec/seal_*/open_*/agree_to_vec/mac_to_vecdefault trait methods inoxicrypto-core; inoxicrypto-hashit gateshash_to_vec,blake3_xof,parallel_hash*_xof, the streamingHashBuilder, and the SHAKE/cSHAKE/TupleHash XOF helpers. What survives--no-default-features: the inherenthash_fixed::<N>()methods andHash::hash/hash_to_array::<N>(), exercised end to end in the newcrates/oxicrypto-hash/tests/no_alloc.rsover SHA-256, SHA-512, and BLAKE3. - The hybrid encryption path — the facade crate.
oxicryptois the only crate that seesoxicrypto-pq,oxicrypto-kdf, andoxicrypto-aeadsimultaneously, sopq_hybrid_encryption.rslives there: encapsulate with aKemimplementation, run HKDF-SHA-256 extract-then-expand (hkdf_sha256_extract/hkdf_sha256_expand, RFC 5869) over the shared secret with a domain-separated salt and info string, then seal/open withaead_impl(AeadAlgo::Aes256Gcm). This closes three previously-deferred cross-crate TODOs (pq↔kdf, pq↔aead, aead↔pq) without inverting the dependency graph. - AEAD internals + the MAC fix.
Aes128Gcm/Aes256Gcm,ChaCha20Poly1305/XChaCha20Poly1305,Aes128GcmSiv/Aes256GcmSiv, OCB3, and the STREAM chunked-AEAD construction all move toaead0.6’sAeadInOuttrait generation, with nonce/tag construction going throughNonce::<C>::try_from/Tag::<C>::try_frominstead of a length-panickingGenericArray::from_slice. Separately,oxicrypto-mac’sverify_truncated/mac_truncatednow validate an inclusive16..=digest_lenrange before slicing, closing the panic path.
Getting Started
cargo add oxicrypto
[dependencies]
oxicrypto = "0.2.1"
# Post-quantum primitives (off by default):
oxicrypto = { version = "0.2.1", features = ["pq-preview"] }
Hashing:
use oxicrypto::{blake3, sha256, sha512};
let digest = sha256(b"hello world");
let digest = sha512(b"hello world");
let digest = blake3(b"hello world");
Authenticated encryption, keys and nonce drawn from the crate’s own RNG:
use oxicrypto::{aead_impl, random_bytes, random_nonce, AeadAlgo};
let aead = aead_impl(AeadAlgo::Aes256Gcm);
let key = random_bytes(32)?; // 32 bytes for AES-256-GCM
let nonce = random_nonce::<12>()?; // 12-byte nonce; must never repeat for a given key
let ct = aead.seal_to_vec(&key, &nonce, b"aad", b"plaintext")?;
let pt = aead.open_to_vec(&key, &nonce, b"aad", &ct)?;
assert_eq!(pt, b"plaintext");
What’s New in 0.2.1
stack_safemodule (oxicrypto-pq) —run_on_large_stack,mldsa87_generate_stack_safe/_sign_stack_safe/_verify_stack_safe, and theOXICRYPTO_MLDSA_STACKconstant (2 MiB) for ML-DSA-87.- Default-on
allocfeature (oxicrypto-core,oxicrypto-hash) — genuinecore-only builds with--no-default-features; supersedesoxicrypto-hash’s oldno_stdfeature, which was documentation-only and had no link-time effect. Breaking for anyone referencing--features no_stdby name. - PQ KEM → HKDF → AEAD hybrid encryption test (
oxicrypto,pq-previewfeature) — end-to-end coverage forMlKem768andXWing768. bench_arch_profile.sh(oxicrypto-bench) — per-architecture Criterion baselines (arch-<uname -m>) so results from different machines never overwrite each other;--archiveforwards a per-arch label tobench_archive.sh.Hash/StreamingHash/CryptoErrorre-exported fromoxicrypto-hash’s crate root, so downstream code doesn’t need an explicitoxicrypto-coredependency just to name them.- New
CONTRIBUTING.md/SECURITY.md— build/test/lint commands, contributor rules, and a private disclosure process ([email protected], latest-0.x-only support). - Changed — the AEAD backend’s
aead0.5 → 0.6AeadInOutmigration (internal only, no public API change); dependency upgrades acrossaes-gcm,chacha20poly1305,aead,chacha20,aes-gcm-siv,ocb3,p256/p384/p521/k256,ed448-goldilocks,x448, andaws-lc-rs; ML-DSA-87’s stack requirement re-measured from a hardcoded 8/16 MiB down to 2 MiB across all its tests and thepq_benchmarkscriterion group;oxicrypto-benchmarkedpublish = false. - Security —
HmacSha256/HmacSha384/HmacSha512’sverify_truncated/mac_truncatedpreviously validated only a lower bound on the requested length and sliced the digest buffer to it — a caller-supplied length past the digest size (33+ bytes for SHA-256, 49+ for SHA-384, 65+ for SHA-512) panicked instead of erroring. Both methods now returnCryptoError::BadInputfor any out-of-range length. - 1736 tests pass with
cargo nextest run --all-features --workspace(1612 with default features).
Tips
-
Reach for
stack_safeinstead of rolling your own thread wrapper for ML-DSA-87. It ships the correctly sized worker thread internally:use oxicrypto::pq::{mldsa87_generate_stack_safe, mldsa87_sign_stack_safe, mldsa87_verify_stack_safe}; let (seed, vk_bytes) = mldsa87_generate_stack_safe()?; let sig = mldsa87_sign_stack_safe(&seed, b"message")?; mldsa87_verify_stack_safe(&vk_bytes, b"message", &sig)?;This needs the
pq-previewfeature, and works from any ambient thread — including the default libtest worker-thread stack — with no manualthread::Builderon your side. -
Building for a microcontroller? Turn off the default
allocfeature.cargo build -p oxicrypto-hash --no-default-featureslinks onlycore; reach forhash_fixed::<N>()andHash::hash/hash_to_array::<N>()instead ofhash_to_vecor the XOF helpers, which needalloc. -
Drop any
--features no_stdreference. It’s gone as of 0.2.1 —oxicrypto-hashnow uses a default-onallocfeature plus an opt-instdfeature that implies it. -
If you called
verify_truncated/mac_truncatedwith a caller-controlled length, upgrade now. Pre-0.2.1 an oversized length panicked (a DoS on any path that runs it over attacker-controlled input); 0.2.1 returnsCryptoError::BadInputinstead, and any panic-catching workaround you had is no longer necessary. -
Copy the hybrid-encryption pattern instead of reinventing it.
crates/oxicrypto/tests/pq_hybrid_encryption.rsis a complete worked example of KEM → HKDF-SHA-256 → AES-256-GCM with correct domain separation — start there before writing your own KEM-DEM construction. -
Record a per-architecture benchmark baseline.
bench_arch_profile.shunderoxicrypto-benchsaves results underarch-<uname -m>so an aarch64 run never overwrites an x86_64 one; the AES-NI leg is an intentional deferral that must be recorded on real x86_64 hardware.
This is the foundation
OxiCrypto depends on nothing — it’s the foundation layer the rest of the COOLJAPAN cryptography stack stands on. It is already depended on by:
- OxiTLS — the Pure Rust TLS stack.
- OxiStore — encrypted storage; its
oxistore-encryptcrate bridges to OxiCrypto’s key-derivation and HSM primitives. - OxiSQL — the Pure Rust SQL layer.
- oxify, oxionnx (model signing), and oxirag (content addressing) — the application-layer consumers named directly in OxiCrypto’s own README.
Around it sit sibling Pure Rust projects removing their own native dependency from the floor of the ecosystem — OxiArc (compression, replacing zip/flate2/zstd), OxiBLAS, OxiFFT, and OxiZ (the Pure Rust SMT solver).
Repository: https://github.com/cool-japan/oxicrypto
Star the repo if you want a post-quantum signature scheme whose stack use is measured instead of guessed, and cryptographic primitives that build with no heap at all when you need them to.
The era of shipping a round, over-conservative stack-size number because nobody measured the real one is over. Pure Rust cryptography is here — measured, sovereign, and FFI-free.
— KitaSan at COOLJAPAN OÜ July 18, 2026