CryptGuard v2
crypt_guard is a post-quantum sealing library centered on one safe default flow:
ML-KEM (FIPS 203) -> HKDF -> Authenticated Envelope (HPKE-shaped)
The primary API produces one self-contained CGv2 envelope. Nonce handling,
key derivation, and KEM encapsulation are all internal — the caller provides
a public key and plaintext and receives a sealed Envelope back.
Current release status: 2.0.3. The Phase 4 safe-default upgrade is
implemented, externally consumer-tested, and test-green. This patch release
closes the post-alpha hardening findings for constant-time HMAC verification,
legacy HKDF key derivation, and secret zeroization.
Safe Default: Encryptor / Decryptor
use ;
use XChaCha20Poly1305;
#
use ;
use OsRng;
The Envelope type is a self-describing serialisable blob:
{ header, kem_ciphertext, nonce, ciphertext }. The nonce is
generated internally and bound into the AEAD AAD; callers never
juggle it separately.
What is New in v2
| Area | v1.x | v2 |
|---|---|---|
| KEM | pqcrypto Kyber (NIST Round 3) | ML-KEM (FIPS 203 final) |
| Signing | Falcon / Dilithium | ML-DSA (FIPS 204) + SLH-DSA (FIPS 205) |
| Key schedule | ad-hoc passphrase KDF | HKDF-SHA256/512 with domain-separated labels |
| Envelope | (ciphertext, kyber_secret) tuple |
CGv2 Envelope — one self-describing blob |
| Nonce | caller-managed, separate artifact | nonce embedded and bound inside envelope |
| Type enforcement | decorative content axis | compile-enforced content-axis typestate |
| API shape | macro-first | Encryptor/Decryptor staged builders (macros still work) |
Protocol: HPKE-shaped CGv2 Envelope
crypt_guard v2 follows the structural pattern of HPKE (RFC 9180) as deployed by Cloudflare, AWS, and Google for post-quantum TLS:
Sender:
(kem_ct, shared_secret) = ML-KEM.Encapsulate(recipient_pk)
session_key, base_nonce = HKDF(ikm=shared_secret, salt=kem_ct,
info="crypt_guard:v2:aead:<alg>")
ciphertext = AEAD.Seal(session_key, nonce, aad, plaintext)
envelope = { header, kem_ct, nonce, ciphertext }
Receiver:
shared_secret = ML-KEM.Decapsulate(kem_ct, recipient_sk)
session_key = HKDF(same params)
plaintext = AEAD.Open(session_key, nonce, aad, ciphertext)
The shared secret is zeroized immediately after key derivation, following the
NIST SP 800-227 direction for KEM-based protocols. The header field carries the
algorithm identifiers (kem_id, kdf_id, aead_id) so the
envelope is self-describing and forwards-compatible.
Supported Algorithms
Key Encapsulation (KEM)
| Marker | Algorithm | Security | Standard |
|---|---|---|---|
MlKem512 |
ML-KEM-512 | Category 1 | FIPS 203 |
MlKem768 |
ML-KEM-768 | Category 3 (recommended) | FIPS 203 |
MlKem1024 |
ML-KEM-1024 | Category 5 | FIPS 203 |
Authenticated Encryption (AEAD)
| Marker | Algorithm | Notes |
|---|---|---|
XChaCha20Poly1305 |
XChaCha20-Poly1305 | Default; 24-byte nonce |
AesGcmSiv |
AES-256-GCM-SIV | Nonce-misuse resistant |
Digital Signatures
| Algorithm | Feature | Standard |
|---|---|---|
| ML-DSA-44 / 65 / 87 | ml-dsa-backend (default) |
FIPS 204 |
| SLH-DSA | sign-slhdsa |
FIPS 205 |
Feature Flags
| Flag | Default | What it adds |
|---|---|---|
ml-kem-backend |
yes | ML-KEM-512/768/1024 (FIPS 203) |
ml-dsa-backend |
yes | ML-DSA-44/65/87 (FIPS 204) |
sign-slhdsa |
no | SLH-DSA (FIPS 205) |
aes-ctr |
no | AES-CTR stream cipher |
aes-xts |
no | AES-XTS disk encryption |
archive |
no | tar/xz/gz archive helpers |
legacy-pqclean |
no | Legacy Kyber/Falcon/Dilithium + old tuple API |
To use only the new FIPS path without legacy code:
[]
= { = "2.0.3", = true }
To include the legacy path for reading data encrypted with v1.x:
[]
= { = "2.0.3", = ["legacy-pqclean"] }
Typestate Design: Kyber<Process, Size, Content, Algorithm>
The underlying Kyber<P, S, C, A> type encodes four axes in the type:
| Axis | Variants |
|---|---|
| Process | Encryption, Decryption |
| Size | MlKem512, MlKem768, MlKem1024 |
| Content | Data, Message, Files |
| Algorithm | XChaCha20Poly1305, AesGcmSiv, … |
Calling encrypt_file on a Message instance is a compile error (E0599).
The Encryptor/Decryptor builders use the same underlying type but
expose only the safe Data path by default.
Signing
use OsRng;
use ;
let mut rng = OsRng;
let = keypair?;
let sig = sign?;
verify?;
# Ok::
Architecture
crypt_guard::api (Encryptor / Decryptor — safe entry points)
-> crypt_guard::protocol (CGv2 Envelope + Header + AAD construction)
-> crypt_guard::kem (ML-KEM backend trait + ML-KEM-512/768/1024)
-> crypt_guard::kdf (HKDF-SHA256/512 with domain-separated labels)
-> crypt_guard::core (AEAD wiring; typestate Kyber<P,S,C,A>)
-> crypt_guard::sign (ML-DSA, SLH-DSA)
-> crypt_guard::legacy (pqcrypto Kyber/Falcon/Dilithium — feature-gated)
Legacy Compatibility
If you have data encrypted with crypt_guard v1.x (pqcrypto Kyber, tuple-return
API, manual nonce), enable the legacy-pqclean feature:
[]
= { = "2.0.3", = ["legacy-pqclean"] }
The old Kyber<Encryption, Kyber1024, Message, AES> types, the
encryption! / decryption! macros, the kyber_keypair! macro, and
the tuple-returning encrypt_msg / decrypt_msg functions remain available
under the legacy module and through the crate re-exports.
// Legacy usage (requires --features legacy-pqclean)
use ;
let = kyber_keypair!;
let = encryption!?;
let plaintext = decryption!?;