Skip to main content

Crate tholos_pq

Crate tholos_pq 

Source
Expand description

§tholos-pq

Post-quantum multi-recipient encryption with a versioned CBOR wire format.

§Overview

tholos-pq encrypts messages to multiple recipients using ML-KEM-1024 (Kyber-1024) for key encapsulation, XChaCha20-Poly1305 for symmetric encryption, and Dilithium-3 for sender authentication.

CI Coverage Status crates.io docs.rs License MSRV

§Features

  • Multi-recipient encryption: encrypt once for N recipients
  • Post-quantum algorithms: ML-KEM-1024 and Dilithium-3
  • Sender authentication via Dilithium-3 signatures over the signed inner payload
  • Versioned CBOR wire format for interoperability
  • ML-KEM-1024 and ML-DSA-65 via pure-Rust crates (ml-kem, dilithium-rs); XChaCha20-Poly1305 via chacha20poly1305

§Algorithm Suite

  • Key Encapsulation: ML-KEM-1024 (Kyber-1024) for per-recipient key wrapping
  • Symmetric Encryption: XChaCha20-Poly1305 for payload and CEK encryption
  • Digital Signatures: ML-DSA-65 (Dilithium3) for sender authentication
  • Wire Format: Versioned CBOR (suite = Kyber1024+XChaCha20P1305+MlDsa65)

§Installation

[dependencies]
tholos-pq = "0.3"

§Usage

§Basic Example

use tholos_pq::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (pub_a, priv_a) = gen_recipient_keypair("alice");
    let (pub_b, priv_b) = gen_recipient_keypair("bob");
    let sender = gen_sender_keypair("server1");
    let allowed = vec![(sender.sid.clone(), sender_pub(&sender).pk_dilithium)];

    let message = b"Hello, post-quantum world!";
    let wire = encrypt(message, &sender, &[pub_a.clone(), pub_b.clone()])?;

    let decrypted_a = decrypt(&wire, "alice", &priv_a.sk_kyber, &allowed)?;
    let decrypted_b = decrypt(&wire, "bob", &priv_b.sk_kyber, &allowed)?;
    assert_eq!(decrypted_a, message);
    assert_eq!(decrypted_b, message);
    Ok(())
}

§Multi-Recipient Encryption

use tholos_pq::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let sender = gen_sender_keypair("server1");
    let (pub_a, priv_a) = gen_recipient_keypair("alice");
    let (pub_b, priv_b) = gen_recipient_keypair("bob");
    let (pub_c, priv_c) = gen_recipient_keypair("charlie");
    let allowed = vec![(sender.sid.clone(), sender_pub(&sender).pk_dilithium)];

    let wire = encrypt(
        b"Message for A, B, and C",
        &sender,
        &[pub_a.clone(), pub_b.clone(), pub_c.clone()],
    )?;

    let pt_a = decrypt(&wire, "alice", &priv_a.sk_kyber, &allowed)?;
    let pt_b = decrypt(&wire, "bob", &priv_b.sk_kyber, &allowed)?;
    let pt_c = decrypt(&wire, "charlie", &priv_c.sk_kyber, &allowed)?;
    assert_eq!(pt_a, b"Message for A, B, and C");
    assert_eq!(pt_b, b"Message for A, B, and C");
    assert_eq!(pt_c, b"Message for A, B, and C");
    Ok(())
}

§Sender Authentication

use tholos_pq::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let sender1 = gen_sender_keypair("server1");
    let sender2 = gen_sender_keypair("server2");
    let (pub_key, priv_key) = gen_recipient_keypair("recipient");
    let allowed = vec![(sender1.sid.clone(), sender_pub(&sender1).pk_dilithium)];

    let wire1 = encrypt(b"Hello", &sender1, &[pub_key.clone()])?;
    let pt1 = decrypt(&wire1, "recipient", &priv_key.sk_kyber, &allowed)?;
    assert_eq!(pt1, b"Hello");

    let wire2 = encrypt(b"Hello", &sender2, &[pub_key])?;
    let result = decrypt(&wire2, "recipient", &priv_key.sk_kyber, &allowed);
    assert!(matches!(result, Err(TholosError::BadSignature)));
    Ok(())
}

§Inspecting Headers (Replay Protection)

use tholos_pq::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (pub_key, priv_key) = gen_recipient_keypair("recipient");
    let sender = gen_sender_keypair("server1");
    let allowed = vec![(sender.sid.clone(), sender_pub(&sender).pk_dilithium)];

    let wire = encrypt(b"Hello", &sender, &[pub_key])?;
    let header = verify_header(&wire, &allowed)?;
    // Applications can track header.msg_id and header.timestamp_unix for replay protection.
    let _ = header.msg_id;

    let pt = decrypt(&wire, "recipient", &priv_key.sk_kyber, &allowed)?;
    assert_eq!(pt, b"Hello");
    Ok(())
}

§API Reference

§Key Generation

  • gen_recipient_keypair(kid) -> (RecipientPub, RecipientPriv)
  • gen_sender_keypair(sid) -> SenderKeypair
  • sender_pub(sender) -> SenderPub

§Encryption and Decryption

  • encrypt(plaintext, sender, recipients) -> Result<Vec<u8>, TholosError>
  • decrypt(wire, my_kid, my_sk, allowed_senders) -> Result<Vec<u8>, TholosError>
  • verify_header(wire, allowed_senders) -> Result<Header, TholosError>

§Error Types

  • BadSignature: signature invalid or sender not allowed
  • MissingEnvelope: no envelope for the recipient
  • Malformed: invalid wire field
  • Aead: AEAD failure
  • Ser: CBOR serialization/deserialization failure
  • NoRecipients: encrypt called with an empty recipient list
  • UnsupportedSuite: unsupported version or algorithm suite

§Known Limitations

  • No forward secrecy: recipient ML-KEM keys are long-lived; compromise exposes past messages to that recipient
  • No replay protection: msg_id and timestamp_unix are authenticated but not checked; use verify_header and track msg_id in your application if needed
  • CBOR encoding uses maintained ciborium; signatures cover encoded inner bytes verbatim

§Security Considerations

  • Cryptographic operations use OsRng for randomness
  • Private keys are the caller’s responsibility to protect; CEK/KEK material, ML-KEM decapsulation keys, and ML-DSA secret keys are zeroized on drop
  • The allowed sender list must be managed carefully
  • No forward secrecy: recipient ML-KEM keys are long-lived; compromise exposes past messages to that recipient
  • No replay protection: msg_id and timestamp_unix are authenticated but not checked; use verify_header and track msg_id in your application if needed

§Testing

The crate includes integration tests, property-based tests (proptest), security regression tests, and doctests (this README is included in crate documentation).

cargo test
cargo test --test property
make check

Run the demo:

cargo run --example demo

§Wire Format

The wire format is a versioned CBOR BundleSigned structure:

  • inner: opaque signed CBOR bytes encoding the unsigned bundle
  • sig_dilithium: ML-DSA-65 signature over inner verbatim

The unsigned bundle contains:

  • Header: version, suite, sender, recipient IDs, message ID, timestamp
  • Payload: XChaCha20-Poly1305 ciphertext
  • Recipient Envelopes: per-recipient ML-KEM ciphertexts and wrapped CEKs

§Dependencies

  • ml-kem: pure-Rust ML-KEM-1024
  • dilithium-rs: ML-DSA-65 (Dilithium3)
  • ciborium: CBOR serialization
  • chacha20poly1305: XChaCha20-Poly1305
  • hkdf: key derivation

§License

Licensed under the Apache License, Version 2.0.

§Contributing

Contributions are welcome. Please run make check before submitting a pull request. Post-quantum multi-recipient encryption with a versioned wire format.

Structs§

BundleSigned
Final signed bundle ready for transmission.
BundleUnsigned
Unsigned bundle containing the encrypted message and recipient envelopes.
EncryptOptions
Options for deterministic / injectable encryption parameters.
Header
Message header containing metadata.
RecipientEnvelope
Per-recipient encryption envelope.
RecipientPriv
Recipient private key material.
RecipientPub
Recipient public key information.
SenderKeypair
Sender keypair for signing messages.
SenderPub
Sender public key information.

Enums§

TholosError
Errors that can occur during encryption, decryption, or serialization operations.

Constants§

DILITHIUM3_PK_LEN
ML-DSA-65 / Dilithium3 public key length in bytes.
DILITHIUM3_SIG_LEN
ML-DSA-65 / Dilithium3 detached signature length in bytes.
MLKEM1024_PK_LEN
ML-KEM-1024 public key and ciphertext length in bytes.
SUITE_V1
Versioned algorithm suite identifier for the current wire format.

Functions§

decrypt
Decrypt a message as a recipient and verify the sender’s signature.
encrypt
Encrypt a message for multiple recipients and sign it with the sender’s key.
encrypt_with
Encrypt with caller-supplied RNG, message ID, and timestamp.
gen_recipient_keypair
Generate a new recipient keypair.
gen_recipient_keypair_with
Generate a recipient keypair using the provided RNG (for tests / vectors).
gen_sender_keypair
Generate a new sender keypair (ML-DSA-65).
gen_sender_keypair_deterministic
Generate a sender keypair deterministically from a 32-byte seed (tests / vectors).
sender_keypair_from_bytes
Reconstruct a sender keypair from raw ML-DSA-65 key bytes (for tests / vectors).
sender_pub
Extract the public key information from a sender keypair.
verify_header
Verify a wire bundle and return its authenticated header without decrypting.