Skip to content

Repository files navigation

Multisig

A Rust implementation of the multiformatsmultisig specification. 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 crate. Non-standard algorithms use RFC 4251 "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 crate.

Wire Format

A Multisig is serialized as:

SIGIL (0x1239) | signature_codec | Varbytes(message) | Varuint(num_attributes) |
[ AttrId | Varbytes(attribute_value) ] * num_attributes
  • 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.
  • messageVarbytes (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

CodecMulticodec nameSSH algorithmThresholdNotes
EddsaMsigeddsa-msigssh-ed25519noEd25519 signatures
Es256KMsiges256k-msigsecp256k1@multisignoECDSA over secp256k1
Es256Msiges256-msigecdsa-sha2-nistp256@multisignoECDSA over NIST P-256
Es384Msiges384-msigecdsa-sha2-nistp384@multisignoECDSA over NIST P-384
Es521Msiges521-msigecdsa-sha2-nistp521@multisignoECDSA over NIST P-521
Rs256Msigrs256-msigrsa-sha256@multisignoRSA-SHA256 signatures
Bls12381G1Msigbls12_381-g1-msigbls12_381-g1@multisigyesBLS signatures on G1 (48-byte sig)
Bls12381G2Msigbls12_381-g2-msigbls12_381-g2@multisigyesBLS signatures on G2 (96-byte sig)

Post-Quantum Signatures

CodecMulticodec nameSSHNotes
Mldsa65Msigmldsa-65-msignoML-DSA (Dilithium) security level 65; FIPS 204
Mldsa87Msigmldsa-87-msignoML-DSA security level 87; FIPS 204
FnDsa512Msigfn-dsa-512-msignoFN-DSA (Falcon) 512; FIPS 206 (draft)
FnDsa1024Msigfn-dsa-1024-msignoFN-DSA (Falcon) 1024; FIPS 206 (draft)
Mayo1Msigmayo-1-msignoMAYO-1
Mayo2Msigmayo-2-msignoMAYO-2
Mayo3Msigmayo-3-msignoMAYO-3
Mayo5Msigmayo-5-msignoMAYO-5
SlhdsaSha2128FMsigslhdsa-sha2-128f-msignoSLH-DSA (SPHINCS+) SHA-2 128f; FIPS 205
SlhdsaSha2128SMsigslhdsa-sha2-128s-msignoSLH-DSA SHA-2 128s
SlhdsaSha2192FMsigslhdsa-sha2-192f-msignoSLH-DSA SHA-2 192f
SlhdsaSha2192SMsigslhdsa-sha2-192s-msignoSLH-DSA SHA-2 192s
SlhdsaSha2256FMsigslhdsa-sha2-256f-msignoSLH-DSA SHA-2 256f
SlhdsaSha2256SMsigslhdsa-sha2-256s-msignoSLH-DSA SHA-2 256s
SlhdsaShake128FMsigslhdsa-shake-128f-msignoSLH-DSA SHAKE 128f
SlhdsaShake128SMsigslhdsa-shake-128s-msignoSLH-DSA SHAKE 128s
SlhdsaShake192FMsigslhdsa-shake-192f-msignoSLH-DSA SHAKE 192f
SlhdsaShake192SMsigslhdsa-shake-192s-msignoSLH-DSA SHAKE 192s
SlhdsaShake256FMsigslhdsa-shake-256f-msignoSLH-DSA SHAKE 256f
SlhdsaShake256SMsigslhdsa-shake-256s-msignoSLH-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.

CodecMulticodec nameComponentsSSH
Ed25519Mayo2Msiged25519-mayo2-msigEd25519 + MAYO-2no
Ed25519Mldsa65Msiged25519-mldsa65-msigEd25519 + ML-DSA-65no
Ed25519Fndsa512Msiged25519-fndsa512-msigEd25519 + FN-DSA-512no
Bls12381G1Mldsa65Msigbls12381-g1-mldsa65-msigBLS12-381 G1 + ML-DSA-65no
Bls12381G1Fndsa512Msigbls12381-g1-fndsa512-msigBLS12-381 G1 + FN-DSA-512no
Bls12381G1Mayo1Msigbls12381-g1-mayo1-msigBLS12-381 G1 + MAYO-1no
Bls12381G1Mayo2Msigbls12381-g1-mayo2-msigBLS12-381 G1 + MAYO-2no

Threshold Signature Shares (BLS12-381)

CodecMulticodec nameSSH algorithmNotes
Bls12381G1ShareMsigbls12_381-g1-share-msigbls12_381-g1-share@multisigA BLS G1 partial signature from a threshold share
Bls12381G2ShareMsigbls12_381-g2-share-msigbls12_381-g2-share@multisigA BLS G2 partial signature from a threshold share

Attribute IDs

Each Multisig carries a set of attributes identified by a u8 code:

CodeNameUsed byDescription
0sig-dataallThe raw signature bytes
1payload-encodingall (optional)The multicodec encoding of the signed payload
2schemeBLSBLS scheme type: 0=Basic, 1=MessageAugmentation, 2=ProofOfPossession
3thresholdBLS sharesThe threshold t (plaintext, Full disclosure mode)
4limitBLS sharesThe share count n (plaintext, Full/Partial disclosure modes)
5share-identifierBLS shares32-byte BLS scalar identifier for this share
6threshold-dataBLS combinedSerialized ThresholdData — the accumulated share map
7threshold-disclosureBLS (optional)Disclosure mode: 0=Full, 1=Partial, 2=FullConfidentialial
8encrypted-threshold-metaBLS (optional)AEAD-encrypted CBOR blob containing t and/or n
9threshold-meta-cipherBLS (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

TraitMethodsPurpose
AttrViewpayload_encoding(), scheme()Access the payload encoding codec and signing scheme
DataViewsig_bytes()Access the raw signature bytes
ConvViewto_ssh_signature()Convert to an OpenSSH ssh_key::Signature
ThresholdAttrViewthreshold(), limit(), identifier(), threshold_data()Read threshold parameters (BLS only)
ThresholdViewshares(), shares_with_disclosure(), add_share(), add_share_with_meta(), combine(), combine_with_meta()Accumulate and combine threshold signature shares (BLS only)
ThresholdDisclosureViewdisclosure_mode(), read_threshold_params(), to_disclosure()Read or convert the threshold disclosure mode (all codecs)
Viewsattr_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

Codec familyAttrViewDataViewConvViewThresholdAttrViewThresholdView
BLS G1/G2 (combined + share)bls12381::Viewbls12381::Viewbls12381::Viewbls12381::Viewbls12381::View (combined only)
Ed25519ed25519::Viewed25519::Viewed25519::View
secp256k1secp256k1::Viewsecp256k1::Viewsecp256k1::View
NIST P-256/384/521nist_p::Viewnist_p::Viewnist_p::View
RSArsa::Viewrsa::Viewrsa::View
ML-DSA 65/87ml_dsa::Viewml_dsa::Viewml_dsa::View
FN-DSA 512/1024fn_dsa::Viewfn_dsa::Viewfn_dsa::View
MAYO 1/2/3/5mayo::Viewmayo::Viewmayo::View
SLH-DSA (all 12)slh_dsa::Viewslh_dsa::Viewslh_dsa::View
Ed25519-MAYO2ed25519_mayo2::Viewed25519_mayo2::Viewed25519_mayo2::View
Other hybridsed25519_hybrid::Viewed25519_hybrid::Viewed25519_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 newMultisig. The original is unchanged. This is most visible in Builder::try_build():

letmut 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:

MethodDescription
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)
.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 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

use multi_key::{Builder,Views};use multi_codec::Codec;// Generate an Ed25519 key and sign a messagelet 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

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:

SchemeCodeDescription
Basic0Raw BLS. It is vulnerable to rogue-key attacks without PoP checking.
MessageAugmentation1Prepends a domain tag to the message before signing.
ProofOfPossession2Requires 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

use multi_key::{Builder,Views};use multi_codec::Codec;// Split a BLS G2 key into 3-of-5 shareslet 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 Multisigletmut ms = partial_sigs[0].clone();for ps in&partial_sigs[1..]{
ms = ms.threshold_view()?.add_share(ps)?;}// Combine into the final signaturelet 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

Modet (threshold)n (limit)Who sees tWho sees n
Full (default, 0)plaintext attributeplaintext attributeeveryoneeveryone
Partial (1)encrypted (AEAD)plaintext attributekey-holder onlyeveryone (auditable)
FullConfidentialial (2)encrypted (AEAD)encrypted (AEAD)key-holder onlykey-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

ConsiderationFullPartialFullConfidentialial
Backward compatibleyesyes (attribute defaults to Full if absent)yes
Observer learns tyesnono
Observer learns nyesyesno
Requires meta_keynofor reading tfor reading t and n
Auditable nyesyesno
Risk if meta_key lostn/at irrecoverablet and n irrecoverable
Performance overheadnonenegligible (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():

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:

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:

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:

let(t, n) = encrypted.disclosure_view()?
.read_threshold_params(Some(&meta_mk))?;

Combining Encrypted Shares

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):

// Full -> Partiallet partial = full.disclosure_view()?
.to_disclosure(ThresholdDisclosure::Partial,Some(&meta_mk),None)?;// Partial -> FullConfidentialiallet confidential = partial.disclosure_view()?
.to_disclosure(ThresholdDisclosure::FullConfidentialial,Some(&meta_mk),Some(&meta_mk))?;// FullConfidentialial -> Fulllet 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

FeatureDefaultDescription
serdeyesSerde serialization for Multisig and AttrId

Links

About

Multiformats implementation for digital signatures

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages