multi-key 2.2.0

Multikey self-describing cryptographic key data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
[![](https://img.shields.io/badge/made%20by-Cryptid%20Technologies-gold.svg?style=flat-square)][CRYPTID]
[![](https://img.shields.io/badge/project-provenance-purple.svg?style=flat-square)][PROVENANCE]
[![](https://img.shields.io/badge/project-multiformats-blue.svg?style=flat-square)][MULTIFORMATS]
![](https://github.com/cryptidtech/multi-key/actions/workflows/rust.yml/badge.svg)

# 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 = "2"` in `Cargo.toml`. Import it as `multi_key` in Rust, for example `use multi_key::Builder;`.

## Current Status

This implementation of the multikey specification supports an extensive set of public key and secret key cryptography keys. These span classical, post-quantum, and hybrid schemes:

- **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
  (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.

See the [Supported Key Formats](#supported-key-formats) section below for the exhaustive list of codecs.

This implementation supports encrypting and decrypting keys at rest with ChaCha20-Poly1305 AEAD. The keys are derived via the bcrypt PBKDF from a preimage. A legacy bare-ChaCha20 fallback is honored on decrypt so older keystores continue to work. Re-encrypting upgrades them to the authenticated AEAD format.

KEM-based message encryption uses `SealView` and `OpenView` with a choice of four AEAD codecs (ChaCha20-Poly1305, XChaCha20-Poly1305, AES-GCM-128, AES-GCM-256). HKDF-SHA512 derives the AEAD key from the KEM shared secret.

For threshold cryptography, this implementation supports three mechanisms:

1. **BLS12-381 Shamir splitting** of G1/G2 keys, including threshold signing and verifying.
2. **Distributed Key Generation (DKG)** threshold shares for Ed25519, P-256, P-384, secp256k1, BLS12-381, and Ed448, with an authenticated threshold marker bundle (TSIG-1).
3. A generic **`keysplit`** module that provides verifiable Feldman VSS for ECC keys, gf256 byte-sharing for RSA and all PQ and hybrid keys, and a dual mode (gf256 + Feldman) for Ed25519 and X25519.

This crate also supports converting to and from SSH format keys with the [`ssh-key`][SSHKEY] crate. This gives full OpenSSH compatibility for reading OpenSSH serialized keys and converting them to Multi-Key format. This includes non-standard SSH key protocols such as secp256k1 and BLS12-381 G1/G2 keys. These use the [RFC 4251][RFC4251] standard for "additional algorithms" names with the `@multikey` domain suffix. See the [SSH Key Conversions](#ssh-key-conversions) section for the full table.

For the technical details of the design of the multikey or nonce format, refer to the specifications linked above.

## Introduction

This is a Rust implementation of a multicodec format for cryptographic keys. The design of the format is intentionally abstract. It supports any kind of cryptographic key in any state (for example, encrypted or unencrypted). The format is a container of key material with abstract, algorithm-specific views and a generic, self-describing data storage format.

Every piece of data in the serialized Multi-Key object either has a known fixed size or a self-describing variable size. Software that processes these objects does not need to support all encryption algorithms to calculate the size of the serialized object and skip over it.

## Supported Key Formats

The tables below enumerate every key codec supported by this crate. Each algorithm has
`Pub` (public key) and `Priv` (private key) variants unless otherwise noted. The codec
identifiers come from the [multicodec][MULTICODEC] registry and are surfaced as
`multi_codec::Codec` variants.

### Classical Signing

| Algorithm | Codecs | Notes |
|---|---|---|
| Ed25519 | `Ed25519Pub` / `Ed25519Priv` | Ed25519 signatures |
| secp256k1 | `Secp256K1Pub` / `Secp256K1Priv` | ECDSA over secp256k1 |
| NIST P-256 | `P256Pub` / `P256Priv` | ECDSA + ECDH |
| NIST P-384 | `P384Pub` / `P384Priv` | ECDSA + ECDH |
| NIST P-521 | `P521Pub` / `P521Priv` | ECDSA + ECDH |
| RSA-2048 | `Rsa2048Pub` / `Rsa2048Priv` | RSA-SHA256 signatures |
| RSA-3072 | `Rsa3072Pub` / `Rsa3072Priv` | RSA-SHA256 signatures |
| RSA-4096 | `Rsa4096Pub` / `Rsa4096Priv` | RSA-SHA256 signatures |
| BLS12-381 G1 | `Bls12381G1Pub` / `Bls12381G1Priv` | BLS signatures on G1; also a TimeCrypt KEM |
| BLS12-381 G2 | `Bls12381G2Pub` / `Bls12381G2Priv` | BLS signatures on G2; also a TimeCrypt KEM |

### Post-Quantum Signing

| Algorithm | Codecs | Parameter sets |
|---|---|---|
| FN-DSA | `FnDsa512Pub`/`Priv`, `FnDsa1024Pub`/`Priv` | 512, 1024 |
| ML-DSA | `Mldsa65Pub`/`Priv`, `Mldsa87Pub`/`Priv` | 65, 87 |
| 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 |
| 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 |

### Hybrid Signing (Classical + Post-Quantum)

| Hybrid | Codecs | Components |
|---|---|---|
| Ed25519-MAYO2 | `Ed25519Mayo2Pub` / `Ed25519Mayo2Priv` | Ed25519 + MAYO-2 |
| Ed25519-ML-DSA-65 | `Ed25519Mldsa65Pub` / `Ed25519Mldsa65Priv` | Ed25519 + ML-DSA-65 |
| Ed25519-FN-DSA-512 | `Ed25519Fndsa512Pub` / `Ed25519Fndsa512Priv` | Ed25519 + FN-DSA-512 |
| BLS12-381-G1-ML-DSA-65 | `Bls12381G1Mldsa65Pub` / `Bls12381G1Mldsa65Priv` | BLS G1 + ML-DSA-65 |
| BLS12-381-G1-FN-DSA-512 | `Bls12381G1Fndsa512Pub` / `Bls12381G1Fndsa512Priv` | BLS G1 + FN-DSA-512 |
| BLS12-381-G1-MAYO-1 | `Bls12381G1Mayo1Pub` / `Bls12381G1Mayo1Priv` | BLS G1 + MAYO-1 |
| BLS12-381-G1-MAYO-2 | `Bls12381G1Mayo2Pub` / `Bls12381G1Mayo2Priv` | BLS G1 + MAYO-2 |

### Hybrid KEMs (Classical + Post-Quantum)

| Hybrid | Codecs | Components |
|---|---|---|
| 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) |
| Deprecated X25519-McEliece-348864 | `X25519Mceliece348864Pub` / `X25519Mceliece348864Priv` | X25519 + Classic McEliece 348864; behind the `deprecated` feature |

### Threshold Key Shares

| Mechanism | Codecs | Notes |
|---|---|---|
| BLS12-381 Shamir shares | `Bls12381G1PubShare`/`PrivShare`, `Bls12381G2PubShare`/`PrivShare` | Split/combine via `ThresholdView`; threshold sign/verify |
| DKG threshold shares | `Ed25519ThreshPubShare`/`PrivShare`, `P256ThreshPubShare`/`PrivShare`, `P384ThreshPubShare`/`PrivShare`, `Secp256K1ThreshPubShare`/`PrivShare`, `Bls12381ThreshPubShare`/`PrivShare`, `Ed448ThreshPubShare`/`PrivShare` | DKG metadata via `ThresholdKeyView`; authenticated marker (TSIG-1) |
| Generic `keysplit` shares | `KeySplitShare` | Feldman VSS (ECC), gf256 byte-sharing (RSA + PQ + hybrids), dual mode (Ed25519/X25519) |

### Symmetric

| Algorithm | Codec | Notes |
|---|---|---|
| ChaCha20-Poly1305 | `Chacha20Poly1305` | Used both for at-rest Multi-Key encryption and as a symmetric key codec |

## Views on the Multi-Key Data

To provide an abstract interface to cryptographic keys for all algorithms, this crate gives "views" on the Multi-Key data. These are read-only abstract interfaces to the Multi-Key attributes with implementations for different supporting algorithms.

The set of views provides generic access to the general attributes (`multi_key::AttrView`) of the Multi-Key, the key data (`multi_key::DataView`), and views on the KDF attributes (`multi_key::KdfAttrView`) and cipher attributes (`multi_key::CipherAttrView`) for encrypted Multi-Keys. For algorithms that support threshold operations, there is a threshold attributes view (`multi_key::ThresholdAttrView`) and a higher-level DKG metadata view (`multi_key::ThresholdKeyView`).

For operations you can do with a Multi-Key, there is:

- a cipher view (`multi_key::CipherView`) for encrypting and decrypting a Multi-Key at rest,
- a conversion view (`multi_key::ConvView`) for converting the Multi-Key to other formats (for example, to and from SSH key format, and secret keys to public keys),
- a fingerprint view (`multi_key::FingerprintView`) for getting a key fingerprint with a given hashing codec,
- a KDF view (`multi_key::KdfView`) for generating cipher keys for use by a cipher view to encrypt or decrypt the Multi-Key,
- a seal view (`multi_key::SealView`) and open view (`multi_key::OpenView`) for KEM-based message encryption and decryption,
- a threshold view (`multi_key::ThresholdView`) for key splitting and combining keys,
- a sign view (`multi_key::SignView`) and verify view (`multi_key::VerifyView`) for creating and verifying [`Multisig`][MULTISIG] digital signatures.

Two additional modules provide threshold functionality outside the view traits:

- `multi_key::keysplit` — generic verifiable threshold key splitting (Feldman VSS, gf256, dual) exposed as free `split`, `combine`, and `verify_share` functions.
- `multi_key::threshold_marker` — DKG marker stamping and reading and TSIG-1 marker authentication, including the `MarkerView` trait and `threshold_kind` and `threshold_params` helpers.

Operations that seem to mutate the Multi-Key (for example, encrypt, decrypt, convert) in fact do a copy-on-write (CoW) operation. They return a new Multi-Key with the mutation applied.

### Creating Views

Views are created through the builder-pattern `multi_key::ViewBuilder`
(re-exported at the crate root and in the prelude). Select a view kind
fluently, then build:

```rust
use multi_key::{Builder, ViewBuilder};
use multi_codec::Codec;

let mut rng = rand::rng();
let mk = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng)
    .unwrap()
    .try_build()
    .unwrap();

// sign a message and verify it
let signer = ViewBuilder::new(&mk).sign().build().unwrap();
let sig = signer.sign(b"message", false, None).unwrap();
let verifier = ViewBuilder::new(&mk).verify().build().unwrap();
verifier.verify(&sig, Some(b"message")).unwrap();
```

The 15 kinds that need no second key all follow this shape: `attr`,
`cipher_attr`, `data`, `kdf_attr`, `threshold_attr`, `threshold_key`,
`conv`, `fingerprint`, `sign`, `verify`, `seal`, `open`, `threshold`,
`disclosure`, and `merkle_state`. The `cipher` and `kdf` kinds attach the
second key their dispatch reads. A view borrows its Multikey, so keep the
Multikey alive while the view is in use:

```rust
// encrypt/decrypt the Multi-Key at rest
let encrypter = ViewBuilder::new(&mk).cipher(&cipher_key).build()?;
let encrypted = encrypter.encrypt()?;

// derive a cipher key from a passphrase
let kdf = ViewBuilder::new(&mk).kdf(&kdf_key).build()?;
let derived = kdf.derive_key(b"passphrase")?;
```

The original `multi_key::Views` extension trait is deprecated since 2.1.0
in favor of `ViewBuilder`. It stays as a source-compatible delegating shim
with the same `*_view()` methods, marked as deprecated at compile time.
Note that `multi_sig::Views` is a different, unrelated trait.

### Custom Protocol Keys

A custom protocol key (sigil-0) carries the `Codec::Identity` codec with the `AlgorithmName` attribute (code 27, the protocol name) and the `KeyType` attribute (code 28, one raw byte) from [provenance-specifications][PROVENANCE]. The `KeyType` byte convention is absent or 0 for public keys and 1 for secret keys. Custom keys have no built-in views, so `ViewBuilder` dispatches them to caller-supplied local-codec factories registered with `with_local_codec`. Different custom protocols share the `Codec::Identity` codec, so a factory identifies the key's protocol through the `AlgorithmName` attribute (not through the codec):

```rust
use multi_key::{AttrId, Builder, Error, SignView, ViewBuilder};
use multi_codec::Codec;
use multi_sig::Multisig;

let mk = Builder::new(Codec::Identity)
    .with_key_bytes(b"my-custom-key-material".as_slice())
    .with_algorithm_name("my-protocol")
    .with_key_type(1) // 1 = secret key
    .try_build()
    .unwrap();

struct MyProtocolSign {
    algorithm: String,
}

impl SignView for MyProtocolSign {
    fn sign(&self, _: &[u8], _: bool, _: Option<u8>) -> Result<Multisig, Error> {
        Err(Error::UnsupportedAlgorithm(format!("{} sign", self.algorithm)))
    }
}

// the factory builds a MyProtocolSign for the key it is handed. a real
// factory for several protocols sharing Identity reads the AlgorithmName
// attribute from the passed-in key and branches on it, serving only its
// own protocol.
let signer = ViewBuilder::new(&mk)
    .sign()
    .with_local_codec(Codec::Identity, |key| {
        let name = key
            .attributes
            .get(&AttrId::AlgorithmName)
            .ok_or_else(|| Error::UnsupportedAlgorithm("AlgorithmName missing".into()))?;
        Ok(Box::new(MyProtocolSign {
            algorithm: String::from_utf8_lossy(name).into_owned(),
        }))
    })
    .build()
    .unwrap();
```

Built-in views always win: a factory registered for a standard codec whose built-in view exists is never consulted. A repeat `with_local_codec` call for the same kind and codec replaces the earlier factory. Factory errors propagate unchanged. The `disclosure` kind is codec-independent (its view applies to every key), so a factory registered for that kind is never consulted.

## SSH Key Conversions

This crate converts to and from the SSH key format with the [`ssh-key`][SSHKEY] crate. Standard SSH algorithms are handled natively. Non-standard algorithms use the [RFC 4251][RFC4251] "additional algorithms" mechanism with an `ssh_key::Algorithm::Other` opaque key and an algorithm name ending in the literal `@multikey` suffix. This is a wire-format identifier, distinct from the crate name.

### Native SSH algorithms (no `@multikey` suffix)

| Algorithm | SSH algorithm name |
|---|---|
| Ed25519 | `ssh-ed25519` |
| ECDSA P-256 | `ecdsa-sha2-nistp256` |
| ECDSA P-384 | `ecdsa-sha2-nistp384` |
| ECDSA P-521 | `ecdsa-sha2-nistp521` |

### Custom `@multikey` algorithms (opaque SSH keys)

| Algorithm | SSH algorithm name |
|---|---|
| secp256k1 | `secp256k1@multikey` |
| BLS12-381 G1 | `bls12_381-g1@multikey` |
| BLS12-381 G1 share | `bls12_381-g1-share@multikey` |
| BLS12-381 G2 | `bls12_381-g2@multikey` |
| BLS12-381 G2 share | `bls12_381-g2-share@multikey` |
| RSA-2048/3072/4096 | `rsa-sha256@multikey` |
| ML-DSA-65 | `ml-dsa-65@multikey` |
| ML-DSA-87 | `ml-dsa-87@multikey` |
| FN-DSA-512 | `fn-dsa-512@multikey` |
| FN-DSA-1024 | `fn-dsa-1024@multikey` |
| MAYO-1 | `mayo-1@multikey` |
| MAYO-2 | `mayo-2@multikey` |
| MAYO-3 | `mayo-3@multikey` |
| MAYO-5 | `mayo-5@multikey` |
| SLH-DSA SHA-2 128f | `slh-dsa-sha2-128f@multikey` |
| SLH-DSA SHA-2 128s | `slh-dsa-sha2-128s@multikey` |
| SLH-DSA SHA-2 192f | `slh-dsa-sha2-192f@multikey` |
| SLH-DSA SHA-2 192s | `slh-dsa-sha2-192s@multikey` |
| SLH-DSA SHA-2 256f | `slh-dsa-sha2-256f@multikey` |
| SLH-DSA SHA-2 256s | `slh-dsa-sha2-256s@multikey` |
| SLH-DSA SHAKE 128f | `slh-dsa-shake-128f@multikey` |
| SLH-DSA SHAKE 128s | `slh-dsa-shake-128s@multikey` |
| SLH-DSA SHAKE 192f | `slh-dsa-shake-192f@multikey` |
| SLH-DSA SHAKE 192s | `slh-dsa-shake-192s@multikey` |
| SLH-DSA SHAKE 256f | `slh-dsa-shake-256f@multikey` |
| SLH-DSA SHAKE 256s | `slh-dsa-shake-256s@multikey` |

The import direction (`Builder::new_from_ssh_public_key` and `Builder::new_from_ssh_private_key`) supports all of the algorithms above.

### 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 (deprecated), all FrodoKEM variants, the BLS12-381 TimeCrypt KEM, and all hybrid signing and hybrid KEM schemes.

## Threshold Operations

### BLS12-381 Shamir Splitting

`ThresholdView::split(threshold, limit)` splits a `Bls12381G1Priv` or `Bls12381G2Priv` into `Bls12381G1PrivShare` or `Bls12381G2PrivShare` shares with `blsful`'s `SecretKey::split`. Shares are recombined with `combine`. Threshold signing and verifying is supported on the share codecs. Requires `2 <= threshold <= limit <= 255`.

### DKG Threshold Shares

The DKG share codecs (`Ed25519Thresh*`, `P256Thresh*`, `P384Thresh*`, `Secp256K1Thresh*`, `Bls12381Thresh*`, `Ed448Thresh*`) carry DKG metadata attributes (`DkgThreshold`, `DkgLimit`, `DkgIdentifier`, `DkgGroupPublicKey`, `DkgOwnerId`). The `ThresholdKeyView` trait exposes `group_pubkey()`, `is_threshold_key()`, `participant_count()`, `threshold()`, and `owner_vlad()`. The `threshold_marker` module stamps and authenticates a marker bundle (TSIG-1) with a controller signing key via `sign_marker` and `verify_marker`.

### Generic `keysplit` Module

`multi_key::keysplit` provides scheme-aware verifiable threshold splitting for any key type as free functions (`split`, `combine`, `verify_share`) that produce `KeySplitShare` Multi-Keys:

- **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 (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).

## Threshold Confidentiality

By default, threshold `t` and share count `n` are stored as **plaintext** attributes on every key share. Any observer of a share learns the threshold parameters. This crate supports three configurable disclosure modes that control the confidentiality of `t` and `n`. These apply to BLS12-381 Shamir shares and the generic `keysplit` module.

### Disclosure Modes

| Mode | `t` (threshold) | `n` (limit) | Who sees `t` | Who sees `n` |
|---|---|---|---|---|
| `Full` (default, 0) | plaintext attribute | plaintext attribute | everyone | everyone |
| `Partial` (1) | encrypted (AEAD) | plaintext attribute | key-holder only | everyone (auditable) |
| `FullConfidentialial` (2) | encrypted (AEAD) | encrypted (AEAD) | key-holder only | key-holder only |

The encrypted values are sealed with **ChaCha20-Poly1305 AEAD**. They are stored as a CBOR-encoded `ThresholdMetadata` blob in `AttrId::EncryptedThresholdMeta`. The cipher parameters (codec and nonce) are recorded in `AttrId::ThresholdMetaCipher` so the blob is self-describing for decryption. A separate **meta key** (a 32-byte symmetric `Multikey` with `Codec::Chacha20Poly1305`) is required to encrypt and decrypt the metadata.

### When to Use Each Mode

- **`Full`** — Use when `t` and `n` are not sensitive. This is the default. It is backward-compatible with all existing shares. It is appropriate for open governance systems where the threshold structure is public knowledge.

- **`Partial`** — Use when the total number of participants `n` should be auditable (for example, for governance transparency) but the threshold `t` should be hidden from share holders and observers. Hiding `t` means an adversary who compromises some shares does not know how many more they need to reconstruct the key. The `meta_key` is required to read `t`. The value `n` is freely readable.

- **`FullConfidentialial`** — Use when both `t` and `n` must be kept secret. An observer who sees a share cannot determine the group size or how many shares are needed. This is the strongest confidentiality mode. The `meta_key` is required to read both `t` and `n`.

### Trade-offs

| Consideration | Full | Partial | FullConfidentialial |
|---|---|---|---|
| Backward compatible | yes | yes (attribute defaults to Full if absent) | yes |
| Observer learns `t` | yes | no | no |
| Observer learns `n` | yes | yes | no |
| Requires `meta_key` | no | for reading `t` | for reading `t` and `n` |
| Auditable `n` | yes | yes | no |
| Risk if `meta_key` lost | n/a | `t` irrecoverable | `t` and `n` irrecoverable |
| Performance overhead | none | negligible (AEAD on ~10 bytes) | negligible |

**Key management risk.** Losing the `meta_key` makes `t` (Partial) or both `t` and `n` (FullConfidentialial) irrecoverable. This prevents key combination. The `meta_key` should be stored or backed up with the existing at-rest encryption mechanisms. You can always convert back to `Full` mode (with the `meta_key`) before you lose it.

**DKG note.** DKG threshold values (`t` and `n`) are inherently known to all participants because they are agreed during the DKG ceremony. The confidentiality modes do not apply to DKG shares. A `to_disclosure()` call on a DKG share returns an error. Future work could add "hidden threshold DKG" where participants do not know `t`. That requires protocol-level changes (FROST-style), not just encoding changes.

### Creating Shares with a Disclosure Mode

There are three ways to produce shares in a given disclosure mode:

**1. Direct creation via `split_with_disclosure()`:**

```rust
use multi_key::{Builder, ViewBuilder, ThresholdDisclosure};

let meta_key = multi_key::generate_meta_key();
let meta_mk = Builder::new(Codec::Chacha20Poly1305)
    .with_key_bytes(&meta_key.as_slice())
    .try_build()?;

// BLS Shamir split with FullConfidentialial disclosure
let shares = ViewBuilder::new(&mk).threshold().build()?
    .split_with_disclosure(3, 5,
        ThresholdDisclosure::FullConfidentialial, Some(&meta_mk))?;
```

**2. Builder construction:**

```rust
let share = Builder::new(Codec::Bls12381G2PrivShare)
    .with_disclosure(ThresholdDisclosure::Partial, Some(&meta_mk), 3, 5)
    .with_identifier(&identifier)
    .with_key_bytes(&key_bytes)
    .try_build()?;
```

**3. Convert an existing share:**

```rust
let encrypted = ViewBuilder::new(&share).disclosure().build()?
    .to_disclosure(ThresholdDisclosure::FullConfidentialial, Some(&meta_mk), None)?;
```

### Reading Threshold Parameters from Encrypted Shares

Use `read_threshold_params()` with the `meta_key` to decrypt `t` and `n`:

```rust
let (t, n) = ViewBuilder::new(&encrypted).disclosure().build()?
    .read_threshold_params(Some(&meta_mk))?;
```

### Combining Encrypted Shares

```rust
let combined = ViewBuilder::new(&mk).threshold().build()?
    .combine_with_meta(Some(&meta_mk))?;
```

For the generic `keysplit` module, use `split_with_disclosure()` and `combine_with_meta()`:

```rust
use multi_key::keysplit;

let shares = keysplit::split_with_disclosure(
    &mk, 3, 5, ThresholdDisclosure::Partial, Some(&meta_mk), rand::rng())?;
let combined = keysplit::combine_with_meta(&shares, Some(&meta_mk))?;
```

### Converting Between Modes

The `to_disclosure()` method converts between any pair of modes. It reads the current `t` and `n` (decrypting if needed with `current_meta_key`), then re-stamps the attributes in the target mode (encrypting if needed with `meta_key`):

```rust
// Full → Partial
let partial = ViewBuilder::new(&full).disclosure().build()?
    .to_disclosure(ThresholdDisclosure::Partial, Some(&meta_mk), None)?;

// Partial → FullConfidentialial
let confidential = ViewBuilder::new(&partial).disclosure().build()?
    .to_disclosure(ThresholdDisclosure::FullConfidentialial, Some(&meta_mk), Some(&meta_mk))?;

// FullConfidentialial → Full
let full_again = ViewBuilder::new(&confidential).disclosure().build()?
    .to_disclosure(ThresholdDisclosure::Full, None, Some(&meta_mk))?;
```

## Encryption

### At-Rest Multi-Key Encryption

Multi-Keys can be encrypted at rest with **ChaCha20-Poly1305** AEAD (`CipherView`). The cipher key is derived from a preimage via the **bcrypt PBKDF** (`KdfView`, 32-byte salt, configurable rounds). A legacy bare-ChaCha20 fallback is honored on decrypt so keystores encrypted before AEAD was added continue to work. Re-encrypting upgrades them to the authenticated format.

### KEM Seal / Open

KEM-based message encryption uses `SealView` and `OpenView`. The KEM shared secret is expanded into an AEAD key via **HKDF-SHA512**. Then one of four AEAD codecs may be used:

| AEAD codec | Key size | Nonce size |
|---|---|---|
| `Chacha20Poly1305` | 32 bytes | 12 bytes |
| `Xchacha20Poly1305` | 32 bytes | 24 bytes |
| `AesGcm128` | 16 bytes | 12 bytes |
| `AesGcm256` | 32 bytes | 12 bytes |

Individual KEM views may restrict the allowed AEAD codec. For example, X25519-ML-KEM-768 only permits `Chacha20Poly1305` per its specification.

## Cargo Features

| Feature | Default | Description |
|---|---|---|
| `serde` | yes | Serde serialization for `Multikey`, `KeyShare`, `SharePayload`, `ThresholdParticipant` |
| `wasm` | no | WebAssembly support via `getrandom/wasm_js`; switches `blsful` to the `rust` backend and `ssh-key` to `ecdsa`/`ed25519`/`p256`/`p384`/`p521` features on `wasm32` |

## Security

- Private keys are wrapped in `Zeroizing` buffers and automatically zeroized on drop.
- `Debug` output for private key material is redacted.
- The `ViewBuilder` is `Send` + `Sync` (with and without registered factories). The view trait objects it returns carry no `Send`/`Sync` supertrait, so they do not carry that bound at the type level.
- Mutation operations use copy-on-write semantics. They return a new `Multi-Key` rather than mutating in place.

## Links

- [Cryptid Technologies][CRYPTID]
- [Provenance Specifications][PROVENANCE]
- [Multiformats][MULTIFORMATS]
- [Multikey Specification][MULTIKEY]
- [Nonce Specification][NONCE]
- [Multisig][MULTISIG]
- [`ssh-key` crate][SSHKEY]
- [RFC 4251][RFC4251]

[CRYPTID]: https://cryptid.tech
[PROVENANCE]: https://github.com/cryptidtech/provenance-specifications/
[MULTIFORMATS]: https://github.com/multiformats/multiformats
[MULTIKEY]: https://github.com/cryptidtech/provenance-specifications/blob/main/specifications/multikey.md
[NONCE]: https://github.com/cryptidtech/provenance-specifications/blob/main/specifications/nonce.md
[MULTICODEC]: https://github.com/multiformats/multicodec
[SSHKEY]: https://crates.io/crates/ssh-key
[RFC4251]: https://www.rfc-editor.org/rfc/rfc4251.html#page-11
[MULTISIG]: https://github.com/cryptidtech/provenance-specifications/blob/main/specifications/multisig.md