← Research

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.

GitHub crates.io docs.rs

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_768 or SHA_256, every key an object made from one, and every algorithm family a Cargo feature.
  • no_std with alloc. The crate needs a global allocator and nothing from std.
  • 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.

FamilyStandardParameter setsFeature
ML-KEMFIPS 203ML-KEM-512, ML-KEM-768, ML-KEM-1024ml-kem
FrodoKEMISO/IEC 18033-2:2006/Amd 2:2026FrodoKEM-976 and FrodoKEM-1344, eFrodoKEM-976 and eFrodoKEM-1344, each with AES or SHAKEfrodokem
ML-DSAFIPS 204ML-DSA-44, ML-DSA-65, ML-DSA-87; pure and HashML-DSAml-dsa
SLH-DSAFIPS 205, RFC 990912 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 fixesslh-dsa
HSS/LMSRFC 8554, RFC 9858, NIST SP 800-208LMS with SHA-256 or SHAKE256, m = 24 or 32, heights 5, 10, 15, 20, 25; LM-OTS with w = 1, 2, 4, 8lms
XMSS, XMSSMTRFC 8391, NIST SP 800-208the SP 800-208 sets: SHA-2 or SHAKE256, n = 24 or 32; XMSS heights 10, 16, 20; XMSSMT with 8 height and layer combinationsxmss
SHA-2FIPS 180-4SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256sha2
SHA-3FIPS 202, NIST SP 800-185SHA3-224, SHA3-256, SHA3-384, SHA3-512, SHAKE128, SHAKE256, cSHAKE128, cSHAKE256, KMAC128, KMAC256sha3
BLAKE2RFC 7693BLAKE2b-160, -256, -384, -512; BLAKE2s-128, -160, -224, -256; BLAKE2b-MAC, BLAKE2s-MACblake2
BLAKE3BLAKE3 specificationBLAKE3 (XOF), BLAKE3-256, BLAKE3-MAC, BLAKE3-KDFblake3
AsconNIST SP 800-232Ascon-Hash256, Ascon-XOF128, Ascon-CXOF128ascon
HMACFIPS 198-1HMAC-SHA-224, HMAC-SHA-256, HMAC-SHA-384, HMAC-SHA-512hmac
HKDFRFC 5869HKDF-SHA-256, HKDF-SHA-384, HKDF-SHA-512hkdf

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}…")
}

Output

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}…")
}

Output

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}…")
}

Output

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}…")
}

Output

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}…")
}

Output

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}…")
}

Output

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)
    }
}

Output

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)
    }
}

Output

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)
    }
}

Output

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()
}

Output

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()
}

Output

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()
}

Output

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()
}

Output

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()
}

Output

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()
}

Output

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()
}

Output

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()
}

Output

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()
}

Output

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. unsafe code 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 implements Clone.
  • 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: getrandom on Linux, Android, FreeBSD, NetBSD, DragonFly, illumos and Solaris, getentropy on Apple platforms and OpenBSD, ProcessPrng on Windows, and random_get on WASI preview 1. On a target without one, operations that need randomness return Error::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 _into forms 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-pq

Versions

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.