krypteia-tessera — Shared Hash Primitives for the krypteia workspace

Crates.io Docs.rs docs (guide) License: Apache-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 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 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.

  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<H>

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::<H>(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::<Crc32> and mgf1::<Crc32> will not compile, so a linear checksum can never be mistaken for a MAC. See Side-channel posture.

Traits

Two small traits keep the generic code (hmac, mgf1, hkdf, pbkdf2, the FIPS 204/205 encoders in quantica) written once:

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.

[dependencies]
tessera = { package = "krypteia-tessera", path = "../tessera" }

Quick start

Hashing (SHA-256)

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)

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

use tessera::{hmac, Sha256};

let mut tag = [0u8; 32];
hmac::<Sha256>(b"secret key", b"message", &mut tag);

HKDF-SHA-256 (extract-and-expand)

use tessera::{hkdf, Sha256};

let prk = hkdf::extract::<Sha256>(b"salt", b"input keying material");
let mut okm = [0u8; 42];
hkdf::expand::<Sha256>(&prk, b"context info", &mut okm).unwrap();

Password hashing (Argon2id)

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)

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.

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

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

License

Apache-2.0.