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
57 changes: 48 additions & 9 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,62 @@ name: Rust

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

env:
CARGO_TERM_COLOR: always

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust Toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Check Code Format
run: cargo fmt --all -- --check

- name: Code Lint
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Build
run: cargo build --verbose --all-features

- name: Run tests
run: cargo test --verbose --all-features

msrv:
name: Verify MSRV (1.85)
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: |
cargo test --verbose
cargo test --verbose --all-targets --all-features
- uses: actions/checkout@v4

- name: Install Rust Toolchain (1.85)
uses: dtolnay/rust-toolchain@1.85.0

- name: Check
run: cargo check --all-features

audit:
name: Security Audit
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust Toolchain
uses: dtolnay/rust-toolchain@stable

- name: Install cargo-audit
run: cargo install cargo-audit

- name: Run cargo audit
run: cargo audit
151 changes: 151 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,157 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.8] - 2026-07-16

### Security
- Removed `rsa` crate from dependency tree by dropping the unnecessary `crypto`
feature from `ssh-key` on native targets (was `["crypto"]`, now
`["alloc", "ecdsa", "ed25519"]` — matching the wasm target). multi-sig only
uses `ssh_key::Signature`/`Algorithm`/`AlgorithmName` (encoding types); the
`crypto` feature pulled in `ssh-key`'s `rsa` feature, which dragged in the
vulnerable `rsa 0.10.0-rc.18` (RUSTSEC-2023-0071, Marvin Attack). The RSA
view uses `Algorithm::Other(...)` not `Algorithm::Rsa`, so no `rsa` feature
is needed.
- Removed unmaintained `serde_cbor` dev-dependency (RUSTSEC-2021-0127). Replaced
with `ciborium` (already a runtime dependency) in 4 CBOR round-trip tests.

### Changed
- `Multisig` non-human-readable `Deserialize` path now uses
`deserialize_byte_buf` with a `ByteBufVisitor` that accepts borrowed bytes,
owned bytes, and byte buffers — compatible with `serde_test`, `serde_cbor`,
and `ciborium` (the previous `&'de [u8]` bound only worked with
deserializers that lend borrowed slices).

### Dependencies
- `ssh-key` (native target): `features = ["crypto"]` →
`default-features = false, features = ["alloc", "ecdsa", "ed25519"]`
- Removed `serde_cbor = "0.11"` dev-dependency
- Dependency count reduced from 233 to 221 crates

## [1.0.7] - 2026-07-16

### Security
- Added `MAX_DECODED_SIZE = 16 MiB` total decoded-size cap to
`Multisig::try_decode_from` (tracks consumed bytes across the attribute
decode loop, returns `Error::InputTooLarge`). Per-attribute payloads are
also individually capped by `Varbytes::MAX_DECODED_SIZE` via `multi_util`.
Mitigates CWE-400.
- Added `MAX_THRESHOLD_PARTICIPANTS = 1024` cap in `threshold_meta.rs`,
enforced in `bls12381.rs` `SigShare::try_decode_from` where threshold/limit
values are decoded (returns `Error::TooManyParticipants`). Mitigates CWE-400.
- Added `new_from_bls_signature_with_codec(codec, sig)` and
`new_from_bls_signature_share_with_codec(codec, threshold, limit, sigshare)`
constructors that take an explicit BLS12-381 codec, avoiding the
length-based codec inference heuristic (48 bytes → G1, 96 bytes → G2).
- Deprecated `new_from_bls_signature` and `new_from_bls_signature_share` with
`#[deprecated]` notes pointing to the explicit-codec constructors.
- Updated internal `combine` method to use `new_from_bls_signature_with_codec`.

### Changed
- Upgraded to Edition 2024 (`edition = "2024"`, `rust-version = "1.85"`).
- Added `[lints.clippy]` (pedantic/nursery/cargo at warn) and
`[lints.rust] unsafe_code = "deny"` with targeted `#![allow(...)]` for
stylistic lints.
- Added `Error::InputTooLarge { claimed, max }` and
`Error::TooManyParticipants(usize, usize)` error variants.
- Exported `MAX_DECODED_SIZE` and `MAX_THRESHOLD_PARTICIPANTS` from crate root.

### CI
- Expanded CI from build+test to include: fmt check, clippy `-D warnings`,
MSRV (1.85) check, and cargo audit job.

### Documentation
- Added `SECURITY.md` documenting std-only status, RC dependencies
(`blsful`, `ssh-key`, `vsss-rs`), decoded-size caps, BLS codec inference,
and memory safety properties.

### Tests
- Added `test_too_many_attributes_rejected` and `test_valid_roundtrip_with_caps`.

## [1.0.6] - 2026-07-16

### Changed
- Made `serde` a required dependency (the `threshold_meta` module always
derives `Serialize`/`Deserialize` for its CBOR blob types). The `serde`
feature flag is retained for backward compatibility and controls only the
public `serde` impl module.
- Upgraded `chacha20poly1305` from 0.10 to 0.11.
- Upgraded `getrandom` from 0.2 to 0.4.
- Simplified `Error` type (removed redundant variants).

## [1.0.5] - 2026-07-14

### Added
- Synced from bettersign workspace: PQC signature views (ML-DSA, FN-DSA,
MAYO, SLH-DSA, RSA, NIST-P), hybrid signature views (Ed25519+MAYO2,
Ed25519+ML-DSA-65, Ed25519+FN-DSA-512), `types.rs` module with type-safe
wrappers.
- Added threshold disclosure modes (`ThresholdDisclosure::Full`,
`Partial`, `FullConfidentialial`) with ChaCha20-Poly1305 AEAD encryption
of threshold metadata (`threshold_meta.rs`).
- Added `AttrId` variants: `ThresholdDisclosure`,
`EncryptedThresholdMeta`, `ThresholdMetaCipher`.
- Added `DisclosureView` for threshold disclosure mode operations.
- Added comprehensive test suite: `edge_case_tests.rs`,
`proptest_tests.rs`, `security_tests.rs`.
- Added `Builder::with_disclosure` and
`Builder::with_encrypted_threshold_meta`.
- Added `MAX_ATTRIBUTES = 256` cap on attribute count in
`Multisig::try_decode_from` (returns `Error::TooManyAttributes`).
- Added benchmarks (`multisig_bench.rs`).
- Added BLS threshold signing support with share combine/split.
- Added SSH signature conversion (`ConvView::to_ssh_signature`).
- Added `PayloadEncoding` attribute and `AttrView` trait.
- Added `Null` impl for `Multisig`.

### Changed
- Refactored `Multisig` to be attributes-based (like `Multikey`).
- Updated `README.md` with comprehensive documentation.
- Updated codec names for multicodec table sync.
- Updated `blsful` dependency.
- `ssh-key` `default-features = false` for `wasm32-*` targets.
- Put `ssh-*` behind a feature flag for non-wasm32 targets.

### Fixed
- Fixed wire serialization.
- Fixed codec updates.
- Fixed serde of `AttrId`.
- Fixed builder from BLS signature.
- Fixed clippy warnings.

## [1.0.4] - 2025-07-18

### Changed
- Simplified `Deserialize` implementation for `Multisig`.
- Fixed clippy warnings.

## [1.0.3] - 2024-12-02

### Changed
- Updated `blsful` crate version.

## [1.0.2] - 2024-08-27

### Added
- WASM support: `ssh-key` with `default-features = false` for `wasm32-*`
targets.
- CI testing for all targets and features.

### Changed
- Updated codec names for multicodec table sync.
- Updated `LICENSE` file.
- Fixed multibase dependency.
- Fixed codec updates.
- Fixed clippy warnings.

### Fixed
- Fixed tests for updated dependencies.

## [1.0.1] - 2026-07-13

### Fixed
- Fixed codec names after multicodec table sync.

## [1.0.0] - 2026-07-13

### Changed
Expand Down
16 changes: 12 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[package]
name = "multi-sig"
version = "1.0.6"
edition = "2021"
version = "1.0.8"
edition = "2024"
rust-version = "1.85"
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]
description = "Multisig self-describing multicodec implementation for digital signatures"
repository = "https://github.com/cryptidtech/multi-sig.git"
Expand Down Expand Up @@ -40,17 +41,24 @@ ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["allo

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
blsful = { version = "4.0.0-rc1", default-features = false, features = ["blst"] }
ssh-key = { version = "0.7.0-rc.11", features = ["crypto"] }
ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["alloc", "ecdsa", "ed25519"] }

[dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] }
hex = "0.4"
proptest = "1.4"
serde_cbor = "0.11"
serde_json = "1.0"
serde_test = "1.0"

[[bench]]
name = "multisig_bench"
harness = false
path = "benches/multisig_bench.rs"

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }

[lints.rust]
unsafe_code = "deny"
66 changes: 66 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Security Policy

## Overview

The `multi-sig` crate provides self-describing digital signatures following
the multisig specification. This document outlines the security properties,
threat model, and guarantees of this crate.

## std-only Status

This crate is **std-only**. It depends on `std::collections::BTreeMap`,
`std::fmt`, and `unsigned-varint` with the `std` feature. The crypto
dependency stack (`blsful`, `ssh-key`, `chacha20poly1305`) also requires
std. A `no_std` conversion is not planned for this crate.

## Release-Candidate Dependencies

This crate depends on the following release-candidate (RC) crates:

- `blsful = "4.0.0-rc1"` — BLS12-381 signature implementation
- `ssh-key = "0.7.0-rc.11"` — SSH key/signature encoding
- `vsss-rs = "6.0.0-rc2"` (transitive via `blsful`) — verifiable secret
sharing

These are pinned to RC versions because stable releases are not yet
available. This is a **tracked acceptance**: the RC versions are reviewed
on each release and will be upgraded to stable when available. Consumers
should be aware that RC APIs may change before stabilisation.

## Decoded-Size Caps

The decoder enforces the following caps on untrusted wire data to mitigate
CWE-400 (Uncontrolled Resource Consumption):

- **`MAX_ATTRIBUTES = 256`** — maximum number of attributes per `Multisig`.
- **`MAX_DECODED_SIZE = 16 MiB`** — maximum total decoded bytes per
`Multisig`. Tracked across the attribute decode loop.
- **`MAX_THRESHOLD_PARTICIPANTS = 1024`** — maximum threshold or limit
value in a BLS signature share.
- Per-attribute `Varbytes` payloads are individually capped by
`multi_util::varbytes::MAX_DECODED_SIZE` (16 MiB).

Exceeding any cap returns a clean `Err` (`Error::TooManyAttributes`,
`Error::InputTooLarge`, or `Error::TooManyParticipants`); the decoder never
panics on oversized input.

## BLS12-381 Codec Inference

The deprecated `Builder::new_from_bls_signature` and
`Builder::new_from_bls_signature_share` constructors infer the BLS12-381
codec (G1 vs G2) from the compressed-point byte length (48 bytes -> G1,
96 bytes -> G2). This is a heuristic, not cryptographic binding. Prefer
`new_from_bls_signature_with_codec` and
`new_from_bls_signature_share_with_codec`, which take an explicit codec
parameter.

## Memory Safety

- **No unsafe code**: `#![deny(unsafe_code)]` is enforced at compile time.
- **Input validation**: All decode paths validate lengths, attribute
counts, and codec identifiers.

## Reporting Vulnerabilities

Report security issues via the project's GitHub issue tracker or privately
to the maintainers.
7 changes: 6 additions & 1 deletion benches/multisig_bench.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
//! Performance benchmarks for multi-sig
#![allow(
clippy::semicolon_if_nothing_returned,
clippy::uninlined_format_args,
clippy::doc_markdown
)]

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use multi_codec::Codec;
use multi_sig::{Builder, Multisig, SIG_CODECS};
use multi_trait::TryDecodeFrom;
Expand Down
2 changes: 1 addition & 1 deletion src/attrid.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{error::AttributesError, Error};
use crate::{Error, error::AttributesError};
use multi_trait::{EncodeInto, TryDecodeFrom};
use std::fmt;

Expand Down
23 changes: 22 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,31 @@ pub enum Error {
DuplicateAttribute(u8),
/// Attribute count exceeds the configured maximum
///
/// Returned by [`crate::ms::Multisig::try_decode_from`] when the number of
/// Returned by `Multisig::try_decode_from` when the number of
/// attributes declared in the wire data exceeds
/// [`crate::ms::MAX_ATTRIBUTES`]. Bounds the work a crafted input can
/// force the decoder to perform and mitigates CWE-400.
#[error("attribute count {0} exceeds maximum {1}")]
TooManyAttributes(usize, usize),
/// Decoded size exceeds the configured maximum
///
/// Returned by `Multisig::try_decode_from` when the total
/// decoded byte count exceeds [`crate::ms::MAX_DECODED_SIZE`]. Bounds the
/// worst-case allocation for untrusted wire data and mitigates CWE-400
/// (Uncontrolled Resource Consumption).
#[error("decoded size {claimed} exceeds maximum {max}")]
InputTooLarge {
/// The number of bytes claimed by the wire data
claimed: usize,
/// The configured maximum decoded size
max: usize,
},
/// Participant count exceeds the configured maximum
///
/// Returned when a threshold or limit value decoded from a BLS share
/// exceeds [`crate::views::threshold_meta::MAX_THRESHOLD_PARTICIPANTS`].
#[error("participant count {0} exceeds maximum {1}")]
TooManyParticipants(usize, usize),
/// Failed Varsig conversion
#[error("Failed Varsig conversion: {0}")]
FailedConversion(String),
Expand Down Expand Up @@ -189,6 +208,8 @@ impl Error {
Self::MissingSigil => "MissingSigil",
Self::DuplicateAttribute(_) => "DuplicateAttribute",
Self::TooManyAttributes(_, _) => "TooManyAttributes",
Self::InputTooLarge { .. } => "InputTooLarge",
Self::TooManyParticipants(_, _) => "TooManyParticipants",
Self::FailedConversion(_) => "FailedConversion",
Self::UnsupportedAlgorithm(_) => "UnsupportedAlgorithm",
}
Expand Down
Loading
Loading