crypto-pq
Secure, optimized post-quantum cryptography in pure Rust with no dependencies: ML-KEM, FrodoKEM, ML-DSA, SLH-DSA, HSS/LMS and XMSS, plus SHA-2, SHA-3, BLAKE2, BLAKE3, Ascon, HMAC and HKDF.
A quantum computer large enough to run Shor’s algorithm would break RSA and elliptic-curve cryptography, the public-key methods in wide use today. Data recorded now could be decrypted once such a computer exists, so the move to post-quantum algorithms has to start before one does. In August 2024 NIST published its first post-quantum FIPS standards: ML-KEM (FIPS 203) for key establishment, ML-DSA (FIPS 204) and SLH-DSA (FIPS 205) for signatures.
crypto-pq implements those standards in pure Rust, together with FrodoKEM, the stateful
hash-based signatures HSS/LMS, XMSS and XMSSMT, and SHA-2, SHA-3, BLAKE2, BLAKE3,
Ascon, HMAC and HKDF. It is one no_std crate with no dependencies, built to a public
specification that fixes every operation, size, format and error.
- Consistent API. Every algorithm is a constant such as
ML_KEM_768orSHA_256, every key an object made from one, and every algorithm family a Cargo feature. no_stdwithalloc. The crate needs a global allocator and nothing fromstd.- Checked in CI. Valgrind checks the constant-time code on x86-64 and ARM64, and the official test vectors run on every push.
What it provides
The crate covers thirteen algorithm families, each behind its own Cargo feature.
| Family | Standard | Parameter sets | Feature |
|---|---|---|---|
| ML-KEM | FIPS 203 | ML-KEM-512, ML-KEM-768, ML-KEM-1024 | ml-kem |
| FrodoKEM | ISO/IEC 18033-2:2006/Amd 2:2026 | FrodoKEM-976 and FrodoKEM-1344, eFrodoKEM-976 and eFrodoKEM-1344, each with AES or SHAKE | frodokem |
| ML-DSA | FIPS 204 | ML-DSA-44, ML-DSA-65, ML-DSA-87; pure and HashML-DSA | ml-dsa |
| SLH-DSA | FIPS 205, RFC 9909 | 12 sets (SHA2 or SHAKE; 128, 192 or 256; s or f); pure and HashSLH-DSA, and the 12 HashSLH-DSA values with the pre-hash that RFC 9909 fixes | slh-dsa |
| HSS/LMS | RFC 8554, RFC 9858, NIST SP 800-208 | LMS with SHA-256 or SHAKE256, m = 24 or 32, heights 5, 10, 15, 20, 25; LM-OTS with w = 1, 2, 4, 8 | lms |
| XMSS, XMSSMT | RFC 8391, NIST SP 800-208 | the SP 800-208 sets: SHA-2 or SHAKE256, n = 24 or 32; XMSS heights 10, 16, 20; XMSSMT with 8 height and layer combinations | xmss |
| SHA-2 | FIPS 180-4 | SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256 | sha2 |
| SHA-3 | FIPS 202, NIST SP 800-185 | SHA3-224, SHA3-256, SHA3-384, SHA3-512, SHAKE128, SHAKE256, cSHAKE128, cSHAKE256, KMAC128, KMAC256 | sha3 |
| BLAKE2 | RFC 7693 | BLAKE2b-160, -256, -384, -512; BLAKE2s-128, -160, -224, -256; BLAKE2b-MAC, BLAKE2s-MAC | blake2 |
| BLAKE3 | BLAKE3 specification | BLAKE3 (XOF), BLAKE3-256, BLAKE3-MAC, BLAKE3-KDF | blake3 |
| Ascon | NIST SP 800-232 | Ascon-Hash256, Ascon-XOF128, Ascon-CXOF128 | ascon |
| HMAC | FIPS 198-1 | HMAC-SHA-224, HMAC-SHA-256, HMAC-SHA-384, HMAC-SHA-512 | hmac |
| HKDF | RFC 5869 | HKDF-SHA-256, HKDF-SHA-384, HKDF-SHA-512 | hkdf |
ML-KEM, ML-DSA and SLH-DSA keys encode as raw bytes, DER (SubjectPublicKeyInfo, PKCS #8) and PEM; FrodoKEM keys are raw only. HSS/LMS, XMSS and XMSSMT public keys encode as raw bytes, DER and PEM; their private keys live in their state store and are not exported. Section 5 of the specification lists every size.
Getting started
Add the crate to Cargo.toml. It needs Rust 1.88 or later.
[dependencies]
crypto-pq = "1"
The default features enable every algorithm family; hmac enables sha2, and hkdf enables
hmac. To build only some families:
[dependencies.crypto-pq]
version = "1"
default-features = false
features = ["ml-kem", "ml-dsa"]
Every algorithm family has an example below: a complete program, shown with what it prints.
Key encapsulation 2 examples
ML-KEM-768: key encapsulation, confirmed with HMAC-SHA-256
use crypto_pq::{Error, HMAC_SHA_256, KeyFormat, KeyGenOptions, ML_KEM_768};
fn main() -> Result<(), Error> {
let receiver = ML_KEM_768.generate_key_pair(&KeyGenOptions::default())?;
let public_key = receiver.public_key.export_key(KeyFormat::Raw)?;
let sent = receiver.public_key.encapsulate()?;
let received = receiver.private_key.decapsulate(&sent.ciphertext)?;
let tag = HMAC_SHA_256.digest(&sent.shared_secret, b"confirm")?;
let confirmed = HMAC_SHA_256.verify(&received, b"confirm", &tag);
println!("{}", ML_KEM_768.name());
println!("public key {} bytes", public_key.len());
println!(
"ciphertext {} bytes {}",
sent.ciphertext.len(),
fingerprint(&sent.ciphertext)
);
println!("shared secret {} bytes", received.len());
println!("key confirmation {confirmed}");
Ok(())
}
fn fingerprint(bytes: &[u8]) -> String {
let hex: String =
bytes[..8].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}…")
}
ML-KEM-768
public key 1184 bytes
ciphertext 1088 bytes a195686dd715907e…
shared secret 32 bytes
key confirmation true
FrodoKEM-976-SHAKE and its ephemeral variant, eFrodoKEM-976-SHAKE
use crypto_pq::{
EFRODOKEM_976_SHAKE, Error, FRODOKEM_976_SHAKE, HMAC_SHA_256, KeyFormat,
KeyGenOptions,
};
fn main() -> Result<(), Error> {
for kem in [FRODOKEM_976_SHAKE, EFRODOKEM_976_SHAKE] {
let receiver = kem.generate_key_pair(&KeyGenOptions::default())?;
let public_key = receiver.public_key.export_key(KeyFormat::Raw)?;
let sent = receiver.public_key.encapsulate()?;
let received = receiver.private_key.decapsulate(&sent.ciphertext)?;
let tag = HMAC_SHA_256.digest(&sent.shared_secret, b"confirm")?;
let confirmed = HMAC_SHA_256.verify(&received, b"confirm", &tag);
println!("{}", kem.name());
println!("public key {} bytes", public_key.len());
println!(
"ciphertext {} bytes {}",
sent.ciphertext.len(),
fingerprint(&sent.ciphertext)
);
println!("shared secret {} bytes", received.len());
println!("key confirmation {confirmed}");
}
Ok(())
}
fn fingerprint(bytes: &[u8]) -> String {
let hex: String =
bytes[..8].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}…")
}
FrodoKEM-976-SHAKE
public key 15632 bytes
ciphertext 15792 bytes 0046653f5a322de9…
shared secret 24 bytes
key confirmation true
eFrodoKEM-976-SHAKE
public key 15632 bytes
ciphertext 15744 bytes 229d838cc35da212…
shared secret 24 bytes
key confirmation true
Signatures 4 examples
ML-DSA-65: a signature with a context string
use crypto_pq::{
Error, KeyFormat, KeyGenOptions, ML_DSA_65, SignOptions, VerifyOptions,
};
fn main() -> Result<(), Error> {
let pair = ML_DSA_65.generate_key_pair(&KeyGenOptions::default())?;
let public_key = pair.public_key.export_key(KeyFormat::Raw)?;
let options = SignOptions::default().with_context(b"example v1");
let signature = pair.private_key.sign(b"message", &options)?;
let verify = VerifyOptions::default().with_context(b"example v1");
let valid = pair.public_key.verify(&signature, b"message", &verify);
let other = pair.public_key.verify(&signature, b"other", &verify);
println!("{}", ML_DSA_65.name());
println!("public key {} bytes", public_key.len());
println!(
"signature {} bytes {}",
signature.len(),
fingerprint(&signature)
);
println!("verify message {valid}");
println!("verify other {other}");
Ok(())
}
fn fingerprint(bytes: &[u8]) -> String {
let hex: String =
bytes[..8].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}…")
}
ML-DSA-65
public key 1952 bytes
signature 3309 bytes c89b32b6377e7d13…
verify message true
verify other false
HashML-DSA: ML-DSA-65 over a SHA-512 pre-hash of the message
use crypto_pq::{
Error, KeyGenOptions, ML_DSA_65, SHA_512, SignOptions, VerifyOptions,
};
fn main() -> Result<(), Error> {
let pair = ML_DSA_65.generate_key_pair(&KeyGenOptions::default())?;
let options = SignOptions::default().with_pre_hash(SHA_512);
let signature = pair.private_key.sign(b"message", &options)?;
let verify = VerifyOptions::default().with_pre_hash(SHA_512);
let valid = pair.public_key.verify(&signature, b"message", &verify);
let pure = VerifyOptions::default();
let as_pure = pair.public_key.verify(&signature, b"message", &pure);
println!("{} as HashML-DSA with {}", ML_DSA_65.name(), SHA_512.name());
println!(
"signature {} bytes {}",
signature.len(),
fingerprint(&signature)
);
println!("verify message {valid}");
println!("verify as pure {as_pure}");
Ok(())
}
fn fingerprint(bytes: &[u8]) -> String {
let hex: String =
bytes[..8].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}…")
}
ML-DSA-65 as HashML-DSA with SHA-512
signature 3309 bytes 5d726f0100b52495…
verify message true
verify as pure false
SLH-DSA-SHA2-128s: a hash-based signature
use crypto_pq::{
Error, KeyFormat, KeyGenOptions, SLH_DSA_SHA2_128S, SignOptions,
VerifyOptions,
};
fn main() -> Result<(), Error> {
let pair =
SLH_DSA_SHA2_128S.generate_key_pair(&KeyGenOptions::default())?;
let public_key = pair.public_key.export_key(KeyFormat::Raw)?;
let signature =
pair.private_key.sign(b"message", &SignOptions::default())?;
let verify = VerifyOptions::default();
let valid = pair.public_key.verify(&signature, b"message", &verify);
let other = pair.public_key.verify(&signature, b"other", &verify);
println!("{}", SLH_DSA_SHA2_128S.name());
println!("public key {} bytes", public_key.len());
println!(
"signature {} bytes {}",
signature.len(),
fingerprint(&signature)
);
println!("verify message {valid}");
println!("verify other {other}");
Ok(())
}
fn fingerprint(bytes: &[u8]) -> String {
let hex: String =
bytes[..8].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}…")
}
SLH-DSA-SHA2-128s
public key 32 bytes
signature 7856 bytes 93290bc891ab39a8…
verify message true
verify other false
HashSLH-DSA: the SHA2-128s set with the SHA-256 pre-hash of RFC 9909
use crypto_pq::{
Error, HASH_SLH_DSA_SHA2_128S_WITH_SHA256, KeyGenOptions, SignOptions,
VerifyOptions,
};
fn main() -> Result<(), Error> {
let algorithm = HASH_SLH_DSA_SHA2_128S_WITH_SHA256;
let pair = algorithm.generate_key_pair(&KeyGenOptions::default())?;
let signature =
pair.private_key.sign(b"message", &SignOptions::default())?;
let verify = VerifyOptions::default();
let valid = pair.public_key.verify(&signature, b"message", &verify);
let other = pair.public_key.verify(&signature, b"other", &verify);
println!("{}", algorithm.name());
println!(
"signature {} bytes {}",
signature.len(),
fingerprint(&signature)
);
println!("verify message {valid}");
println!("verify other {other}");
Ok(())
}
fn fingerprint(bytes: &[u8]) -> String {
let hex: String =
bytes[..8].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}…")
}
HashSLH-DSA-SHA2-128s-with-SHA256
signature 7856 bytes e2173dc1d76ded7b…
verify message true
verify other false
Stateful signatures 3 examples
HSS/LMS: one LMS tree of height 5, 32 signatures, its state in a store
use crypto_pq::{
Error, HSS_LMS, KeyFormat, StateStore, StatefulKeyGenOptions,
StatefulParameters,
};
fn main() -> Result<(), Error> {
let levels = [("LMS_SHA256_M32_H5", "LMOTS_SHA256_N32_W8")];
let mut pair = HSS_LMS.generate_key_pair(
StatefulParameters::Levels(&levels),
MemoryStore::default(),
&StatefulKeyGenOptions::default(),
)?;
let public_key = pair.public_key.export_key(KeyFormat::Raw)?;
let signature = pair.private_key.sign(b"message")?;
let valid = pair.public_key.verify(&signature, b"message");
println!("{} with LMS_SHA256_M32_H5", HSS_LMS.name());
println!("public key {} bytes", public_key.len());
println!("signature {} bytes", signature.len());
println!("verify message {valid}");
let left = pair.private_key.remaining_signatures();
println!("signatures left {left}");
Ok(())
}
#[derive(Default)]
struct MemoryStore(Option<Vec<u8>>);
impl StateStore for MemoryStore {
fn read(&mut self) -> Result<Option<Vec<u8>>, Error> {
Ok(self.0.clone())
}
fn update(
&mut self,
previous: Option<&[u8]>,
next: &[u8],
) -> Result<bool, Error> {
if self.0.as_deref() != previous {
return Ok(false);
}
self.0 = Some(next.to_vec());
Ok(true)
}
}
HSS/LMS with LMS_SHA256_M32_H5
public key 60 bytes
signature 1296 bytes
verify message true
signatures left 31
XMSS: one tree of height 10, 1024 signatures, its state in a store
use crypto_pq::{
Error, KeyFormat, StateStore, StatefulKeyGenOptions, StatefulParameters,
XMSS,
};
fn main() -> Result<(), Error> {
let mut pair = XMSS.generate_key_pair(
StatefulParameters::Name("XMSS-SHA2_10_256"),
MemoryStore::default(),
&StatefulKeyGenOptions::default(),
)?;
let public_key = pair.public_key.export_key(KeyFormat::Raw)?;
let signature = pair.private_key.sign(b"message")?;
let valid = pair.public_key.verify(&signature, b"message");
println!("{} with XMSS-SHA2_10_256", XMSS.name());
println!("public key {} bytes", public_key.len());
println!("signature {} bytes", signature.len());
println!("verify message {valid}");
let left = pair.private_key.remaining_signatures();
println!("signatures left {left}");
Ok(())
}
#[derive(Default)]
struct MemoryStore(Option<Vec<u8>>);
impl StateStore for MemoryStore {
fn read(&mut self) -> Result<Option<Vec<u8>>, Error> {
Ok(self.0.clone())
}
fn update(
&mut self,
previous: Option<&[u8]>,
next: &[u8],
) -> Result<bool, Error> {
if self.0.as_deref() != previous {
return Ok(false);
}
self.0 = Some(next.to_vec());
Ok(true)
}
}
XMSS with XMSS-SHA2_10_256
public key 68 bytes
signature 2500 bytes
verify message true
signatures left 1023
XMSSMT: two layers of trees, 220 signatures, its state in a store
use crypto_pq::{
Error, KeyFormat, StateStore, StatefulKeyGenOptions, StatefulParameters,
XMSS_MT,
};
fn main() -> Result<(), Error> {
let parameters = "XMSSMT-SHA2_20/2_256";
let mut pair = XMSS_MT.generate_key_pair(
StatefulParameters::Name(parameters),
MemoryStore::default(),
&StatefulKeyGenOptions::default(),
)?;
let public_key = pair.public_key.export_key(KeyFormat::Raw)?;
let signature = pair.private_key.sign(b"message")?;
let valid = pair.public_key.verify(&signature, b"message");
println!("{parameters}");
println!("public key {} bytes", public_key.len());
println!("signature {} bytes", signature.len());
println!("verify message {valid}");
let left = pair.private_key.remaining_signatures();
println!("signatures left {left}");
Ok(())
}
#[derive(Default)]
struct MemoryStore(Option<Vec<u8>>);
impl StateStore for MemoryStore {
fn read(&mut self) -> Result<Option<Vec<u8>>, Error> {
Ok(self.0.clone())
}
fn update(
&mut self,
previous: Option<&[u8]>,
next: &[u8],
) -> Result<bool, Error> {
if self.0.as_deref() != previous {
return Ok(false);
}
self.0 = Some(next.to_vec());
Ok(true)
}
}
XMSSMT-SHA2_20/2_256
public key 68 bytes
signature 4963 bytes
verify message true
signatures left 1048575
Hashes and XOFs 5 examples
SHA-256 and SHA-512/256 of "abc", in one call and in parts
use crypto_pq::{SHA_256, SHA_512_256};
fn main() {
let digest = SHA_256.digest(b"abc");
let mut hasher = SHA_256.create();
hasher.update(b"a");
hasher.update(b"bc");
let in_parts = hasher.digest();
println!("{}", SHA_256.name());
println!("digest {}", hex(&digest));
println!("in parts {}", hex(&in_parts));
println!("{}", SHA_512_256.name());
println!("digest {}", hex(&SHA_512_256.digest(b"abc")));
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
SHA-256
digest ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
in parts ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
SHA-512/256
digest 53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23
SHA3-256, SHAKE256 and cSHAKE128 of "abc"
use crypto_pq::{CSHAKE128, Error, SHA3_256, SHAKE256, XofOptions};
fn main() -> Result<(), Error> {
let digest = SHA3_256.digest(b"abc");
let output = SHAKE256.digest(b"abc", 32)?;
let options =
XofOptions::default().with_customization(b"Email Signature");
let custom = CSHAKE128.configure(&options)?.digest(b"abc", 32)?;
println!("{}", SHA3_256.name());
println!("digest {}", hex(&digest));
println!("{}", SHAKE256.name());
println!("output {}", hex(&output));
println!("{}, customization \"Email Signature\"", CSHAKE128.name());
println!("output {}", hex(&custom));
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
SHA3-256
digest 3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532
SHAKE256
output 483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739
cSHAKE128, customization "Email Signature"
output d7a9b8839577422dbe3ef0bdd0cc835090552f62a53d4f354b6ba618c6c22b2b
BLAKE2b-256 and BLAKE2s-256 of "abc", and a personalized BLAKE2b
use crypto_pq::{BLAKE2B_256, BLAKE2S_256, Error, HashOptions};
fn main() -> Result<(), Error> {
let blake2b = BLAKE2B_256.digest(b"abc");
let blake2s = BLAKE2S_256.digest(b"abc");
let options = HashOptions::default().with_personalization(b"example");
let personalized = BLAKE2B_256.configure(&options)?.digest(b"abc");
println!("{}", BLAKE2B_256.name());
println!("digest {}", hex(&blake2b));
println!("{}", BLAKE2S_256.name());
println!("digest {}", hex(&blake2s));
println!("{}, personalization \"example\"", BLAKE2B_256.name());
println!("digest {}", hex(&personalized));
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
BLAKE2b-256
digest bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319
BLAKE2s-256
digest 508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982
BLAKE2b-256, personalization "example"
digest 0fd7e1dbde932019dad2db349a2ef40f53fa40294a90755cfa5338a9df2cbeb8
BLAKE3-256 of "abc", and BLAKE3 read as an XOF
use crypto_pq::{BLAKE3, BLAKE3_256, Error};
fn main() -> Result<(), Error> {
let digest = BLAKE3_256.digest(b"abc");
let mut xof = BLAKE3.create();
xof.update(b"abc")?;
let first = xof.read(32)?;
let next = xof.read(32)?;
println!("{}", BLAKE3_256.name());
println!("digest {}", hex(&digest));
println!("{} as an XOF", BLAKE3.name());
println!("first 32 {}", hex(&first));
println!("next 32 {}", hex(&next));
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
BLAKE3-256
digest 6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85
BLAKE3 as an XOF
first 32 6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85
next 32 1fb250ae7393f5d02813b65d521a0d492d9ba09cf7ce7f4cffd900f23374bf0b
Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 of "abc"
use crypto_pq::{
ASCON_CXOF128, ASCON_HASH256, ASCON_XOF128, Error, XofOptions,
};
fn main() -> Result<(), Error> {
let digest = ASCON_HASH256.digest(b"abc");
let output = ASCON_XOF128.digest(b"abc", 32)?;
let options = XofOptions::default().with_customization(b"example");
let custom = ASCON_CXOF128.configure(&options)?.digest(b"abc", 32)?;
println!("{}", ASCON_HASH256.name());
println!("digest {}", hex(&digest));
println!("{}", ASCON_XOF128.name());
println!("output {}", hex(&output));
println!("{}, customization \"example\"", ASCON_CXOF128.name());
println!("output {}", hex(&custom));
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
Ascon-Hash256
digest 45aa03431c3c829b3b066f33e844b0cc4d20a45af92d3dcfdf34f40fc20935cf
Ascon-XOF128
output b87198613d724232505baa68187f925708c009fe6ec13d19ce3c7aa6b20b2f0b
Ascon-CXOF128, customization "example"
output b5a651b2ffe9d220c9b0b9a00f10287ef0eab4929b241ceb8d6222802c356bdc
MACs and KDFs 4 examples
HMAC-SHA-256: a tag, checked in constant time
use crypto_pq::{Error, HMAC_SHA_256};
fn main() -> Result<(), Error> {
let key = [7; 32];
let tag = HMAC_SHA_256.digest(&key, b"message")?;
let valid = HMAC_SHA_256.verify(&key, b"message", &tag);
let altered = HMAC_SHA_256.verify(&key, b"Message", &tag);
println!("{}", HMAC_SHA_256.name());
println!("tag {}", hex(&tag));
println!("verify {valid}");
println!("altered {altered}");
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
HMAC-SHA-256
tag bca40e084cc929a220c1452dfe13886aebeb9abc2f55ff5154c78b024be21064
verify true
altered false
KMAC128 with a customization string
use crypto_pq::{Error, KMAC128, MacOptions};
fn main() -> Result<(), Error> {
let key = [7; 32];
let options = MacOptions::default().with_customization(b"example");
let kmac = KMAC128.configure(&options)?;
let tag = kmac.digest(&key, b"message")?;
let valid = kmac.verify(&key, b"message", &tag);
println!("{}, customization \"example\"", KMAC128.name());
println!("tag {}", hex(&tag));
println!("verify {valid}");
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
KMAC128, customization "example"
tag eaf0a341513c00fa363b059090533545f61377193e04ac1c1d259434b661efa0
verify true
HKDF-SHA-256: two keys from one secret, one for each info string
use crypto_pq::{Error, HKDF_SHA_256, KdfOptions};
fn main() -> Result<(), Error> {
let secret = b"input keying material";
let salted = KdfOptions::default().with_salt(b"salt");
let encrypt =
HKDF_SHA_256.derive(secret, 32, &salted.with_info(b"encrypt"))?;
let mac = HKDF_SHA_256.derive(secret, 32, &salted.with_info(b"mac"))?;
println!("{}, salt \"salt\"", HKDF_SHA_256.name());
println!("encrypt {}", hex(&encrypt));
println!("mac {}", hex(&mac));
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
HKDF-SHA-256, salt "salt"
encrypt 45fd815d7cce6e7ad10040e295f5a5607ff7a45d87d2e8ff0f062c49f02a2905
mac be8fbbbb38b96ce9ad0decbf5d495b19ce82df70a6366224493fd71fb88109b2
BLAKE3-MAC and BLAKE3-KDF: a keyed tag and a derived key
use crypto_pq::{BLAKE3_KDF, BLAKE3_MAC, Error, KdfOptions};
fn main() -> Result<(), Error> {
let key = [7; 32];
let tag = BLAKE3_MAC.digest(&key, b"message")?;
let valid = BLAKE3_MAC.verify(&key, b"message", &tag);
let context = "example.com 2026-10-25 session keys v1";
let options = KdfOptions::default().with_info(context.as_bytes());
let derived =
BLAKE3_KDF.derive(b"input keying material", 32, &options)?;
println!("{}", BLAKE3_MAC.name());
println!("tag {}", hex(&tag));
println!("verify {valid}");
println!("{}, context \"{context}\"", BLAKE3_KDF.name());
println!("key {}", hex(&derived));
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
BLAKE3-MAC
tag 1e9ebc6f3294d7315b443bdf6d9814ab1623206b854a20efbf4e3677dda920fd
verify true
BLAKE3-KDF, context "example.com 2026-10-25 session keys v1"
key fddc86e153dd312f744aca799f1a3f76ce01a6c9d5a9f965ec2971b7c0e51ef8
Each output is what the program printed when it was built and run against crypto-pq 1.0.0. The ciphertext and signature fingerprints differ on every run, so Run draws them fresh.
The API documentation covers every operation, including the
stateful signatures with their StateStore and the opt-in hazmat feature for test vectors. The
README’s usage requirements state
the rules that are part of the API contract.
How it is built
- Pure Rust, no dependencies. Every algorithm is implemented in the crate, with no build
script.
unsafecode is confined to the entropy source, the wipe, one-time initialization and the CPU modules, plus the valgrind requests of the constant-time check build, and each block states why it is sound. - Constant-time code. Code that handles secret data has no branch or memory access that depends on a secret. CI checks this with valgrind memcheck, every secret marked as undefined, on x86-64 and ARM64, in the default and the portable build.
- Data-independent timing. On ARM64 CPUs with FEAT_DIT, the asymmetric secret operations run
with DIT set;
enable_data_independent_timing()extends it to MACs and KDFs. - Wiping. Private keys and
SecretBytes, the type of every secret output, wipe their memory when dropped, with writes that the compiler cannot remove. Neither implementsClone. - Self-tests. Key generation runs a pairwise consistency test by default, private-key import validates the key, and every stateful signature is verified before it is released.
- Randomness comes only from the operating system:
getrandomon Linux, Android, FreeBSD, NetBSD, DragonFly, illumos and Solaris,getentropyon Apple platforms and OpenBSD,ProcessPrngon Windows, andrandom_geton WASI preview 1. On a target without one, operations that need randomness returnError::RngFailure. - CPU paths. Kernels for x86-64 (AVX2, SSE4.1, SHA-NI, AES-NI) and ARM64 (NEON, AES, SHA-2, SHA-3, SHA-512) are selected at run time, with portable code on every target and no target CPU flag needed.
- Allocation. Every operation is synchronous. Fixed-size operations allocate only their
returned value, and the
_intoforms allocate nothing.
Built to a specification
crypto-pq implements a public specification written in RFC style: the API, the algorithms, the key formats, the errors, the security requirements and the conformance rules. Its Rust profile maps it to this crate, and a decision log records the reasons for its choices.
The repository carries the compliance suite: NIST ACVP and CAVP vectors, C2SP Wycheproof vectors, RFC test vectors, Ascon, BLAKE2, BLAKE3 and FrodoKEM known-answer tests, XMSS vectors from the RFC 8391 reference implementation, and cross-check files that cover every error case, option and format. CI runs the suite on every push and checks every file against its SHA-256; the full ACVP sets and the large-data tests run weekly.
CI builds and tests the crate on Linux x86-64 and ARM64, macOS ARM64, Windows x86-64 and ARM64 and Linux musl, and under emulation on 32-bit x86, RISC-V 64 and big-endian s390x, with stable Rust, and on Linux x86-64 with Rust 1.88 as well.
Releases that can be checked
Releases are published from signed tags rust-vX.Y.Z on main. The crate is built on x86-64
and ARM64 hosts and the two builds are compared byte for byte; it carries a build provenance
attestation and is published through crates.io trusted publishing.
The release page has the
crate and its SHA256SUMS. With the GitHub CLI, a downloaded crate is checked against its
attestation:
gh attestation verify crypto-pq-1.0.0.crate --repo crypterial/crypto-pqVersions
The crate follows Semantic Versioning, and its MAJOR.MINOR is the version of the specification
that it implements: every 1.0.x release implements specification 1.0. Within 1.x, no public item
is removed or renamed and no specified behaviour changes; a minor release can add algorithms,
options, operations and error codes, which is why Error is non-exhaustive.
Security fixes are released as a new PATCH version of the latest MAJOR.MINOR, and only the latest release is supported. The 1.0.0 announcement on the blog introduces the release, and the changelog lists the changes in each release.
License and contact
crypto-pq is licensed under either the Apache License, Version 2.0 or the MIT license, at your option.
- Source and specification: github.com/crypterial/crypto-pq
- API documentation: docs.rs/crypto-pq
- Package: crates.io/crates/crypto-pq
- Security reports: privately through GitHub Security Advisories
- Everything else: hello@crypterial.com