Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- name: Check
run: cargo check --all-features

- name: Check default features (McEliece excluded)
run: cargo check

audit:
name: Security Audit
runs-on: ubuntu-latest
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-09-14

### Added

- XEdDSA view for X25519 keys (`views::xeddsa`): an Ed25519-form signature over a 32-byte message that verifies against the Edwards form of the X25519 public key. Signatures carry the `XeddsaMsig` codec. Includes strict verification, small-order key rejection, and known-answer tests.
- `deprecated` cargo feature (default off). Restores the Classic McEliece support described under Deprecated below.

### Changed

- BREAKING. Classic McEliece (`Mceliece348864Pub`/`Priv`) and the X25519-McEliece-348864 hybrid moved behind the `deprecated` cargo feature. Default builds contain no McEliece code and no `mceliece348864` dependency. Default builds still decode stored McEliece multikeys, but `Builder::new_from_random_bytes` fails for McEliece codecs with `UnsupportedCodec`, and the McEliece views reject them.
- `multi-codec` dependency raised from `1.3` to `1.5` (adds the McEliece codec deprecation attributes).

### Deprecated

- The Classic McEliece key views, the `X25519-McEliece-348864` hybrid view, and the `MCELIECE_KEY_CODECS` constant. Key-recovery attacks now solve the TII McEliece challenges; see [tii-solved](https://github.com/mjosaarinen/tii-solved) for the recovered keys. Enable the `deprecated` feature to keep using them.

## [1.2.2] - 2026-09-01

### Fixed
Expand Down
13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "multi-key"
version = "1.2.2"
version = "2.0.0"
edition = "2024"
rust-version = "1.96"
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]
Expand All @@ -21,6 +21,11 @@ wasm = ["getrandom/wasm_js"]
legacy_chacha20_fallback = []
lamport = ["dep:lamport_signature_plus", "dep:sha3", "dep:blake2", "dep:shake"]
xmss = ["dep:xmss"]
# Deprecated Classic McEliece KEM support (key recovery attacks now solve the
# TII McEliece challenges; see https://github.com/mjosaarinen/tii-solved).
# Excluded from the default build; enable to keep the McEliece key views and
# their tests compiling.
deprecated = ["dep:mceliece348864"]
# Tests that take over 60 seconds in debug builds (SLH-DSA, Classic McEliece,
# XMSS). Excluded from the default `cargo test` run; enable with
# `cargo test --features slow-tests` to run the full suite.
Expand All @@ -43,13 +48,13 @@ getrandom = { version = "0.4", features = ["sys_rng"] }
hex = "0.4"
hkdf = "0.13"
k256 = { version = "0.14", features = ["ecdh"] }
mceliece348864 = { version = "1.0" }
mceliece348864 = { version = "1.0", optional = true }
ml-dsa = "0.1.1"
ml-kem = "0.3"
multi-base = { version = "1.0", default-features = false }
multi-codec = "1.3"
multi-codec = "1.5"
multi-hash = "1.1"
multi-sig = "1.3"
multi-sig = "1.4"
multi-trait = { version = "1.0", default-features = false }
multi-util = "1.1"
p256 = { version = "0.14", features = ["ecdsa", "ecdh"] }
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

# Multi-Key

A Rust implementation of the [multiformats][MULTIFORMATS] [multikey specification][MULTIKEY] and [nonce specification][NONCE]. The published crate is **`multi-key`**. Depend on it as `multi-key = "1.0"` in `Cargo.toml`. Import it as `multi_key` in Rust, for example `use multi_key::Builder;`.
A Rust implementation of the [multiformats][MULTIFORMATS] [multikey specification][MULTIKEY] and [nonce specification][NONCE]. The published crate is **`multi-key`**. Depend on it as `multi-key = "2"` in `Cargo.toml`. Import it as `multi_key` in Rust, for example `use multi_key::Builder;`.

## Current Status

Expand All @@ -14,8 +14,9 @@ This implementation of the multikey specification supports an extensive set of p
- **Classical signing** — Ed25519, secp256k1, NIST P-256/P-384/P-521, RSA-2048/3072/4096,
and BLS12-381 G1/G2.
- **Post-quantum signing** — FN-DSA, ML-DSA, MAYO, and SLH-DSA (all parameter sets).
- **Key encapsulation / key agreement** — X25519, ML-KEM, sntrup, Classic McEliece,
FrodoKEM, and the BLS12-381 TimeCrypt pairing-based KEM.
- **Key encapsulation / key agreement** — X25519, ML-KEM, sntrup, Classic McEliece
(deprecated; behind the `deprecated` feature), FrodoKEM, and the BLS12-381
TimeCrypt pairing-based KEM.
- **Hybrid signing** — combinations of Ed25519 or BLS12-381 G1 with a PQ signing scheme.
- **Hybrid KEMs** — combinations of X25519 with a PQ KEM.
- **Secret-key / symmetric** — ChaCha20-Poly1305 keys.
Expand Down Expand Up @@ -73,14 +74,18 @@ identifiers come from the [multicodec][MULTICODEC] registry and are surfaced as
| MAYO | `Mayo1Pub`/`Priv`, `Mayo2Pub`/`Priv`, `Mayo3Pub`/`Priv`, `Mayo5Pub`/`Priv` | 1, 2, 3, 5 |
| SLH-DSA | `SlhdsaSha2128FPub`/`Priv`, `SlhdsaSha2128SPub`/`Priv`, `SlhdsaSha2192FPub`/`Priv`, `SlhdsaSha2192SPub`/`Priv`, `SlhdsaSha2256FPub`/`Priv`, `SlhdsaSha2256SPub`/`Priv`, `SlhdsaShake128FPub`/`Priv`, `SlhdsaShake128SPub`/`Priv`, `SlhdsaShake192FPub`/`Priv`, `SlhdsaShake192SPub`/`Priv`, `SlhdsaShake256FPub`/`Priv`, `SlhdsaShake256SPub`/`Priv` | 12 sets: SHA-2/SHAKE × 128/192/256 × F/S |

### Deprecated Algorithms

Classic McEliece (the `Mceliece348864Pub`/`Priv` codecs) and the X25519-McEliece-348864 hybrid are **deprecated**. Key-recovery attacks now solve the TII McEliece challenges; see [tii-solved](https://github.com/mjosaarinen/tii-solved) for the recovered keys. The McEliece views compile only with the `deprecated` cargo feature and are excluded from the default build. Stored McEliece multikeys still decode in default builds, but `Builder::new_from_random_bytes` fails for McEliece codecs with `UnsupportedCodec`.

### KEMs / Key Agreement

| Algorithm | Codecs | Notes |
|---|---|---|
| X25519 | `X25519Pub` / `X25519Priv` | ECDH; returns ephemeral public key from `seal` |
| ML-KEM | `Mlkem768Pub`/`Priv`, `Mlkem1024Pub`/`Priv` | 768, 1024 |
| sntrup | `Sntrup761Pub`/`Priv`, `Sntrup857Pub`/`Priv`, `Sntrup953Pub`/`Priv`, `Sntrup1013Pub`/`Priv`, `Sntrup1277Pub`/`Priv` | 761, 857, 953, 1013, 1277 |
| Classic McEliece | `Mceliece348864Pub` / `Mceliece348864Priv` | 348864 |
| Deprecated Classic McEliece | `Mceliece348864Pub` / `Mceliece348864Priv` | 348864; behind the `deprecated` feature |
| FrodoKEM | `FrodoKem640AesPub`/`Priv`, `FrodoKem976AesPub`/`Priv`, `FrodoKem1344AesPub`/`Priv`, `FrodoKem640ShakePub`/`Priv`, `FrodoKem976ShakePub`/`Priv`, `FrodoKem1344ShakePub`/`Priv` | 640/976/1344 × AES/SHAKE |
| BLS12-381 TimeCrypt | (uses the G1/G2 codecs above) | Pairing-based KEM built into the BLS views |

Expand All @@ -103,7 +108,7 @@ identifiers come from the [multicodec][MULTICODEC] registry and are surfaced as
| X25519-sntrup761 | `X25519Sntrup761Pub` / `X25519Sntrup761Priv` | X25519 + sntrup761 |
| X25519-ML-KEM-768 | `X25519Mlkem768Pub` / `X25519Mlkem768Priv` | X25519 + ML-KEM-768 |
| X25519-FrodoKEM-640 | `X25519Frodokem640AesPub`/`Priv`, `X25519Frodokem640ShakePub`/`Priv` | X25519 + FrodoKEM-640 (AES/SHAKE) |
| X25519-McEliece-348864 | `X25519Mceliece348864Pub` / `X25519Mceliece348864Priv` | X25519 + Classic McEliece 348864 |
| Deprecated X25519-McEliece-348864 | `X25519Mceliece348864Pub` / `X25519Mceliece348864Priv` | X25519 + Classic McEliece 348864; behind the `deprecated` feature |

### Threshold Key Shares

Expand Down Expand Up @@ -190,7 +195,7 @@ The import direction (`Builder::new_from_ssh_public_key` and `Builder::new_from_

### Key types that do not support SSH conversion

All KEM-only and hybrid key types explicitly reject SSH conversion and return `UnsupportedAlgorithm`. These include X25519, ML-KEM, all sntrup sizes, Classic McEliece, all FrodoKEM variants, the BLS12-381 TimeCrypt KEM, and all hybrid signing and hybrid KEM schemes.
All KEM-only and hybrid key types explicitly reject SSH conversion and return `UnsupportedAlgorithm`. These include X25519, ML-KEM, all sntrup sizes, Classic McEliece (deprecated), all FrodoKEM variants, the BLS12-381 TimeCrypt KEM, and all hybrid signing and hybrid KEM schemes.

## Threshold Operations

Expand All @@ -209,7 +214,7 @@ The DKG share codecs (`Ed25519Thresh*`, `P256Thresh*`, `P384Thresh*`, `Secp256K1
- **Feldman VSS** — secp256k1, P-256/P-384/P-521, BLS12-381 G1/G2 (verifiable, with
commitments).
- **gf256 byte-sharing** — RSA and all PQ families (ML-DSA, ML-KEM, SLH-DSA, FN-DSA, MAYO,
sntrup, FrodoKEM, Classic McEliece) and all hybrids.
sntrup, FrodoKEM, Classic McEliece (deprecated)) and all hybrids.
- **Dual mode** — Ed25519 and X25519: a gf256 share of the 32-byte seed (exact restore)
plus a Feldman scalar share (threshold-signing-ready).

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The `Multikey` comment field is stored as a plain `String`. It is not zeroized o

## Hybrid KEM Combiner Hash (M3)

The AEAD-key KDF is unified across all four hybrid KEMs. It uses HKDF-SHA512 via the shared `aead::derive_aead_key` helper. The secret-combiner hash is not unified. `x25519_mlkem768` uses SHA-512. The other three hybrid KEMs (`x25519_sntrup761`, `x25519_frodokem640`, `x25519_mceliece348864`) use BLAKE3. Both constructions are cryptographically sound. The split is accepted. The combiner hash feeds into HKDF-SHA512, which accepts arbitrary input length.
The AEAD-key KDF is unified across all four hybrid KEMs. It uses HKDF-SHA512 via the shared `aead::derive_aead_key` helper. The secret-combiner hash is not unified. `x25519_mlkem768` uses SHA-512. The other three hybrid KEMs (`x25519_sntrup761`, `x25519_frodokem640`, `x25519_mceliece348864`) use BLAKE3. Both constructions are cryptographically sound. The split is accepted. The combiner hash feeds into HKDF-SHA512, which accepts arbitrary input length. Note that `x25519_mceliece348864` is deprecated (behind the `deprecated` feature) because key-recovery attacks now solve the TII McEliece challenges; see https://github.com/mjosaarinen/tii-solved. The combiner note above is historical context for code still compiled with that feature.

## Decoded-Size Caps

Expand Down
2 changes: 2 additions & 0 deletions src/keysplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,8 @@ mod tests {
assert_all(&mk::SNTRUP_KEY_CODECS);
}

#[cfg(feature = "deprecated")]
#[allow(deprecated)]
#[test]
fn roundtrip_mceliece() {
assert_all(&mk::MCELIECE_KEY_CODECS);
Expand Down
12 changes: 12 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@
//! AEAD was added. Disabled by default — AEAD failure is a hard error so
//! unauthenticated ciphertext is never returned as if it were valid. Enable
//! only to migrate pre-AEAD keystores; a warning is emitted on every fallback.
//! - **`deprecated`** (default off): Enables the deprecated Classic McEliece
//! support (see the Deprecations section below).
//!
//! ## Deprecations
//!
//! Classic McEliece (the `Mceliece348864Pub`/`Priv` codecs and the
//! `X25519-McEliece-348864` hybrid) is deprecated. Key-recovery attacks now
//! solve the TII McEliece challenges; see
//! <https://github.com/mjosaarinen/tii-solved> for the recovered keys. The
//! McEliece views compile only with the `deprecated` feature. Stored
//! McEliece multikeys still decode in default builds, but key generation
//! fails with `UnsupportedCodec`.
//!
//! ## Security
//!
Expand Down
49 changes: 45 additions & 4 deletions src/mk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ use crate::views::lamport;
use crate::views::lamport_merkle;
#[cfg(feature = "xmss")]
use crate::views::xmss;
#[cfg(feature = "deprecated")]
use crate::views::{classic_mceliece, x25519_mceliece348864};
use crate::{
AttrId, AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView,
KdfAttrView, KdfView, MerkleStateView, OpenView, SealView, SignView, ThresholdAttrView,
ThresholdDisclosureView, ThresholdKeyView, ThresholdView, VerifyView, Views,
error::{AttributesError, CipherError, ConversionsError, KdfError, SealError, ThresholdError},
views::{
bcrypt, bls12381, bls12381_g1_fndsa512, bls12381_g1_mayo1, bls12381_g1_mayo2,
bls12381_g1_mldsa65, chacha20, classic_mceliece, ed25519, ed25519_fndsa512, ed25519_mayo2,
ed25519_mldsa65, fn_dsa, frodokem, mayo, ml_dsa, ml_kem, nist_p, rsa, secp256k1, slh_dsa,
sntrup, threshold_meta, x25519, x25519_frodokem640, x25519_mceliece348864, x25519_mlkem768,
x25519_sntrup761,
bls12381_g1_mldsa65, chacha20, ed25519, ed25519_fndsa512, ed25519_mayo2, ed25519_mldsa65,
fn_dsa, frodokem, mayo, ml_dsa, ml_kem, nist_p, rsa, secp256k1, slh_dsa, sntrup,
threshold_meta, x25519, x25519_frodokem640, x25519_mlkem768, x25519_sntrup761, xeddsa,
},
};

Expand Down Expand Up @@ -96,6 +97,16 @@ pub const SNTRUP_KEY_CODECS: [Codec; 5] = [
];

/// the list of Classic McEliece key codecs supported for key generation
///
/// This constant is deprecated. Key-recovery attacks now solve the TII
/// McEliece challenges; see <https://github.com/mjosaarinen/tii-solved>.
/// The McEliece views compile only with the `deprecated` feature.
#[cfg(feature = "deprecated")]
#[deprecated(
since = "2.0.0",
note = "Classic McEliece key recovery attacks: see https://github.com/mjosaarinen/tii-solved"
)]
#[allow(deprecated)]
pub const MCELIECE_KEY_CODECS: [Codec; 1] = [Codec::Mceliece348864Priv];

/// the list of FrodoKEM key codecs supported for key generation
Expand Down Expand Up @@ -466,6 +477,8 @@ impl Views for Multikey {
| Codec::Sntrup1013Priv
| Codec::Sntrup1277Pub
| Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)),
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => {
Ok(Box::new(classic_mceliece::View::try_from(self)?))
}
Expand All @@ -491,6 +504,8 @@ impl Views for Multikey {
| Codec::X25519Frodokem640ShakePriv => {
Ok(Box::new(x25519_frodokem640::View::try_from(self)?))
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => {
Ok(Box::new(x25519_mceliece348864::View::try_from(self)?))
}
Expand Down Expand Up @@ -693,6 +708,8 @@ impl Views for Multikey {
| Codec::Sntrup1013Priv
| Codec::Sntrup1277Pub
| Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)),
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => {
Ok(Box::new(classic_mceliece::View::try_from(self)?))
}
Expand All @@ -718,6 +735,8 @@ impl Views for Multikey {
| Codec::X25519Frodokem640ShakePriv => {
Ok(Box::new(x25519_frodokem640::View::try_from(self)?))
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => {
Ok(Box::new(x25519_mceliece348864::View::try_from(self)?))
}
Expand Down Expand Up @@ -965,6 +984,8 @@ impl Views for Multikey {
| Codec::Sntrup1013Priv
| Codec::Sntrup1277Pub
| Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)),
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => {
Ok(Box::new(classic_mceliece::View::try_from(self)?))
}
Expand All @@ -990,6 +1011,8 @@ impl Views for Multikey {
| Codec::X25519Frodokem640ShakePriv => {
Ok(Box::new(x25519_frodokem640::View::try_from(self)?))
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => {
Ok(Box::new(x25519_mceliece348864::View::try_from(self)?))
}
Expand Down Expand Up @@ -1179,6 +1202,8 @@ impl Views for Multikey {
| Codec::Sntrup1013Priv
| Codec::Sntrup1277Pub
| Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)),
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => {
Ok(Box::new(classic_mceliece::View::try_from(self)?))
}
Expand All @@ -1204,6 +1229,8 @@ impl Views for Multikey {
| Codec::X25519Frodokem640ShakePriv => {
Ok(Box::new(x25519_frodokem640::View::try_from(self)?))
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => {
Ok(Box::new(x25519_mceliece348864::View::try_from(self)?))
}
Expand Down Expand Up @@ -1353,6 +1380,8 @@ impl Views for Multikey {
| Codec::Sntrup1013Priv
| Codec::Sntrup1277Pub
| Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)),
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => {
Ok(Box::new(classic_mceliece::View::try_from(self)?))
}
Expand All @@ -1378,6 +1407,8 @@ impl Views for Multikey {
| Codec::X25519Frodokem640ShakePriv => {
Ok(Box::new(x25519_frodokem640::View::try_from(self)?))
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => {
Ok(Box::new(x25519_mceliece348864::View::try_from(self)?))
}
Expand Down Expand Up @@ -1424,6 +1455,8 @@ impl Views for Multikey {
| Codec::Sntrup1013Priv
| Codec::Sntrup1277Pub
| Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)),
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => {
Ok(Box::new(classic_mceliece::View::try_from(self)?))
}
Expand All @@ -1449,6 +1482,8 @@ impl Views for Multikey {
| Codec::X25519Frodokem640ShakePriv => {
Ok(Box::new(x25519_frodokem640::View::try_from(self)?))
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => {
Ok(Box::new(x25519_mceliece348864::View::try_from(self)?))
}
Expand Down Expand Up @@ -1638,6 +1673,7 @@ impl Views for Multikey {
| Codec::XmssSha216256Priv
| Codec::XmssSha220256Pub
| Codec::XmssSha220256Priv => Ok(Box::new(xmss::View::try_from(self)?)),
Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(xeddsa::View::try_from(self)?)),
_ => Err(ConversionsError::UnsupportedCodec(self.codec).into()),
}
}
Expand Down Expand Up @@ -1886,6 +1922,7 @@ impl Views for Multikey {
| Codec::XmssSha216256Priv
| Codec::XmssSha220256Pub
| Codec::XmssSha220256Priv => Ok(Box::new(xmss::View::try_from(self)?)),
Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(xeddsa::View::try_from(self)?)),
_ => Err(ConversionsError::UnsupportedCodec(self.codec).into()),
}
}
Expand Down Expand Up @@ -2034,6 +2071,8 @@ impl Builder {
rng.fill_bytes(&mut seed);
seed.to_vec()
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::Mceliece348864Priv => {
let mut seed = [0u8; 32];
rng.fill_bytes(&mut seed);
Expand All @@ -2056,6 +2095,8 @@ impl Builder {
rng.fill_bytes(&mut seed);
seed.to_vec()
}
#[cfg(feature = "deprecated")]
#[allow(deprecated)]
Codec::X25519Mceliece348864Priv => {
// x25519_seed (32) || mceliece_seed (32) = 64 bytes
let mut seed = [0u8; 64];
Expand Down
Loading
Loading