[][CRYPTID]
[][PROVENANCE]
[][MULTIFORMATS]

# Multisig
A Rust implementation of the [multiformats][MULTIFORMATS] [multisig specification][MULTISIG]. The published crate is **`multi-sig`**. Depend on it as `multi-sig = "1.0"` in `Cargo.toml`. Import it as `multi_sig` in Rust, for example `use multi_sig::Builder;`.
## Current Status
This crate gives self-describing digital signature containers (`Multisig`) for 35 signature codecs. The codecs span classical, post-quantum, and hybrid schemes. It supports BLS12-381 threshold signatures with share accumulation and combination. It supports SSH signature interoperability for all classical schemes plus BLS12-381 combined and share signatures.
**Supported signature families:**
- **Classical:** Ed25519, secp256k1 (ECDSA), NIST P-256/P-384/P-521 (ECDSA), RSA-SHA256, BLS12-381 G1/G2
- **Post-quantum:** ML-DSA (65/87), FN-DSA (512/1024), MAYO (1/2/3/5), SLH-DSA (all 12 parameter sets)
- **Hybrid:** Ed25519+MAYO-2, Ed25519+ML-DSA-65, Ed25519+FN-DSA-512, BLS12-381-G1+ML-DSA-65, BLS12-381-G1+FN-DSA-512, BLS12-381-G1+MAYO-1, BLS12-381-G1+MAYO-2
- **Threshold:** BLS12-381 G1/G2 combined and share signatures with threshold disclosure modes
**SSH interoperability:** Ed25519, secp256k1, NIST P-256/P-384/P-521, RSA-SHA256, and BLS12-381 G1/G2 (combined and share signatures) convert to and from OpenSSH format with the [`ssh-key`][SSHKEY] crate. Non-standard algorithms use [RFC 4251][RFC4251] "additional algorithms" names with the `@multisig` domain suffix, for example `secp256k1@multisig` and `bls12_381-g1-share@multisig`.
## Introduction
This is a Rust implementation of a multicodec container format for digital signatures. The design is intentionally abstract. It supports any kind of digital signature data for any protocol. The format is a container of signature data with abstract, protocol-specific views backed by a generic, self-describing data storage format.
Every piece of data in a serialized Multisig object either has a known fixed size or a self-describing variable size via `Varuint` and `Varbytes`. Software that processes these objects does not need to support all digital signature protocols to calculate the size of the serialized object and skip over it.
The only operations on a Multisig object are those that return attribute data and the threshold signature operations for accumulating and combining signature shares. Any operation that involves a cryptographic key (for example, signing or verifying) is in the companion [`Multi-Key`][MULTIKEY] crate.
## Wire Format
A Multisig is serialized as:
```
```
- **SIGIL** — the multicodec `0x1239` (`Multisig`) distinguishes this format from the older Varsig (`0x34`).
- **signature_codec** — a varuint-encoded multicodec tag that identifies the signature algorithm.
- **message** — `Varbytes` (length-prefixed). If non-empty, the signature is **combined** (it carries the signed message in-band). If empty, the signature is **detached** (the message must be supplied out-of-band for verification).
- **attributes** — a counted list of `(AttrId, Varbytes)` pairs. Attribute IDs are u8 enum values. The decoder rejects duplicate IDs at decode time. Attributes are emitted in `BTreeMap` order (sorted by ID) for deterministic encoding.
The preferred base encoding for Multisig strings is `Base16Lower` (lowercase hex).
## Supported Signature Formats
### Classical Signatures
| `EddsaMsig` | `eddsa-msig` | `ssh-ed25519` | no | Ed25519 signatures |
| `Es256KMsig` | `es256k-msig` | `secp256k1@multisig` | no | ECDSA over secp256k1 |
| `Es256Msig` | `es256-msig` | `ecdsa-sha2-nistp256@multisig` | no | ECDSA over NIST P-256 |
| `Es384Msig` | `es384-msig` | `ecdsa-sha2-nistp384@multisig` | no | ECDSA over NIST P-384 |
| `Es521Msig` | `es521-msig` | `ecdsa-sha2-nistp521@multisig` | no | ECDSA over NIST P-521 |
| `Rs256Msig` | `rs256-msig` | `rsa-sha256@multisig` | no | RSA-SHA256 signatures |
| `Bls12381G1Msig` | `bls12_381-g1-msig` | `bls12_381-g1@multisig` | **yes** | BLS signatures on G1 (48-byte sig) |
| `Bls12381G2Msig` | `bls12_381-g2-msig` | `bls12_381-g2@multisig` | **yes** | BLS signatures on G2 (96-byte sig) |
### Post-Quantum Signatures
| `Mldsa65Msig` | `mldsa-65-msig` | no | ML-DSA (Dilithium) security level 65; FIPS 204 |
| `Mldsa87Msig` | `mldsa-87-msig` | no | ML-DSA security level 87; FIPS 204 |
| `FnDsa512Msig` | `fn-dsa-512-msig` | no | FN-DSA (Falcon) 512; FIPS 206 (draft) |
| `FnDsa1024Msig` | `fn-dsa-1024-msig` | no | FN-DSA (Falcon) 1024; FIPS 206 (draft) |
| `Mayo1Msig` | `mayo-1-msig` | no | MAYO-1 |
| `Mayo2Msig` | `mayo-2-msig` | no | MAYO-2 |
| `Mayo3Msig` | `mayo-3-msig` | no | MAYO-3 |
| `Mayo5Msig` | `mayo-5-msig` | no | MAYO-5 |
| `SlhdsaSha2128FMsig` | `slhdsa-sha2-128f-msig` | no | SLH-DSA (SPHINCS+) SHA-2 128f; FIPS 205 |
| `SlhdsaSha2128SMsig` | `slhdsa-sha2-128s-msig` | no | SLH-DSA SHA-2 128s |
| `SlhdsaSha2192FMsig` | `slhdsa-sha2-192f-msig` | no | SLH-DSA SHA-2 192f |
| `SlhdsaSha2192SMsig` | `slhdsa-sha2-192s-msig` | no | SLH-DSA SHA-2 192s |
| `SlhdsaSha2256FMsig` | `slhdsa-sha2-256f-msig` | no | SLH-DSA SHA-2 256f |
| `SlhdsaSha2256SMsig` | `slhdsa-sha2-256s-msig` | no | SLH-DSA SHA-2 256s |
| `SlhdsaShake128FMsig` | `slhdsa-shake-128f-msig` | no | SLH-DSA SHAKE 128f |
| `SlhdsaShake128SMsig` | `slhdsa-shake-128s-msig` | no | SLH-DSA SHAKE 128s |
| `SlhdsaShake192FMsig` | `slhdsa-shake-192f-msig` | no | SLH-DSA SHAKE 192f |
| `SlhdsaShake192SMsig` | `slhdsa-shake-192s-msig` | no | SLH-DSA SHAKE 192s |
| `SlhdsaShake256FMsig` | `slhdsa-shake-256f-msig` | no | SLH-DSA SHAKE 256f |
| `SlhdsaShake256SMsig` | `slhdsa-shake-256s-msig` | no | SLH-DSA SHAKE 256s |
### Hybrid Signatures (Classical + Post-Quantum)
Hybrid signatures use a nested combiner construction. The classical component signs the message. The PQ component then signs `message || classical_signature`. Verification requires both components to pass.
| `Ed25519Mayo2Msig` | `ed25519-mayo2-msig` | Ed25519 + MAYO-2 | no |
| `Ed25519Mldsa65Msig` | `ed25519-mldsa65-msig` | Ed25519 + ML-DSA-65 | no |
| `Ed25519Fndsa512Msig` | `ed25519-fndsa512-msig` | Ed25519 + FN-DSA-512 | no |
| `Bls12381G1Mldsa65Msig` | `bls12381-g1-mldsa65-msig` | BLS12-381 G1 + ML-DSA-65 | no |
| `Bls12381G1Fndsa512Msig` | `bls12381-g1-fndsa512-msig` | BLS12-381 G1 + FN-DSA-512 | no |
| `Bls12381G1Mayo1Msig` | `bls12381-g1-mayo1-msig` | BLS12-381 G1 + MAYO-1 | no |
| `Bls12381G1Mayo2Msig` | `bls12381-g1-mayo2-msig` | BLS12-381 G1 + MAYO-2 | no |
### Threshold Signature Shares (BLS12-381)
| `Bls12381G1ShareMsig` | `bls12_381-g1-share-msig` | `bls12_381-g1-share@multisig` | A BLS G1 partial signature from a threshold share |
| `Bls12381G2ShareMsig` | `bls12_381-g2-share-msig` | `bls12_381-g2-share@multisig` | A BLS G2 partial signature from a threshold share |
## Attribute IDs
Each Multisig carries a set of attributes identified by a `u8` code:
| 0 | `sig-data` | all | The raw signature bytes |
| 1 | `payload-encoding` | all (optional) | The multicodec encoding of the signed payload |
| 2 | `scheme` | BLS | BLS scheme type: 0=Basic, 1=MessageAugmentation, 2=ProofOfPossession |
| 3 | `threshold` | BLS shares | The threshold `t` (plaintext, Full disclosure mode) |
| 4 | `limit` | BLS shares | The share count `n` (plaintext, Full/Partial disclosure modes) |
| 5 | `share-identifier` | BLS shares | 32-byte BLS scalar identifier for this share |
| 6 | `threshold-data` | BLS combined | Serialized `ThresholdData` — the accumulated share map |
| 7 | `threshold-disclosure` | BLS (optional) | Disclosure mode: 0=Full, 1=Partial, 2=FullConfidentialial |
| 8 | `encrypted-threshold-meta` | BLS (optional) | AEAD-encrypted CBOR blob containing t and/or n |
| 9 | `threshold-meta-cipher` | BLS (optional) | CBOR-encoded cipher info (codec + nonce) for decrypting #8 |
## Views on the Multisig Data
To provide an abstract interface to digital signatures of all schemes, this crate gives "views" on the Multisig data. These are read-only or copy-on-write abstract interfaces with implementations for different supporting signature protocols.
### View Traits
| `AttrView` | `payload_encoding()`, `scheme()` | Access the payload encoding codec and signing scheme |
| `DataView` | `sig_bytes()` | Access the raw signature bytes |
| `ConvView` | `to_ssh_signature()` | Convert to an OpenSSH `ssh_key::Signature` |
| `ThresholdAttrView` | `threshold()`, `limit()`, `identifier()`, `threshold_data()` | Read threshold parameters (BLS only) |
| `ThresholdView` | `shares()`, `shares_with_disclosure()`, `add_share()`, `add_share_with_meta()`, `combine()`, `combine_with_meta()` | Accumulate and combine threshold signature shares (BLS only) |
| `ThresholdDisclosureView` | `disclosure_mode()`, `read_threshold_params()`, `to_disclosure()` | Read or convert the threshold disclosure mode (all codecs) |
| `Views` | `attr_view()`, `data_view()`, `conv_view()`, `threshold_attr_view()`, `threshold_view()`, `disclosure_view()` | Dispatcher trait — obtain any view from a `Multisig` |
### View Dispatch by Codec Family
| BLS G1/G2 (combined + share) | `bls12381::View` | `bls12381::View` | `bls12381::View` | `bls12381::View` | `bls12381::View` (combined only) |
| Ed25519 | `ed25519::View` | `ed25519::View` | `ed25519::View` | — | — |
| secp256k1 | `secp256k1::View` | `secp256k1::View` | `secp256k1::View` | — | — |
| NIST P-256/384/521 | `nist_p::View` | `nist_p::View` | `nist_p::View` | — | — |
| RSA | `rsa::View` | `rsa::View` | `rsa::View` | — | — |
| ML-DSA 65/87 | `ml_dsa::View` | `ml_dsa::View` | `ml_dsa::View` | — | — |
| FN-DSA 512/1024 | `fn_dsa::View` | `fn_dsa::View` | `fn_dsa::View` | — | — |
| MAYO 1/2/3/5 | `mayo::View` | `mayo::View` | `mayo::View` | — | — |
| SLH-DSA (all 12) | `slh_dsa::View` | `slh_dsa::View` | `slh_dsa::View` | — | — |
| Ed25519-MAYO2 | `ed25519_mayo2::View` | `ed25519_mayo2::View` | `ed25519_mayo2::View` | — | — |
| Other hybrids | `ed25519_hybrid::View` | `ed25519_hybrid::View` | `ed25519_hybrid::View` | — | — |
The `disclosure_view()` method is codec-agnostic. It is available on all codecs.
### Copy-on-Write Semantics
Operations that appear to mutate the Multisig (`add_share`, `combine`, `to_disclosure`) in fact perform a copy-on-write (CoW) operation. They return a **new** `Multisig`. The original is unchanged. This is most visible in `Builder::try_build()`:
```rust
let mut ms = Builder::new(Codec::Bls12381G2Msig).try_build()?;
for share in &shares {
ms = {
let tv = ms.threshold_view()?;
// CoW — returns a new Multisig with the share added
tv.add_share(share)?
};
}
```
## Builder API
The `Builder` constructs `Multisig` objects:
| `Builder::new(codec)` | Create a builder for the given signature codec |
| `Builder::new_from_ssh_signature(&sig)` | Construct from an OpenSSH `ssh_key::Signature` |
| `Builder::new_from_bls_signature(&sig)` | Construct from a `blsful::Signature` (infers G1/G2 by byte length) |
| `Builder::new_from_bls_signature_share(t, n, &share)` | Construct from a `blsful::SignatureShare` |
| `.with_message_bytes(&msg)` | Set the message payload (makes a combined signature) |
| `.with_signature_bytes(&data)` | Set the raw signature bytes (`AttrId::SigData`) |
| `.with_payload_encoding(codec)` | Set the payload encoding codec |
| `.with_scheme(scheme_u8)` | Set the BLS scheme type (0/1/2) |
| `.with_threshold(t)` | Set the threshold value (plaintext) |
| `.with_limit(n)` | Set the limit value (plaintext) |
| `.with_identifier(&id)` | Set the share identifier (32-byte BLS scalar) |
| `.with_threshold_data(&data)` | Set the accumulated threshold data blob |
| `.with_disclosure(mode, meta_key, t, n)` | Set t/n with a specific disclosure mode (see [Threshold Confidentiality](#threshold-confidentiality)) |
| `.add_signature_share(&share)` | Accumulate a share for `try_build()` to fold in |
| `.try_build()` | Build the `Multisig` (folds in accumulated shares) |
| `.try_build_encoded()` | Build and wrap in `EncodedMultisig` (base-encoded string) |
## Generating and Verifying Signatures
Signature generation and verification are in the companion [`Multi-Key`][MULTIKEY] crate. They use the `SignView` and `VerifyView` traits on a `Multikey`. The `Multikey::sign_view()` method produces a `Multisig`. The `Multikey::verify_view()` method verifies a `Multisig` against an optional message.
### Generating a Signature
```rust
use multi_key::{Builder, Views};
use multi_codec::Codec;
// Generate an Ed25519 key and sign a message
let mk = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rand::rng())?
.try_build()?;
// Combined signature (carries the message in-band)
let multisig = mk.sign_view()?.sign(b"hello world", true, None)?;
// Detached signature (message supplied out-of-band for verification)
let detached = mk.sign_view()?.sign(b"hello world", false, None)?;
```
### Verifying a Signature
```rust
use multi_key::Views;
// Verify a combined signature (message is carried in the Multisig)
mk.verify_view()?.verify(&multisig, None)?;
// Verify a detached signature (message supplied separately)
mk.verify_view()?.verify(&detached, Some(b"hello world"))?;
```
### Combined vs Detached Signatures
A Multisig is **combined** if the `message` field is non-empty. The signed message is carried in-band. No external message is needed for verification. A Multisig is **detached** if the `message` field is empty. The verifier must supply the original message out-of-band.
The `combined` parameter on `SignView::sign(msg, combined, scheme)` controls this:
- `combined = true` → the message is stored in the `Multisig` (combined signature)
- `combined = false` → the message is not stored (detached signature)
For verification, `VerifyView::verify(sig, msg)`:
- `msg = None` → uses the message stored in the Multisig (combined)
- `msg = Some(bytes)` → uses the externally supplied message (detached)
## Threshold Signatures (BLS12-381)
BLS12-381 is the only signature family that supports threshold signatures in this crate. A threshold BLS signature is produced by multiple parties. Each party signs with their key share. The partial signatures are then combined into a single combined signature. The combined signature verifies against the group public key.
### BLS Signature Schemes
BLS12-381 supports three signature schemes, stored as `AttrId::Scheme`:
| `Basic` | 0 | Raw BLS. It is vulnerable to rogue-key attacks without PoP checking. |
| `MessageAugmentation` | 1 | Prepends a domain tag to the message before signing. |
| `ProofOfPossession` | 2 | Requires a separate PoP signature over the public key. This is the **default** and the strongest rogue-key defence. |
### How Threshold Signatures Work
1. A BLS secret key is split into `n` shares with threshold `t` using the `Multi-Key` crate's `ThresholdView::split(t, n)` or `split_with_disclosure(t, n, mode, meta_key)`.
2. Each shareholder signs the message with their key share. This produces a partial signature (`Bls12381G1ShareMsig` or `Bls12381G2ShareMsig`).
3. The partial signatures are accumulated into a combined `Multisig` with `ThresholdView::add_share()` (CoW) or `add_share_with_meta()`.
4. Once at least `t` shares are accumulated, `ThresholdView::combine()` (or `combine_with_meta()`) reconstructs the combined BLS signature via Lagrange interpolation in the group.
### Accumulating and Combining Shares
```rust
use multi_key::{Builder, Views};
use multi_codec::Codec;
// Split a BLS G2 key into 3-of-5 shares
let mk = Builder::new_from_random_bytes(Codec::Bls12381G2Priv, &mut rand::rng())?
.try_build()?;
let shares = mk.threshold_view()?.split(3, 5)?;
// Each share signs the message (done by the shareholder)
let partial_sigs: Vec<_> = shares.iter()
.map(|s| s.sign_view()?.sign(b"message", true, Some(2))?) // scheme 2 = PoP
.collect();
// Accumulate shares into a combined Multisig
let mut ms = partial_sigs[0].clone();
for ps in &partial_sigs[1..] {
ms = ms.threshold_view()?.add_share(ps)?;
}
// Combine into the final signature
let combined = ms.threshold_view()?.combine()?;
```
### SSH Round-Trip for BLS Share Signatures
BLS share signatures can convert to and from SSH format. The SSH algorithm names are `bls12_381-g1-share@multisig` and `bls12_381-g2-share@multisig`. The share identifier, threshold, and limit are carried inside the SSH signature blob.
## Threshold Confidentiality
By default, threshold `t` and share count `n` are stored as **plaintext** attributes on every 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`:
### Disclosure Modes
| `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 `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
| 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 share 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.
### 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, Views, 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()?;
let shares = mk.threshold_view()?.split_with_disclosure(3, 5,
ThresholdDisclosure::FullConfidentialial, Some(&meta_mk))?;
```
**2. Builder construction:**
```rust
let share = Builder::new(Codec::Bls12381G2ShareMsig)
.with_disclosure(ThresholdDisclosure::Partial, Some(&meta_mk), 3, 5)
.with_identifier(&identifier)
.with_signature_bytes(&sig_bytes)
.try_build()?;
```
**3. Convert an existing share:**
```rust
let encrypted = share.disclosure_view()?
.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) = encrypted.disclosure_view()?
.read_threshold_params(Some(&meta_mk))?;
```
### Combining Encrypted Shares
```rust
let combined = ms.threshold_view()?
.combine_with_meta(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 = full.disclosure_view()?
.to_disclosure(ThresholdDisclosure::Partial, Some(&meta_mk), None)?;
// Partial -> FullConfidentialial
let confidential = partial.disclosure_view()?
.to_disclosure(ThresholdDisclosure::FullConfidentialial, Some(&meta_mk), Some(&meta_mk))?;
// FullConfidentialial -> Full
let full_again = confidential.disclosure_view()?
.to_disclosure(ThresholdDisclosure::Full, None, Some(&meta_mk))?;
```
## Serde Serialization
With the `serde` feature (default), `Multisig` supports dual-form serialization:
- **Human-readable** (JSON, etc.): a struct `{ "codec": "...", "message": "...", "attributes": [...] }` where `codec` is the multicodec name, `message` is a base-encoded `Varbytes`, and `attributes` is a list of `(name, base-encoded-value)` tuples.
- **Compact** (binary formats): the raw wire-format bytes via `serialize_bytes`.
`EncodedMultisig` serializes as a single base-encoded string in readable form and as raw bytes in compact form. `AttrId` round-trips as either a name string or a `u8`.
## Type-Safe Wrappers
The `types` module gives newtypes for type safety:
- `SignatureBytes(Vec<u8>)` — wraps raw signature bytes with `Display` (hex), `AsRef<[u8]>`, and safe conversions.
- `SignatureScheme(Codec)` — wraps a `Codec` as a signature scheme identifier. It is `Copy`. It gives `name()` and `code()` accessors.
## What about Varsig?
A multicodec signature format called Varsig (`0x34`) already exists. It has serious design deficiencies. It relies on out-of-band context for signature-specific values. This makes it impossible to decode without supporting every key codec. Multisig uses a new multicodec sigil `0x1239` to distinguish the two formats. To convert from Varsig to Multisig, pull the relevant data out of the Varsig and use the `Builder` to construct a Multisig.
## Cargo Features
| `serde` | yes | Serde serialization for `Multisig` and `AttrId` |
## Links
- [Cryptid Technologies][CRYPTID]
- [Provenance Specifications][PROVENANCE]
- [Multiformats][MULTIFORMATS]
- [Multisig Specification][MULTISIG]
- [Multi-Key crate][MULTIKEY]
- [`ssh-key` crate][SSHKEY]
- [RFC 4251][RFC4251]
[CRYPTID]: https://cryptid.tech
[PROVENANCE]: https://github.com/cryptidtech/provenance-specifications/
[MULTIFORMATS]: https://github.com/multiformats/multiformats
[MULTISIG]: https://github.com/cryptidtech/provenance-specifications/blob/main/specifications/multisig.md
[SSHKEY]: https://crates.io/crates/ssh-key
[RFC4251]: https://www.rfc-editor.org/rfc/rfc4251.html#page-11
[MULTIKEY]: https://github.com/cryptidtech/multi-key.git