# v0.5.0 — MAC: HMAC-SHA256, HMAC-SHA512, BLAKE3 Keyed
**Release date:** 2026-05-22
**Status:** pre-1.0 milestone. The public API is allowed to evolve in
breaking ways through the `0.x` series; 1.0 freezes it.
---
## What this release is
The authentication-tag surface that pairs with 0.4.0's hashing. A
new `crypt_io::mac` module ships three MACs behind a consistent
compute / verify / streaming triad — and verification is **always**
constant-time, by design.
If you ever wrote `if tag == expected_tag` against a secret value,
this module is the fix. The `*_verify` paths and the streaming
hashers' `verify` methods route through the upstream constant-time
comparators (`hmac::verify_slice`, `blake3::Hash::eq`), so the time
your code takes to reject a tampered tag does not depend on how
many leading bytes happened to match.
---
## Highlights
- **Three algorithms, one API shape.**
- `mac::hmac_sha256` / `hmac_sha512` — universal interop (JWT,
TLS PRF, AWS SigV4, anything spec'd to HMAC).
- `mac::blake3_keyed` — fastest of the three on modern hardware,
typically 4–10× faster than HMAC-SHA256 at the same security
level.
- **Compute + verify, never `==`.** Every algorithm has a
`*_verify` variant that takes `(key, data, expected_tag)` and
returns `bool` (or `Result<bool>` for HMAC) via constant-time
comparison. Module documentation explicitly forbids `tag ==
expected` and points callers at these.
- **Streaming hashers** — `HmacSha256` / `HmacSha512` / `Blake3Mac`
with chainable `update` and consuming `finalize` / `verify`. For
inputs that arrive in chunks or don't fit in memory.
- **Spec-pinned KATs.** RFC 4231 Test Cases 1 + 2 for both
HMAC-SHA256 and HMAC-SHA512 (4 vectors). BLAKE3 keyed empty-input
under the official 32-byte ASCII key, pinned as a byte-array
constant.
- **Verify-rejection coverage.** Every algorithm has explicit
tests for wrong-key, wrong-data, wrong-tag, truncated-tag, and
(BLAKE3) oversized-tag — wrong-length tags surface as a `false`
return, not a panic.
---
## API at a glance
One-shot (HMAC returns `Result` because the upstream trait
signature is fallible; in practice HMAC accepts any key length):
```rust
use crypt_io::mac;
let key = b"shared secret";
let data = b"message to authenticate";
let tag = mac::hmac_sha256(key, data)?;
assert!(mac::hmac_sha256_verify(key, data, &tag)?);
# Ok::<(), crypt_io::Error>(())
```
BLAKE3 keyed is infallible because the key is typed:
```rust
use crypt_io::mac;
let key = [0x42u8; 32]; // 32-byte typed key
let tag = mac::blake3_keyed(&key, b"message"); // never fails
assert!(mac::blake3_keyed_verify(&key, b"message", &tag));
assert!(!mac::blake3_keyed_verify(&key, b"tampered", &tag));
```
Streaming:
```rust
use crypt_io::mac::Blake3Mac;
let key = [0x42u8; 32];
let mut m = Blake3Mac::new(&key);
m.update(b"first chunk ");
m.update(b"second chunk");
let tag = m.finalize();
assert_eq!(tag.len(), 32);
```
Streaming verify:
```rust
# #[cfg(feature = "mac-hmac")] {
use crypt_io::mac::{hmac_sha256, HmacSha256};
let key = b"k";
let tag = hmac_sha256(key, b"msg")?;
let mut m = HmacSha256::new(key)?;
m.update(b"msg");
assert!(m.verify(&tag));
# }
# Ok::<(), crypt_io::Error>(())
```
---
## Choosing a MAC
| JWT (HS256), TLS PRF, AWS request signing, anywhere a spec names HMAC-SHA256 | `mac::hmac_sha256` |
| 64-byte tag for spec compliance | `mac::hmac_sha512` |
| Maximum throughput, you control both sides of the wire | `mac::blake3_keyed` |
| Type-checked fixed-size key | `mac::blake3_keyed` (`&[u8; 32]`) |
| Variable-length key handled internally | `mac::hmac_*` (accepts any length) |
All three are safe at 256-bit symmetric strength.
---
## What's NOT in 0.5.0
- **`subtle` as a direct dep.** Constant-time comparison is provided
by the upstream `hmac` and `blake3` crates, both of which
document the property and route through `subtle` internally.
We do not re-export `subtle` from this crate; if you want it,
add it to your own `Cargo.toml`.
- **Empirical timing verification** (`dudect`-style). That belongs
in the security-hardening phase. For 0.5.0 we trust the upstream
documentation — `hmac::verify_slice` is constant-time,
`blake3::Hash::eq` is constant-time, both are widely audited.
- **MAC benchmarks.** Deferred to Phase 0.8.0 alongside the AEAD
and hash benchmark suites.
---
## Security notes
- **Hash-vs-MAC separation preserved.** `Blake3Hasher` (in `hash`)
is key-free and does not expose `with_key`. The only way to
produce a BLAKE3 keyed tag through this crate is `Blake3Mac` (in
`mac`). This avoids the "I used a raw hash as a MAC" footgun.
- **Verify is the only authentication path.** Tag comparison via
`==` is documented as a security mistake in the module overview;
the `*_verify` and streaming `verify` methods exist precisely so
callers don't write that code.
- **Tag-length variation is a rejection, not a panic.** Wrong-length
`expected_tag` arguments return `false` (or `Ok(false)`), not a
panic on a length-mismatched compare.
- **No raw key bytes in errors.** `Error::Mac` carries only a
`&'static str` reason — never key material, data, or tag bytes.
- **HMAC `Result` is a contract artifact, not a runtime concern.**
HMAC accepts any key length in practice (long keys are hashed
down internally per RFC 2104). The wrapper returns `Result`
because the upstream `KeyInit::new_from_slice` is fallible by
trait signature, but the `Err` arm is unreachable for HMAC.
---
## Compatibility & build
- **Default features extended.** `default` now includes `mac-blake3`
in addition to `mac-hmac`. A fresh `cargo add crypt-io` ships
with all three MACs available.
- **MSRV** unchanged: Rust 1.85 (edition 2024).
- **New `Error::Mac` variant.** `Error` is `#[non_exhaustive]`, so
match sites with a wildcard arm compile unchanged. The variant
is unreachable in practice.
---
## Installation
```toml
[dependencies]
crypt-io = "0.5"
```
Or:
```bash
cargo add crypt-io
```
---
## Verification
| `cargo fmt --all -- --check` | clean |
| `cargo clippy --all-targets --all-features -- -D warnings` | clean |
| `cargo test --all-features` | 95 unit + 1 smoke + 21 doctest — all passing |
| `cargo doc --no-deps --all-features` (with `RUSTDOCFLAGS="-D warnings"`) | clean |
| HMAC-SHA256 RFC 4231 cases 1 + 2 | byte-exact match |
| HMAC-SHA512 RFC 4231 cases 1 + 2 | byte-exact match |
| BLAKE3 keyed empty-input KAT | byte-exact match |
| Verify-rejection (wrong-key / wrong-data / wrong-tag / wrong-length) | all reject across all three MACs |
| Streaming-equals-one-shot | verified per algorithm at multiple chunk boundaries |
| MSRV (1.85) build | clean |
---
## Acknowledgements
`crypt-io` does not implement cryptographic primitives. The math
new in 0.5.0 comes from:
- [`hmac`](https://crates.io/crates/hmac) — RustCrypto's generic
HMAC implementation, including the constant-time `verify_slice`.
- [`sha2`](https://crates.io/crates/sha2) — the SHA-256 / SHA-512
primitives that drive HMAC.
- [`blake3`](https://crates.io/crates/blake3) — official BLAKE3
implementation, including keyed mode and constant-time
`Hash::eq`.
Carried forward: `chacha20poly1305`, `aes-gcm`, `mod-rand`.
---
## What's next
Phase 0.6.0: KDF. HKDF-SHA256, HKDF-SHA512 for deriving keys from
a master, plus Argon2id for password-derived keys — both with
RFC 5869 / known-answer tests and intentionally-slow parameter
defaults for Argon2id.