Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 362 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ members = [
"packages/pq-bitcoin-taproot/rust",
"packages/pq-dkim/rust",
"packages/pq-webauthn/rust",
"packages/pq-threshold-seal/rust",
]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Every package is implemented in TypeScript and Rust with consistent APIs.
|---------|-------------|-----|-----------|-------|
| [`pq-ecies`](packages/pq-ecies) | ECIES-style encryption with ML-KEM | [![npm](https://img.shields.io/npm/v/pq-ecies?label=&style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/pq-ecies) | [![crates.io](https://img.shields.io/crates/v/pq-ecies?label=&style=flat&colorA=000000&colorB=000000)](https://crates.io/crates/pq-ecies) | — |
| [`pq-hpke`](packages/pq-hpke) | Hybrid Public Key Encryption | [![npm](https://img.shields.io/npm/v/pq-hpke?label=&style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/pq-hpke) | [![crates.io](https://img.shields.io/crates/v/pq-hpke?label=&style=flat&colorA=000000&colorB=000000)](https://crates.io/crates/pq-hpke) | — |
| [`pq-threshold-seal`](packages/pq-threshold-seal) | Experimental ML-KEM threshold sealing envelopes | [![npm](https://img.shields.io/npm/v/pq-threshold-seal?label=&style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/pq-threshold-seal) | [![crates.io](https://img.shields.io/crates/v/pq-threshold-seal?label=&style=flat&colorA=000000&colorB=000000)](https://crates.io/crates/pq-threshold-seal) | Pending |
| [`pq-kem-combiner`](packages/pq-kem-combiner) | Secure multi-KEM combiner | [![npm](https://img.shields.io/npm/v/pq-kem-combiner?label=&style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/pq-kem-combiner) | [![crates.io](https://img.shields.io/crates/v/pq-kem-combiner?label=&style=flat&colorA=000000&colorB=000000)](https://crates.io/crates/pq-kem-combiner) | — |
| [`pq-noise`](packages/pq-noise) | Noise protocol patterns with PQ | [![npm](https://img.shields.io/npm/v/pq-noise?label=&style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/pq-noise) | [![crates.io](https://img.shields.io/crates/v/pq-noise?label=&style=flat&colorA=000000&colorB=000000)](https://crates.io/crates/pq-noise) | — |

Expand Down
23 changes: 23 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 120 additions & 0 deletions packages/pq-threshold-seal/SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# PQ Threshold Seal Version 1

Status: experimental and unaudited.

## Primitive composition

- ML-KEM-768 encapsulates one shared secret per ordered recipient.
- GF(256) Shamir sharing splits a random 32-byte body key.
- ChaCha20-Poly1305 encrypts the body and wraps each share.
- SHAKE256 derives the body-key commitment, wrapping keys, and nonces.

All integer fields are unsigned and big-endian. All byte strings are used
exactly as supplied.

## Binding context

The context is encoded as three consecutive fields in this order:

1. `domain`
2. `session`
3. `roster_hash`

Each field is encoded as a four-byte length followed by its bytes. The context
is external to the envelope. A caller must provide the same context to seal,
unwrap, and open.

## Shamir sharing

The body key is the constant term of 32 independent polynomials over the
Rijndael GF(256) field with reduction polynomial `0x11b`. Recipient positions
are the one-based coordinates `1` through `n`.

Random bytes are consumed in this order:

1. 32-byte body key
2. `k - 1` consecutive 32-byte coefficient arrays
3. one 32-byte ML-KEM encapsulation message for each recipient

This ordering exists to support deterministic compatibility vectors. Production
callers must use a cryptographically secure random source.

## Derivations

`parameters` is the three-byte string `k || n || recipient_index`. The body
uses recipient index zero.

```text
commitment =
SHAKE256("pq-threshold-seal/commit/v1" || context || body_key, 32)

kek =
SHAKE256(
"pq-threshold-seal/kek/v1" ||
context ||
parameters ||
commitment ||
ml_kem_ciphertext ||
ml_kem_shared_secret,
32
)

nonce =
SHAKE256(
"pq-threshold-seal/nonce/v1" ||
purpose ||
context ||
parameters ||
commitment ||
ml_kem_ciphertext,
12
)
```

`purpose` is `pq-threshold-seal/body/v1` for the body or
`pq-threshold-seal/share/v1` for a wrapped share.

The authenticated data is:

```text
purpose || context || parameters || commitment || ml_kem_ciphertext
```

For the body, the ML-KEM ciphertext component is empty.

## Canonical envelope

```text
magic 4 bytes "PQTS"
version 1 byte 0x01
k 1 byte
n 1 byte
body-key commitment 32 bytes
body length 4 bytes
body ciphertext body length bytes

repeated n times:
recipient index 1 byte
ML-KEM ciphertext 1088 bytes
wrapped share 48 bytes
```

The body and each share ciphertext include a 16-byte Poly1305 tag. Recipient
records must appear in ascending one-based order. Decoders reject trailing
bytes, noncanonical indexes, invalid thresholds, short tags, and bodies larger
than 16 MiB.

## Security boundary

The construction does not authenticate recipient public keys and does not
store replay state. Applications must authenticate the ordered roster, commit
to it through `roster_hash`, supply unique session identifiers, and enforce
their replay policy. Independent cryptographic review is required before
production use.

## Compatibility vector

`rust/test-vectors/v1.json` supplies deterministic ML-KEM key-generation seeds and
the exact sealing randomness stream. Both implementations assert the encoded
envelope length, key commitment, and SHA-256 digest. A digest mismatch means
the canonical envelope bytes differ.
30 changes: 30 additions & 0 deletions packages/pq-threshold-seal/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "pq-threshold-seal"
version = "0.0.1"
edition = "2021"
rust-version = "1.78"
description = "Experimental ML-KEM-768 threshold sealing envelopes"
license = "MIT"
readme = "README.md"
keywords = ["post-quantum", "cryptography", "threshold", "ml-kem"]
categories = ["cryptography", "no-std"]

[features]
default = ["std"]
std = ["chacha20poly1305/std", "ml-kem/std", "sha3/std"]

[dependencies]
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc"] }
hybrid-array = "=0.2.0-rc.9"
kem = "=0.3.0-pre.0"
ml-kem = { version = "0.2.3", default-features = false, features = ["deterministic"] }
rand_core = "0.6.4"
sha3 = { version = "0.10.8", default-features = false }
zeroize = { version = "1.8.1", default-features = false, features = ["alloc"] }

[dev-dependencies]
hex = "0.4.3"
rand_chacha = "0.3.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10.9"
49 changes: 49 additions & 0 deletions packages/pq-threshold-seal/rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# pq-threshold-seal

Experimental Rust implementation of a generic `k`-of-`n` post-quantum
threshold sealing envelope.

The body is encrypted once with ChaCha20-Poly1305. Its 32-byte key is split
with GF(256) Shamir secret sharing. Each share is wrapped under a key derived
from an ML-KEM-768 shared secret. SHAKE256 binds every operation to the
threshold, recipient position, ordered roster, and caller-provided context.

```rust
use pq_threshold_seal::{
decap_unwrap_share, reconstruct_and_open, seal, BindingContext, Threshold,
};

let context = BindingContext {
domain: b"example.protocol",
session: b"session-42",
roster_hash: b"application-computed-roster-hash",
};

let sealed = seal(
b"payload",
&recipient_encapsulation_keys,
Threshold::new(2, 3)?,
&context,
&mut rng,
)?;

let first = decap_unwrap_share(decapsulation_key_1, &sealed, 1, &context)?;
let third = decap_unwrap_share(decapsulation_key_3, &sealed, 3, &context)?;
let plaintext = reconstruct_and_open(&[first, third], &sealed, &context)?;
# Ok::<(), pq_threshold_seal::Error>(())
```

## Security status

This package is experimental. The individual primitives are standardized or
conventional, but this composition has not been independently audited. The
package does not provide recipient authentication, committee discovery, key
custody, consensus, gossip, blockchain transaction formats, or replay storage.
Applications must authenticate public keys, compute the ordered `roster_hash`,
provide unique session identifiers, and enforce their own replay policy.

## Compatibility

The canonical `PQTS` version 1 encoding and derivations are shared with the
TypeScript package. See `../SPEC.md` and the shared vectors in
`test-vectors`.
Loading