# krypteia-tessera — Shared Hash Primitives for the krypteia workspace [![Crates.io](https://img.shields.io/crates/v/krypteia-tessera.svg)](https://crates.io/crates/krypteia-tessera) [![Docs.rs](https://docs.rs/krypteia-tessera/badge.svg)](https://docs.rs/krypteia-tessera) [![docs (guide)](https://img.shields.io/badge/docs-krypteia--rs.dev-3fb950)](https://krypteia-rs.dev) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) Pure-Rust implementations of every hash, XOF, keyed-hash, key-derivation function and checksum the workspace needs — SHA-2, SHA-3 / Keccak, the SHAKE / cSHAKE XOFs, MGF1, the legacy/alt family (SHA-1, RIPEMD-160, BLAKE2b/2s, BLAKE3), the SP 800-185 keyed constructions (KMAC, TupleHash, ParallelHash), HMAC, the KDF family (HKDF, PBKDF2, SP 800-108, scrypt, Argon2), and the non-cryptographic CRC-16/32 checksums. `tessera` is the **single home for every hash** in the [krypteia](https://github.com/cslashm/krypteia) workspace: `quantica` (post-quantum) and `arcana` (classical) both build on it rather than carrying their own copies of Keccak or SHA-2. A *tessera* was the Roman token of identity and proof — a fitting name for a crate whose whole job is fingerprints and derivation. ## Design rules The crate inherits the krypteia workspace design rules: 1. **Pure Rust, zero external crates** — only `core` and `alloc`. The crate is `#![no_std]`; `alloc` is used only where an output length is not known at the call site (`hkdf::extract`, the SP 800-185 helpers). No workspace dependency at all — `tessera` sits at the bottom of the dependency graph so that both `arcana` and `quantica` can depend on it without a cycle. 2. **Embedded-friendly** — fixed-output APIs write into caller-provided slices (`finalize(&mut [u8])`, `pbkdf2(…, dk: &mut [u8])`), so the hot paths allocate nothing. Target devices: secure elements, STM32 (Cortex-M0/M4/M33), RISC-V (ESP32-C3, …). 3. **Constant-timeness is scoped, not blanket** — the hash compression functions process **public** data, so their (data-oblivious) structure is CT by construction but CT is not a stated goal for them. The secret-handling primitives (HMAC, HKDF, PBKDF2, SP 800-108) are data-oblivious in the key. The memory-hard functions (scrypt, Argon2d/id) have **known data-dependent memory access** and are *not* constant-time — see [Side-channel posture](#side-channel-posture) for the exact leaking paths. CRC is non-cryptographic and non-CT by design. 4. **Validated** — every algorithm is tested against pinned FIPS / NIST SP / RFC reference vectors plus the official NIST CAVP (SHS) and ACVP (hashes) corpora. See [Test validation](#test-validation). 5. **C / JS-exposable** — the companion crates `tessera_ffi` (C ABI) and `tessera_wasm` (WebAssembly) mirror the public surface. ## Algorithms ### Hash functions (`impl Digest`) | Algorithm | Output | Module | Standard | |------------------|-----------|------------------------------|-----------------| | SHA-1 | 160 b | `sha1::Sha1` *(legacy)* | FIPS 180-4 | | SHA-224 | 224 b | `sha2::Sha224` | FIPS 180-4 | | SHA-256 | 256 b | `sha2::Sha256` | FIPS 180-4 | | SHA-384 | 384 b | `sha2::Sha384` | FIPS 180-4 | | SHA-512 | 512 b | `sha2::Sha512` | FIPS 180-4 | | SHA-512/224 | 224 b | `sha2::Sha512_224` | FIPS 180-4 | | SHA-512/256 | 256 b | `sha2::Sha512_256` | FIPS 180-4 | | SHA3-224 | 224 b | `sha3::Sha3_224` | FIPS 202 | | SHA3-256 | 256 b | `sha3::Sha3_256` | FIPS 202 | | SHA3-384 | 384 b | `sha3::Sha3_384` | FIPS 202 | | SHA3-512 | 512 b | `sha3::Sha3_512` | FIPS 202 | | RIPEMD-160 | 160 b | `ripemd160::Ripemd160` *(legacy)* | ISO/IEC 10118-3 | | BLAKE2b | 1–64 B | `blake2::Blake2b` | RFC 7693 | | BLAKE2s | 1–32 B | `blake2::Blake2s` | RFC 7693 | | BLAKE3 | 32 B / XOF| `blake3::{hash, keyed_hash, derive_key}` | [BLAKE3 spec] | BLAKE2b/2s expose a variable-length constructor `with_output_len(n)`; the default `Digest::OUTPUT_LEN` is the full 64 B / 32 B width. ### Extendable-output functions (`impl Xof`) | Algorithm | Module | Standard | |------------------|------------------------------|-----------------| | SHAKE128 | `sha3::Shake128` | FIPS 202 | | SHAKE256 | `sha3::Shake256` | FIPS 202 | | cSHAKE128 | `sha3::CShake128` | NIST SP 800-185 | | cSHAKE256 | `sha3::CShake256` | NIST SP 800-185 | | MGF1 | `mgf1::mgf1` | RFC 8017 §B.2.1 | | BLAKE3 (XOF mode)| `blake3::hash` (any out len) | [BLAKE3 spec] | `mgf1` is generic over any `Digest`, mirroring how RSA-OAEP / RSA-PSS in `arcana` consume it. `sha3::KeccakState` (rate + domain suffix, `absorb` / `squeeze`) is public so `quantica` can drive Keccak on its hot path and `arcana` can build cSHAKE-keyed constructions on top. ### Keyed hashes / MACs | Algorithm | Module | Standard | |---------------------------|----------------------------------------|-----------------| | HMAC (over any `Digest`) | `hmac::{hmac, hmac_multi}` | RFC 2104, FIPS 198-1 | | KMAC128 / KMAC256 (+XOF) | `sp800_185::{kmac128, kmac256, …_xof}` | NIST SP 800-185 | | TupleHash128 / 256 (+XOF) | `sp800_185::{tuplehash128, …_xof}` | NIST SP 800-185 | | ParallelHash128 / 256 (+XOF) | `sp800_185::{parallelhash128, …_xof}` | NIST SP 800-185 | `hmac::(key, data, out)` instantiates HMAC over any hash implementing `Digest`, so HMAC-SHA-256/384/512, HMAC-SHA3-*, HMAC-SHA-1 and HMAC-RIPEMD-160 all come from one generic function. `hmac_multi` takes a slice of message parts to avoid an intermediate concatenation. ### Key-derivation functions | Algorithm | Module | Standard | |----------------------|---------------------------------------------------|-----------------------| | HKDF | `hkdf::{extract, expand, derive}` (over any `Digest`) | RFC 5869 | | PBKDF2 | `pbkdf2::pbkdf2` (over any `Digest`) | RFC 2898 / SP 800-132 | | SP 800-108 KBKDF | `sp800_108::{counter, feedback, double_pipeline}` | NIST SP 800-108r1 | | scrypt | `scrypt::scrypt` | RFC 7914 | | Argon2 d / i / id | `argon2::{argon2, Variant}` | RFC 9106 | HKDF, PBKDF2 and SP 800-108 are HMAC-derived and generic over the hash. scrypt carries an **inline Salsa20/8 core** (tessera cannot depend on `arcana` where the full Salsa20 lives, so the 8-round core is reimplemented locally — RFC 7914 §3). Argon2 is built on the crate's own BLAKE2b and covers all three variants (`Variant::{D, I, Id}`), with the optional secret (keyed) and associated-data inputs of RFC 9106 §3.1. ### Checksums (non-cryptographic — deliberately **not** `Digest`) | Family | Named variants (under `crc::`) | Reference | |---------|-----------------------------------------------------------------|---------------------| | CRC-32 | `CRC32_ISO_HDLC`, `CRC32_ISCSI`, `CRC32_BZIP2`, `CRC32_MPEG_2`, `CRC32_CKSUM` | RevEng / Rocksoft model | | CRC-16 | `CRC16_ARC`, `CRC16_MODBUS`, `CRC16_USB`, `CRC16_CCITT_FALSE`, `CRC16_XMODEM`, `CRC16_KERMIT` | RevEng / Rocksoft model | `Crc16` / `Crc32` are streaming engines over a `'static` algorithm descriptor built at compile time (`const fn`), returning the native `u16` / `u32`. A user can define an arbitrary variant with `Crc32Algorithm::new(poly, init, refin, refout, xorout, check)`. CRC types **do not implement `Digest` or `Xof`** — this is a deliberate misuse barrier: `hmac::` and `mgf1::` will not compile, so a linear checksum can never be mistaken for a MAC. See [Side-channel posture](#side-channel-posture). ## Traits Two small traits keep the generic code (`hmac`, `mgf1`, `hkdf`, `pbkdf2`, the FIPS 204/205 encoders in `quantica`) written once: ```rust pub trait Digest: Sized { const OUTPUT_LEN: usize; // digest length, octets const BLOCK_LEN: usize; // Merkle–Damgård / sponge rate (HMAC needs it) fn new() -> Self; fn update(&mut self, data: &[u8]); fn finalize(self, out: &mut [u8]); // writes OUTPUT_LEN octets fn digest(data: &[u8], out: &mut [u8]) { /* … */ } // one-shot default } pub trait Xof: Sized { const BLOCK_LEN: usize; // sponge rate, octets fn new() -> Self; fn update(&mut self, data: &[u8]); fn squeeze(&mut self, out: &mut [u8]); // repeatable } ``` Output sizes are caller-provided slices to keep the trait `no_std` and allocation-free. ## Cargo features `tessera` has **no Cargo features** — it is `#![no_std]` + `alloc`, zero-dependency, in every configuration. There is no `std` gate and no feature-gated SCA layer: the crate is a leaf, and its consumers (`arcana`, `quantica`) own the feature surface. ```toml [dependencies] tessera = { package = "krypteia-tessera", path = "../tessera" } ``` ## Quick start ### Hashing (SHA-256) ```rust use tessera::{Digest, Sha256}; let mut out = [0u8; 32]; Sha256::digest(b"hello, tessera", &mut out); assert_eq!(out.len(), 32); ``` ### XOF (SHAKE128, arbitrary output) ```rust use tessera::{Xof, Shake128}; let mut xof = Shake128::new(); xof.update(b"squeeze me"); let mut out = [0u8; 100]; xof.squeeze(&mut out); // may be called again for more ``` ### HMAC-SHA-256 ```rust use tessera::{hmac, Sha256}; let mut tag = [0u8; 32]; hmac::(b"secret key", b"message", &mut tag); ``` ### HKDF-SHA-256 (extract-and-expand) ```rust use tessera::{hkdf, Sha256}; let prk = hkdf::extract::(b"salt", b"input keying material"); let mut okm = [0u8; 42]; hkdf::expand::(&prk, b"context info", &mut okm).unwrap(); ``` ### Password hashing (Argon2id) ```rust use tessera::argon2::{argon2, Variant}; let mut tag = [0u8; 32]; argon2( Variant::Id, b"password", b"saltsaltsalt", &[], &[], // no secret, no associated data 64, // memory: 64 KiB 3, // t_cost: 3 passes 1, // lanes / parallelism &mut tag, ).unwrap(); ``` ### CRC-32 (ISO-HDLC / zlib) ```rust use tessera::{Crc32, crc::CRC32_ISO_HDLC}; let checksum = Crc32::checksum(&CRC32_ISO_HDLC, b"123456789"); assert_eq!(checksum, 0xCBF4_3926); // the RevEng `check` value ``` ## Side-channel posture > **Scope.** `tessera` deals mostly in **public** data (message hashing, > checksums), so most of it has no secret to leak. The exceptions below are > called out precisely, per the workspace rule that a non-constant-time > secret-dependent path is named explicitly, never shipped silently. ### Data-oblivious (no secret-dependent branch or memory index) * **All hash compression functions** (SHA-1/2/3, RIPEMD-160, BLAKE2, BLAKE3) and the SHAKE / cSHAKE / MGF1 XOFs — their control flow and memory access depend only on the input **length**, not on input **values**. * **HMAC, HKDF, SP 800-108 (counter/feedback/double-pipeline), KMAC** — the key is absorbed through the same data-oblivious compression function; there is no key-value-dependent branch. Tag/PRK comparison, when a caller needs it, must use `silentops::ct_eq` (tessera does not compare secrets itself). ### Known non-constant-time paths (by algorithm design) | Path | Leak | Why it is inherent | |-------------------------------|---------------------------------------------------------------------|--------------------| | `scrypt` (ROMix `Integerify`) | Memory index `j = Integerify(X) mod N` is **derived from the (secret-seeded) state** → cache-line access pattern leaks. | RFC 7914 §6: memory-hardness *requires* data-dependent addressing. | | `argon2` with `Variant::D`, and the first slices of `Variant::Id` | Reference-block index is data-dependent → cache-timing. | RFC 9106 §4: Argon2d/id trade SCA resistance for GPU-cracking resistance. | | `pbkdf2` (and HMAC in general) | The password is the HMAC **key**; a key longer than the block is hashed first, so the *number of compression calls* reveals the password **length class** (≤ block vs > block). | RFC 2104 key-length normalisation. | | `crc::*` | Table-indexed by data bytes; **non-cryptographic**, no secret expected. | CRC is an error-detection code, not a MAC. | **Mitigation guidance.** For side-channel-sensitive password hashing prefer **`Argon2i`** (`Variant::I`), which uses data-*independent* addressing throughout and is the RFC 9106 choice when timing/cache leakage is in the threat model. scrypt has no data-independent mode — its memory-hardness is built on data-dependent access and cannot be made cache-oblivious without losing the property. These are documented in each module's `//!` header. ### Not defended CRC is a linear, trivially forgeable checksum: it detects accidental corruption of **public** data, and is neither a MAC nor an integrity-against- tampering primitive. The `Digest` omission is the enforcement of that boundary at the type level. ## Test validation The inline `src/*.rs` tests are minimal smoke KATs; the bulk of the conformance evidence lives in the integration `tests/` tree, driven by official external corpora parsed by the dependency-free readers under `tests/common/`. | Suite (`tests/…`) | Coverage | Vector source | |---------------------|-----------------------------------------------------------------|----------------------------------------------| | `cavp_shs.rs` | SHA-1, SHA-2 family (short/long/Monte-Carlo) | NIST CAVP SHS `.rsp` | | `acvp_hashes.rs` | SHA-3, SHAKE, SHA-2 | NIST ACVP JSON | | `sp800_185.rs` | cSHAKE, KMAC, TupleHash, ParallelHash (+XOF) | NIST SP 800-185 sample 1/2/4 | | `hmac.rs` | HMAC-SHA-1/2/3 | RFC 4231, RFC 2202 | | `blake3.rs` | BLAKE3 hash / keyed / derive_key | upstream BLAKE3 test vectors | | `misc_hashes.rs` | RIPEMD-160, BLAKE2b/2s | ISO 10118-3, RFC 7693 §B | | `crc.rs` | 11 CRC-16/32 variants — each self-checked against its `check` | RevEng catalogue + zlib/binascii cross-check | | `kdf.rs` | HKDF (RFC 5869 A.1-A.3), PBKDF2 (RFC 6070), SP 800-108 (counter gold), scrypt (RFC 7914 §12), Argon2 d/i/id (RFC 9106 §5) | RFC vectors + independent Python cross-checks | Argon2's three RFC 9106 §5 reference tags are pinned inline in `argon2.rs`. The SP 800-108 **counter** mode is gold-validated against the `cryptography` library; feedback / double-pipeline carry a `[TO CONFIRM]` pending NIST CAVP KDF-108 wiring. ```bash cargo test -p krypteia-tessera # lib smoke + all integration suites cargo build -p krypteia-tessera --no-default-features # no_std build (alloc only) cargo clippy -p krypteia-tessera --all-targets -- -D warnings RUSTDOCFLAGS="-D warnings" cargo doc -p krypteia-tessera --no-deps ``` ## Bindings | Companion | Kind | Status | |-----------------|---------------|--------------------------------------------------------------| | `tessera_ffi` | C ABI (`.a` / `.so` + `include/tessera.h`) | v0.2 — mirrors the digest / XOF / HMAC / SP 800-185 surface. | | `tessera_wasm` | WebAssembly (wasm-bindgen) | v0.2 — JS-facing digests, XOFs and keyed hashes. | Hashes need no CSPRNG, so `tessera_wasm` — unlike `arcana_wasm` / `quantica_wasm` — pulls in no `web-sys::Crypto` random source. ## Module map ```text tessera/src/ lib.rs Crate root: re-exports + Digest / Xof traits traits.rs Digest, Xof sha1.rs SHA-1 (FIPS 180-4) — legacy sha2.rs SHA-224/256/384/512/512-224/512-256 (FIPS 180-4) sha3.rs Keccak-f, SHA3-224/256/384/512, SHAKE128/256, cSHAKE128/256, KeccakState ripemd160.rs RIPEMD-160 (ISO/IEC 10118-3) — legacy blake2.rs BLAKE2b, BLAKE2s (RFC 7693) blake3.rs BLAKE3: hash / keyed_hash / derive_key (fixed + XOF) mgf1.rs MGF1 XOF over any Digest (RFC 8017 §B.2.1) hmac.rs HMAC over any Digest (RFC 2104, FIPS 198-1) sp800_185.rs KMAC128/256, TupleHash128/256, ParallelHash128/256 (+XOF) hkdf.rs HKDF extract / expand / derive (RFC 5869) pbkdf2.rs PBKDF2 (RFC 2898 / SP 800-132) sp800_108.rs KBKDF counter / feedback / double-pipeline (SP 800-108r1) scrypt.rs scrypt (RFC 7914) — inline Salsa20/8 core argon2.rs Argon2 d/i/id (RFC 9106) — BLAKE2b core, secret + AD crc.rs CRC-16/32 (RevEng catalogue) — non-cryptographic, not a Digest ``` ## References - [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) — SHA-1, SHA-2 family - [FIPS 202](https://csrc.nist.gov/publications/detail/fips/202/final) — SHA-3, SHAKE - [NIST SP 800-185](https://csrc.nist.gov/publications/detail/sp/800-185/final) — cSHAKE, KMAC, TupleHash, ParallelHash - [FIPS 198-1](https://csrc.nist.gov/publications/detail/fips/198/1/final) — HMAC - [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104) — HMAC - [RFC 7693](https://www.rfc-editor.org/rfc/rfc7693) — BLAKE2 - [BLAKE3 spec](https://github.com/BLAKE3-team/BLAKE3-specs) — BLAKE3 - [ISO/IEC 10118-3](https://www.iso.org/standard/67116.html) — RIPEMD-160 - [RFC 5869](https://www.rfc-editor.org/rfc/rfc5869) — HKDF - [RFC 2898](https://www.rfc-editor.org/rfc/rfc2898) / [NIST SP 800-132](https://csrc.nist.gov/publications/detail/sp/800-132/final) — PBKDF2 - [NIST SP 800-108r1](https://csrc.nist.gov/publications/detail/sp/800-108/rev-1/final) — KBKDF (counter / feedback / double-pipeline) - [RFC 7914](https://www.rfc-editor.org/rfc/rfc7914) — scrypt - [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106) — Argon2 - [RFC 8017](https://www.rfc-editor.org/rfc/rfc8017) — PKCS#1 (MGF1) - [RevEng CRC catalogue](https://reveng.sourceforge.io/crc-catalogue/) — CRC parameter sets - [NIST CAVP](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program) — SHS conformance vectors - [NIST ACVP-Server](https://github.com/usnistgov/ACVP-Server) — modern conformance vectors ## License Apache-2.0. [BLAKE3 spec]: https://github.com/BLAKE3-team/BLAKE3-specs