diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 731d071..d20f7a0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 23f23d5..5449b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index affeb76..93164a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 "] description = "Multisig self-describing multicodec implementation for digital signatures" repository = "https://github.com/cryptidtech/multi-sig.git" @@ -40,13 +41,12 @@ 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" @@ -54,3 +54,11 @@ serde_test = "1.0" 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" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..663c858 --- /dev/null +++ b/SECURITY.md @@ -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. \ No newline at end of file diff --git a/benches/multisig_bench.rs b/benches/multisig_bench.rs index b117e1a..7eb42cf 100644 --- a/benches/multisig_bench.rs +++ b/benches/multisig_bench.rs @@ -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; diff --git a/src/attrid.rs b/src/attrid.rs index f80de11..ca4c6f1 100644 --- a/src/attrid.rs +++ b/src/attrid.rs @@ -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; diff --git a/src/error.rs b/src/error.rs index b4005d6..28bde2c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), @@ -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", } diff --git a/src/lib.rs b/src/lib.rs index 980c7b4..075f286 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,65 @@ unused_import_braces, unused_qualifications )] +// Pedantic/nursery lints are enabled at the workspace level via +// `[lints.clippy]` in Cargo.toml. The following allows suppress stylistic +// lints that would require large-scale churn for minimal security benefit. +// Each is reviewed individually. +#![allow( + // doc-markdown wrapping common terms in backticks is stylistic + clippy::doc_markdown, + // elidable_lifetime_names: explicit lifetimes aid readability + clippy::elidable_lifetime_names, + // missing_errors_doc: public APIs are documented; this is noisy on impl + // blocks and trait impls + clippy::missing_errors_doc, + // missing_panics_doc: we avoid panics; where they exist they are documented + clippy::missing_panics_doc, + // must_use_candidate: too noisy on every function returning Result + clippy::must_use_candidate, + // return_self_not_must_use: builder pattern returns self by design + clippy::return_self_not_must_use, + // use_self: causes churn in match arms on large enums + clippy::use_self, + // semicolon_if_nothing_returned: stylistic preference + clippy::semicolon_if_nothing_returned, + // or_fun_call: false positives for cheap constructors like BTreeMap::new + clippy::or_fun_call, + // missing_const_for_fn: many functions can't be const due to trait bounds + clippy::missing_const_for_fn, + // multiple_crate_versions: blsful pulls in duplicate versions (tracked) + clippy::multiple_crate_versions, + // too_many_lines: large match arms are inherent to this crate + clippy::too_many_lines, + // option_if_let_else: sometimes less readable than if-let + clippy::option_if_let_else, + // needless_for_each: false positives in test code + clippy::needless_for_each, + // single_match_else: single-arm match with else is clearer in context + clippy::single_match_else, + // uninlined_format_args: stylistic; format strings are clear as-is + clippy::uninlined_format_args, + // cast_possible_truncation: intentional in codec conversions + clippy::cast_possible_truncation, + // redundant_pub_crate: needed for crate-internal module visibility + clippy::redundant_pub_crate, + // redundant_clone: false positives where clone is needed for ownership + clippy::redundant_clone, + // items_after_statements: module-level items are ordered logically + clippy::items_after_statements, + // if_not_else: stylistic + clippy::if_not_else, + // explicit_iter_loop: into_iter() is idiomatic + clippy::explicit_iter_loop, + // enum_glob_use: glob imports of enums are used judiciously + clippy::enum_glob_use, + // branches_sharing_code: false positives in complex match arms + clippy::branches_sharing_code, + // too_long_first_doc_paragraph: crate-level doc paragraph length + clippy::too_long_first_doc_paragraph, + // unnecessary_semicolon: false positive + clippy::unnecessary_semicolon, +)] /// Errors produced by this library pub mod error; @@ -78,7 +137,10 @@ pub use attrid::AttrId; /// Multisig implementation pub mod ms; -pub use ms::{Builder, EncodedMultisig, Multisig, MAX_ATTRIBUTES, SIG_CODECS, SIG_SHARE_CODECS}; +pub use ms::{ + Builder, EncodedMultisig, MAX_ATTRIBUTES, MAX_DECODED_SIZE, Multisig, SIG_CODECS, + SIG_SHARE_CODECS, +}; /// Type-safe wrappers for signature components pub mod types; @@ -87,9 +149,9 @@ pub use types::{SignatureBytes, SignatureScheme}; /// Views on the multisig pub mod views; pub use views::{ - decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, AttrView, ConvView, - DataView, ThresholdAttrView, ThresholdDisclosure, ThresholdDisclosureView, ThresholdMetaCipher, - ThresholdMetadata, ThresholdView, Views, + AttrView, ConvView, DataView, MAX_THRESHOLD_PARTICIPANTS, ThresholdAttrView, + ThresholdDisclosure, ThresholdDisclosureView, ThresholdMetaCipher, ThresholdMetadata, + ThresholdView, Views, decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, }; /// Serde serialization diff --git a/src/ms.rs b/src/ms.rs index e61dc71..145fc82 100644 --- a/src/ms.rs +++ b/src/ms.rs @@ -1,17 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ + AttrId, AttrView, ConvView, DataView, Error, ThresholdAttrView, ThresholdView, Views, error::AttributesError, views::{ + DisclosureView, ThresholdDisclosure, ThresholdDisclosureView, bls12381::{self, SchemeTypeId}, ed25519, ed25519_hybrid, ed25519_mayo2, fn_dsa, mayo, ml_dsa, nist_p, rsa, secp256k1, - slh_dsa, threshold_meta, DisclosureView, ThresholdDisclosure, ThresholdDisclosureView, + slh_dsa, threshold_meta, }, - AttrId, AttrView, ConvView, DataView, Error, ThresholdAttrView, ThresholdView, Views, }; use blsful::{ + Signature, SignatureShare, inner_types::{GroupEncoding, PrimeField}, vsss_rs::Share, - Signature, SignatureShare, }; use multi_base::Base; use multi_codec::Codec; @@ -76,6 +77,16 @@ pub const SIGIL: Codec = Codec::Multisig; /// can force the decoder to perform (mitigates CWE-400). pub const MAX_ATTRIBUTES: usize = 256; +/// Maximum total decoded size (in bytes) a single [`Multisig`] will accept +/// when decoding from untrusted wire data. +/// +/// The 16 MiB ceiling comfortably exceeds every legitimate multisig payload in +/// this stack while bounding the worst-case allocation an attacker can trigger +/// with a crafted length prefix. Each `Varbytes` attribute payload is also +/// individually capped by [`multi_util::varbytes::MAX_DECODED_SIZE`] via the +/// `Varbytes::try_decode_from` path. Mitigates CWE-400. +pub const MAX_DECODED_SIZE: usize = 16 * 1024 * 1024; + /// a base encoded varsig pub type EncodedMultisig = BaseEncoded; @@ -149,6 +160,9 @@ impl<'a> TryDecodeFrom<'a> for Multisig { type Error = Error; fn try_decode_from(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> { + // Track total consumed bytes to enforce MAX_DECODED_SIZE (CWE-400). + let start_len = bytes.len(); + // decode the sigil let (sigil, ptr) = Codec::try_decode_from(bytes)?; if sigil != SIGIL { @@ -175,9 +189,20 @@ impl<'a> TryDecodeFrom<'a> for Multisig { for _ in 0..*num_attr { let (id, ptr) = AttrId::try_decode_from(p)?; let (attr, ptr) = Varbytes::try_decode_from(ptr)?; + // Per-attribute size is already capped by Varbytes' + // MAX_DECODED_SIZE (16 MiB). The total decoded-size cap + // below provides a second layer of protection. if attributes.insert(id, (*attr).clone()).is_some() { return Err(Error::DuplicateAttribute(id.code())); } + // Enforce total decoded size cap + let consumed = start_len - ptr.len(); + if consumed > MAX_DECODED_SIZE { + return Err(Error::InputTooLarge { + claimed: consumed, + max: MAX_DECODED_SIZE, + }); + } p = ptr; } (attributes, p) @@ -436,7 +461,7 @@ impl Builder { } bls12381::ALGORITHM_NAME_G1_SHARE => { let sig_share = bls12381::SigShare::try_from(sig.as_bytes())?; - attributes.insert(AttrId::ShareIdentifier, sig_share.0 .0.to_be_bytes().into()); + attributes.insert(AttrId::ShareIdentifier, sig_share.0.0.to_be_bytes().into()); attributes.insert(AttrId::Threshold, Varuint(sig_share.1).into()); attributes.insert(AttrId::Limit, Varuint(sig_share.2).into()); attributes.insert(AttrId::Scheme, sig_share.3.into()); @@ -449,7 +474,7 @@ impl Builder { } bls12381::ALGORITHM_NAME_G2_SHARE => { let sig_share = bls12381::SigShare::try_from(sig.as_bytes())?; - attributes.insert(AttrId::ShareIdentifier, sig_share.0 .0.to_be_bytes().into()); + attributes.insert(AttrId::ShareIdentifier, sig_share.0.0.to_be_bytes().into()); attributes.insert(AttrId::Threshold, Varuint(sig_share.1).into()); attributes.insert(AttrId::Limit, Varuint(sig_share.2).into()); attributes.insert(AttrId::Scheme, sig_share.3.into()); @@ -467,29 +492,37 @@ impl Builder { } /// create a new builder from a Bls Signature + /// + /// # Known limitation (length-based codec inference) + /// + /// The BLS12-381 codec (`Bls12381G1Msig` vs `Bls12381G2Msig`) is selected + /// from the compressed-point byte length: 48 bytes -> G1, 96 bytes -> G2. + /// This is a heuristic rather than cryptographic binding — a 48-byte G2 + /// signature or a 96-byte G1 signature (both invalid for BLS12-381 but + /// constructable by an attacker controlling the input) would be + /// misclassified. Downstream code that trusts this codec tag for curve + /// selection must re-validate the signature against the intended curve + /// rather than relying on the codec alone. + /// + /// Prefer [`Self::new_from_bls_signature_with_codec`] when the curve is + /// known at the call site. + #[deprecated( + since = "1.0.7", + note = "length-based codec inference is ambiguous; use new_from_bls_signature_with_codec" + )] pub fn new_from_bls_signature(sig: &Signature) -> Result where C: blsful::BlsSignatureImpl, { let scheme_type_id = SchemeTypeId::from(sig); let sig_bytes: Vec = sig.as_raw_value().to_bytes().as_ref().to_vec(); - // # Known limitation (length-based codec inference) - // - // The BLS12-381 codec (`Bls12381G1Msig` vs `Bls12381G2Msig`) is selected - // from the compressed-point byte length: 48 bytes -> G1, 96 bytes -> G2. - // This is a heuristic rather than cryptographic binding — a 48-byte G2 - // signature or a 96-byte G1 signature (both invalid for BLS12-381 but - // constructable by an attacker controlling the input) would be - // misclassified. Downstream code that trusts this codec tag for curve - // selection must re-validate the signature against the intended curve - // rather than relying on the codec alone. let codec = match sig_bytes.len() { 48 => Codec::Bls12381G1Msig, // G1Projective::to_compressed() 96 => Codec::Bls12381G2Msig, // G2Projective::to_compressed() _ => { return Err(Error::UnsupportedAlgorithm( "invalid Bls signature size".to_string(), - )) + )); } }; let mut attributes = BTreeMap::new(); @@ -502,7 +535,62 @@ impl Builder { }) } + /// Create a new builder from a BLS signature with an explicit codec. + /// + /// This constructor avoids the length-based codec inference heuristic + /// used by [`Self::new_from_bls_signature`] by requiring the caller to + /// specify the BLS12-381 codec (`Bls12381G1Msig` or `Bls12381G2Msig`) + /// directly. Prefer this constructor when the curve is known at the call + /// site. + /// + /// # Errors + /// + /// Returns [`Error::UnsupportedAlgorithm`] if `codec` is not a BLS12-381 + /// signature codec. + pub fn new_from_bls_signature_with_codec( + codec: Codec, + sig: &Signature, + ) -> Result + where + C: blsful::BlsSignatureImpl, + { + match codec { + Codec::Bls12381G1Msig | Codec::Bls12381G2Msig => {} + _ => { + return Err(Error::UnsupportedAlgorithm(format!( + "{codec:?} is not a BLS12-381 signature codec" + ))); + } + } + let scheme_type_id = SchemeTypeId::from(sig); + let sig_bytes: Vec = sig.as_raw_value().to_bytes().as_ref().to_vec(); + let mut attributes = BTreeMap::new(); + attributes.insert(AttrId::SigData, sig_bytes); + attributes.insert(AttrId::Scheme, scheme_type_id.into()); + Ok(Self { + codec, + attributes: Some(attributes), + ..Default::default() + }) + } + /// create a new builder from a Bls SignatureShare + /// + /// # Known limitation (length-based codec inference) + /// + /// The share codec (`Bls12381G1ShareMsig` vs `Bls12381G2ShareMsig`) is + /// selected from the compressed-point byte length: 48 bytes -> G1, + /// 96 bytes -> G2. As with [`Self::new_from_bls_signature`], this is a + /// heuristic, not cryptographic binding; downstream consumers must + /// re-validate against the intended curve rather than trusting the + /// codec tag alone. + /// + /// Prefer [`Self::new_from_bls_signature_share_with_codec`] when the + /// curve is known at the call site. + #[deprecated( + since = "1.0.7", + note = "length-based codec inference is ambiguous; use new_from_bls_signature_share_with_codec" + )] pub fn new_from_bls_signature_share( threshold: usize, limit: usize, @@ -515,21 +603,13 @@ impl Builder { let sigshare = sigshare.as_raw_value(); let identifier = sigshare.identifier().0.to_repr().as_ref().to_vec(); let value = sigshare.value().0.to_bytes().as_ref().to_vec(); - // # Known limitation (length-based codec inference) - // - // The share codec (`Bls12381G1ShareMsig` vs `Bls12381G2ShareMsig`) is - // selected from the compressed-point byte length: 48 bytes -> G1, - // 96 bytes -> G2. As with [`Self::new_from_bls_signature`], this is a - // heuristic, not cryptographic binding; downstream consumers must - // re-validate against the intended curve rather than trusting the - // codec tag alone. let codec = match value.len() { 48 => Codec::Bls12381G1ShareMsig, // large pubkeys, small signatures 96 => Codec::Bls12381G2ShareMsig, // small pubkeys, large signatures _ => { return Err(Error::UnsupportedAlgorithm( "invalid Bls signature size".to_string(), - )) + )); } }; let mut attributes = BTreeMap::new(); @@ -545,6 +625,51 @@ impl Builder { }) } + /// Create a new builder from a BLS signature share with an explicit codec. + /// + /// This constructor avoids the length-based codec inference heuristic + /// used by [`Self::new_from_bls_signature_share`] by requiring the caller + /// to specify the BLS12-381 share codec + /// (`Bls12381G1ShareMsig` or `Bls12381G2ShareMsig`) directly. + /// + /// # Errors + /// + /// Returns [`Error::UnsupportedAlgorithm`] if `codec` is not a BLS12-381 + /// signature share codec. + pub fn new_from_bls_signature_share_with_codec( + codec: Codec, + threshold: usize, + limit: usize, + sigshare: &SignatureShare, + ) -> Result + where + C: blsful::BlsSignatureImpl, + { + match codec { + Codec::Bls12381G1ShareMsig | Codec::Bls12381G2ShareMsig => {} + _ => { + return Err(Error::UnsupportedAlgorithm(format!( + "{codec:?} is not a BLS12-381 signature share codec" + ))); + } + } + let scheme_type_id = SchemeTypeId::from(sigshare); + let sigshare = sigshare.as_raw_value(); + let identifier = sigshare.identifier().0.to_repr().as_ref().to_vec(); + let value = sigshare.value().0.to_bytes().as_ref().to_vec(); + let mut attributes = BTreeMap::new(); + attributes.insert(AttrId::SigData, value); + attributes.insert(AttrId::Threshold, Varuint(threshold).into()); + attributes.insert(AttrId::Limit, Varuint(limit).into()); + attributes.insert(AttrId::ShareIdentifier, identifier); + attributes.insert(AttrId::Scheme, scheme_type_id.into()); + Ok(Self { + codec, + attributes: Some(attributes), + ..Default::default() + }) + } + /// set the base encoding codec pub fn with_base_encoding(mut self, base: Base) -> Self { self.base_encoding = Some(base); @@ -714,6 +839,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_bls_signature() { let sk = blsful::Bls12381G2::new_secret_key(); let sig = sk @@ -733,6 +859,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_bls_signature_combine() { let sk = blsful::Bls12381G2::new_secret_key(); let sig = sk @@ -815,6 +942,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_bls_signature_ssh_roundtrip() { let sk = blsful::Bls12381G1::new_secret_key(); let sig = sk @@ -841,6 +969,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_bls_signature_combine_ssh_roundtrip() { let sk = blsful::Bls12381G2::new_secret_key(); let sig = sk @@ -905,4 +1034,41 @@ mod tests { assert_eq!(ms1, ms2); assert!(ms2.is_null()); } + + #[test] + fn test_too_many_attributes_rejected() { + use multi_trait::EncodeInto; + // Craft a multisig that claims more than MAX_ATTRIBUTES attributes. + // It should be rejected with TooManyAttributes, not panic. + let mut bad = Vec::new(); + let sigil_bytes: Vec = Codec::Multisig.into(); + bad.extend(sigil_bytes); // sigil + let codec_bytes: Vec = Codec::EddsaMsig.into(); + bad.extend(codec_bytes); // codec + let msg = Varbytes::new(Vec::new()); + bad.extend(msg.encode_into()); // empty message + bad.extend(Varuint(MAX_ATTRIBUTES + 1).encode_into()); // too many attrs + + let result = Multisig::try_from(bad.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + Error::TooManyAttributes(n, max) => { + assert_eq!(n, MAX_ATTRIBUTES + 1); + assert_eq!(max, MAX_ATTRIBUTES); + } + e => panic!("Expected TooManyAttributes, got: {e:?}"), + } + } + + #[test] + fn test_valid_roundtrip_with_caps() { + // Sanity: a well-formed multisig still round-trips with the caps in place. + let ms = Builder::new(Codec::EddsaMsig) + .with_signature_bytes(&[0u8; 64]) + .try_build() + .unwrap(); + let v: Vec = ms.clone().into(); + let ms2 = Multisig::try_from(v.as_slice()).unwrap(); + assert_eq!(ms, ms2); + } } diff --git a/src/serde/de.rs b/src/serde/de.rs index 99457ef..d61e7e9 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - ms::{self, Attributes}, AttrId, Multisig, + ms::{self, Attributes}, }; use core::fmt; use multi_codec::Codec; use multi_util::EncodedVarbytes; use serde::{ - de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, + de::{Error, MapAccess, Visitor}, }; /// Deserialize instance of [`crate::AttrId`] @@ -143,8 +143,35 @@ impl<'de> Deserialize<'de> for Multisig { if deserializer.is_human_readable() { deserializer.deserialize_struct(ms::SIGIL.as_str(), FIELDS, MultisigVisitor) } else { - let b: &'de [u8] = Deserialize::deserialize(deserializer)?; - Ok(Self::try_from(b).map_err(|e| Error::custom(e.to_string()))?) + // Use `deserialize_byte_buf` with a visitor that accepts both + // borrowed and owned bytes. This works with `serde_test` + // (BorrowedBytes), `serde_cbor` (borrowed), and `ciborium` + // (owned). The previous `&'de [u8]` bound only worked with + // deserializers that support borrowing from input. + struct ByteBufVisitor; + + impl<'de> Visitor<'de> for ByteBufVisitor { + type Value = Vec; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("byte buffer") + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + } + + let b = deserializer.deserialize_byte_buf(ByteBufVisitor)?; + Ok(Self::try_from(b.as_slice()).map_err(|e| Error::custom(e.to_string()))?) } } } diff --git a/src/serde/mod.rs b/src/serde/mod.rs index f2d7bfd..cf2ce38 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -9,7 +9,15 @@ mod tests { use multi_base::Base; use multi_codec::Codec; use multi_trait::Null; - use serde_test::{assert_tokens, Configure, Token}; + use serde_test::{Configure, Token, assert_tokens}; + + /// Serialize a value to CBOR bytes using `ciborium` (replaces the + /// unmaintained `serde_cbor` dev-dependency). + fn cbor_to_vec(value: &T) -> Vec { + let mut buf = Vec::new(); + ciborium::into_writer(value, &mut buf).expect("CBOR serialize"); + buf + } #[test] fn test_ed25519_serde_compact() { @@ -38,8 +46,9 @@ mod tests { assert_tokens( &ms.readable(), - &[Token::BorrowedStr("zD4bHwUem3jQTfFd82d2koBo7sa2cAr9mvAJcXEVSAPe8mjDHRaGRYYjFmphxaAsUhENDevuR7J3xtWpW41pqEKrpMQfkZEwFopdm") - ], + &[Token::BorrowedStr( + "zD4bHwUem3jQTfFd82d2koBo7sa2cAr9mvAJcXEVSAPe8mjDHRaGRYYjFmphxaAsUhENDevuR7J3xtWpW41pqEKrpMQfkZEwFopdm", + )], ) } @@ -65,7 +74,9 @@ mod tests { Token::Seq { len: Some(1) }, Token::Tuple { len: 2 }, Token::BorrowedStr("sig-data"), - Token::BorrowedStr("f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + Token::BorrowedStr( + "f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), Token::TupleEnd, Token::SeqEnd, Token::StructEnd, @@ -90,8 +101,8 @@ mod tests { .with_signature_bytes(&[0u8; 64]) .try_build() .unwrap(); - let v = serde_cbor::to_vec(&ms1).unwrap(); - let ms2: Multisig = serde_cbor::from_slice(v.as_slice()).unwrap(); + let v = cbor_to_vec(&ms1); + let ms2: Multisig = ciborium::from_reader(v.as_slice()).unwrap(); assert_eq!(ms1, ms2); } @@ -122,8 +133,9 @@ mod tests { assert_tokens( &ms.readable(), - &[Token::BorrowedStr("zD4bGmynFsniw14r8UfRGjEvoBEGLXGSRh69iptfk43kLUGCLhXMFVmkmLXWoj9AzWGXpG183NV8jXNdsKwY8bJVKDRhWnkUV9w6f") - ], + &[Token::BorrowedStr( + "zD4bGmynFsniw14r8UfRGjEvoBEGLXGSRh69iptfk43kLUGCLhXMFVmkmLXWoj9AzWGXpG183NV8jXNdsKwY8bJVKDRhWnkUV9w6f", + )], ) } @@ -149,7 +161,9 @@ mod tests { Token::Seq { len: Some(1) }, Token::Tuple { len: 2 }, Token::BorrowedStr("sig-data"), - Token::BorrowedStr("f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + Token::BorrowedStr( + "f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), Token::TupleEnd, Token::SeqEnd, Token::StructEnd, @@ -174,8 +188,8 @@ mod tests { .with_signature_bytes(&[0u8; 64]) .try_build() .unwrap(); - let v = serde_cbor::to_vec(&ms1).unwrap(); - let ms2: Multisig = serde_cbor::from_slice(v.as_slice()).unwrap(); + let v = cbor_to_vec(&ms1); + let ms2: Multisig = ciborium::from_reader(v.as_slice()).unwrap(); assert_eq!(ms1, ms2); } @@ -244,7 +258,9 @@ mod tests { Token::Seq { len: Some(1) }, Token::Tuple { len: 2 }, Token::BorrowedStr("sig-data"), - Token::BorrowedStr("f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + Token::BorrowedStr( + "f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), Token::TupleEnd, Token::SeqEnd, Token::StructEnd, @@ -269,8 +285,8 @@ mod tests { .with_signature_bytes(&[0u8; 64]) .try_build() .unwrap(); - let v = serde_cbor::to_vec(&ms1).unwrap(); - let ms2: Multisig = serde_cbor::from_slice(v.as_slice()).unwrap(); + let v = cbor_to_vec(&ms1); + let ms2: Multisig = ciborium::from_reader(v.as_slice()).unwrap(); assert_eq!(ms1, ms2); } @@ -320,9 +336,9 @@ mod tests { assert_tokens( &ms.readable(), - &[ - Token::BorrowedStr("hzr1ejjsyyayykybounzzo85hy3tfkhe19ro6k973bknezbqysqm4u9oax7yfx5t6wnuyz6rnfym7zttnrfajamxdoy91hyobyebonyaryrnykyeb") - ], + &[Token::BorrowedStr( + "hzr1ejjsyyayykybounzzo85hy3tfkhe19ro6k973bknezbqysqm4u9oax7yfx5t6wnuyz6rnfym7zttnrfajamxdoy91hyobyebonyaryrnykyeb", + )], ) } @@ -337,7 +353,10 @@ mod tests { assert_tokens( &ms.readable(), &[ - Token::Struct { name: "multisig", len: 3, }, + Token::Struct { + name: "multisig", + len: 3, + }, Token::BorrowedStr("codec"), Token::BorrowedStr("bls12_381-g1-share-msig"), Token::BorrowedStr("message"), @@ -346,7 +365,9 @@ mod tests { Token::Seq { len: Some(5) }, Token::Tuple { len: 2 }, Token::BorrowedStr("sig-data"), - Token::BorrowedStr("f3098af781f7c0662557112f921e57fb90a848b85c0b397a9fe187f4057ee3ea0a60bf8822817dbc62221709c2de3803f2e"), + Token::BorrowedStr( + "f3098af781f7c0662557112f921e57fb90a848b85c0b397a9fe187f4057ee3ea0a60bf8822817dbc62221709c2de3803f2e", + ), Token::TupleEnd, Token::Tuple { len: 2 }, Token::BorrowedStr("scheme"), @@ -391,8 +412,8 @@ mod tests { .unwrap() .to_inner(); - let v = serde_cbor::to_vec(&ms1).unwrap(); - let ms2: Multisig = serde_cbor::from_slice(v.as_slice()).unwrap(); + let v = cbor_to_vec(&ms1); + let ms2: Multisig = ciborium::from_reader(v.as_slice()).unwrap(); assert_eq!(ms1, ms2); } diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 97e7b6c..752f366 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{ms, AttrId, Multisig}; +use crate::{AttrId, Multisig, ms}; use multi_util::{EncodedVarbytes, EncodingInfo, Varbytes}; use serde::ser::{self, SerializeStruct}; diff --git a/src/views.rs b/src/views.rs index 76c23e6..244141d 100644 --- a/src/views.rs +++ b/src/views.rs @@ -27,9 +27,9 @@ pub mod slh_dsa; /// Threshold disclosure modes and encrypted metadata helpers. pub mod threshold_meta; pub use threshold_meta::{ - decrypt_threshold_meta, disclosure_mode, encrypt_threshold_meta, generate_meta_key, - read_threshold_params, stamp_disclosure_attrs, DisclosureView, ThresholdDisclosure, - ThresholdMetaCipher, ThresholdMetadata, + DisclosureView, MAX_THRESHOLD_PARTICIPANTS, ThresholdDisclosure, ThresholdMetaCipher, + ThresholdMetadata, decrypt_threshold_meta, disclosure_mode, encrypt_threshold_meta, + generate_meta_key, read_threshold_params, stamp_disclosure_attrs, }; /// diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index 66f8a14..5c2bb94 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - error::{AttributesError, ConversionsError, SharesError}, - views::threshold_meta::{self, ThresholdDisclosure}, AttrId, AttrView, Builder, ConvView, DataView, Error, Multisig, ThresholdAttrView, ThresholdView, Views, + error::{AttributesError, ConversionsError, SharesError}, + views::threshold_meta::{self, ThresholdDisclosure}, }; use blsful::{ + Bls12381G1Impl, Bls12381G2Impl, Signature, SignatureSchemes, SignatureShare, inner_types::{G1Projective, G2Projective, Scalar}, vsss_rs::{IdentifierPrimeField, Share, ValueGroup}, - Bls12381G1Impl, Bls12381G2Impl, Signature, SignatureSchemes, SignatureShare, }; use multi_codec::Codec; use multi_trait::{EncodeInto, TryDecodeFrom}; @@ -221,7 +221,7 @@ impl From for Vec { fn from(val: SigShare) -> Self { let mut v = Vec::default(); // add in the share identifier - v.append(&mut val.0 .0.to_be_bytes().into()); + v.append(&mut val.0.0.to_be_bytes().into()); // add in the share threshold v.append(&mut Varuint(val.1).into()); // add in the share limit @@ -256,6 +256,19 @@ impl<'a> TryDecodeFrom<'a> for SigShare { let (threshold, ptr) = Varuint::::try_decode_from(ptr)?; // try to decode the limit let (limit, ptr) = Varuint::::try_decode_from(ptr)?; + // enforce participant caps to bound work from crafted input (CWE-400) + if *threshold > threshold_meta::MAX_THRESHOLD_PARTICIPANTS { + return Err(Error::TooManyParticipants( + *threshold, + threshold_meta::MAX_THRESHOLD_PARTICIPANTS, + )); + } + if *limit > threshold_meta::MAX_THRESHOLD_PARTICIPANTS { + return Err(Error::TooManyParticipants( + *limit, + threshold_meta::MAX_THRESHOLD_PARTICIPANTS, + )); + } // try to decode the share type id let (share_type, ptr) = SchemeTypeId::try_decode_from(ptr)?; // try to decode the share data @@ -545,7 +558,7 @@ impl<'a> ThresholdView for View<'a> { Codec::Bls12381G1Msig => Codec::Bls12381G1ShareMsig, Codec::Bls12381G2Msig => Codec::Bls12381G2ShareMsig, Codec::Bls12381G1ShareMsig | Codec::Bls12381G2ShareMsig => { - return Err(SharesError::IsASignatureShare.into()) + return Err(SharesError::IsASignatureShare.into()); } _ => return Err(Error::UnsupportedAlgorithm(self.ms.codec.to_string())), }; @@ -576,7 +589,7 @@ impl<'a> ThresholdView for View<'a> { // and the payload encoding value let share = Builder::new(codec) .with_message_bytes(&self.ms.message.as_slice()) - .with_identifier(&share.0 .0.to_be_bytes()) + .with_identifier(&share.0.0.to_be_bytes()) .with_threshold(share.1) .with_limit(share.2) .with_signature_bytes(&share.4) @@ -596,7 +609,7 @@ impl<'a> ThresholdView for View<'a> { match self.ms.codec { Codec::Bls12381G1Msig | Codec::Bls12381G2Msig => {} Codec::Bls12381G1ShareMsig | Codec::Bls12381G2ShareMsig => { - return Err(SharesError::IsASignatureShare.into()) + return Err(SharesError::IsASignatureShare.into()); } _ => return Err(Error::UnsupportedAlgorithm(self.ms.codec.to_string())), }; @@ -749,7 +762,7 @@ impl<'a> ThresholdView for View<'a> { let av = self.ms.attr_view()?; av.payload_encoding()? }; - Builder::new_from_bls_signature(&sig)? + Builder::new_from_bls_signature_with_codec(self.ms.codec, &sig)? .with_message_bytes(&self.ms.message.as_slice()) .with_payload_encoding(encoding) .try_build() @@ -795,7 +808,7 @@ impl<'a> ThresholdView for View<'a> { let av = self.ms.attr_view()?; av.payload_encoding()? }; - Builder::new_from_bls_signature(&sig)? + Builder::new_from_bls_signature_with_codec(self.ms.codec, &sig)? .with_message_bytes(&self.ms.message.as_slice()) .with_payload_encoding(encoding) .try_build() @@ -949,7 +962,7 @@ impl<'a> ThresholdView for View<'a> { let av = self.ms.attr_view()?; av.payload_encoding()? }; - Builder::new_from_bls_signature(&sig)? + Builder::new_from_bls_signature_with_codec(self.ms.codec, &sig)? .with_message_bytes(&self.ms.message.as_slice()) .with_payload_encoding(encoding) .try_build() @@ -994,7 +1007,7 @@ impl<'a> ThresholdView for View<'a> { let av = self.ms.attr_view()?; av.payload_encoding()? }; - Builder::new_from_bls_signature(&sig)? + Builder::new_from_bls_signature_with_codec(self.ms.codec, &sig)? .with_message_bytes(&self.ms.message.as_slice()) .with_payload_encoding(encoding) .try_build() diff --git a/src/views/ed25519.rs b/src/views/ed25519.rs index d4d37f7..0d7895e 100644 --- a/src/views/ed25519.rs +++ b/src/views/ed25519.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - error::{AttributesError, ConversionsError}, AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, + error::{AttributesError, ConversionsError}, }; use multi_codec::Codec; diff --git a/src/views/ed25519_hybrid.rs b/src/views/ed25519_hybrid.rs index 7e9e340..7a359a7 100644 --- a/src/views/ed25519_hybrid.rs +++ b/src/views/ed25519_hybrid.rs @@ -2,7 +2,7 @@ //! Generic Ed25519 hybrid multisig view (codec-agnostic signature holder). //! Used by all Ed25519-based Birds-of-Prey hybrid signature codecs. -use crate::{error::AttributesError, AttrId, AttrView, ConvView, DataView, Error, Multisig}; +use crate::{AttrId, AttrView, ConvView, DataView, Error, Multisig, error::AttributesError}; use multi_codec::Codec; pub(crate) struct View<'a> { diff --git a/src/views/ed25519_mayo2.rs b/src/views/ed25519_mayo2.rs index b8d4cca..67e8230 100644 --- a/src/views/ed25519_mayo2.rs +++ b/src/views/ed25519_mayo2.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 //! Ed25519-MAYO2 hybrid multisig view. -use crate::{error::AttributesError, AttrId, AttrView, ConvView, DataView, Error, Multisig}; +use crate::{AttrId, AttrView, ConvView, DataView, Error, Multisig, error::AttributesError}; use multi_codec::Codec; pub(crate) struct View<'a> { diff --git a/src/views/fn_dsa.rs b/src/views/fn_dsa.rs index c013347..119f0f1 100644 --- a/src/views/fn_dsa.rs +++ b/src/views/fn_dsa.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 //! FN-DSA 512/1024 multisig view; FIPS 206 (draft). -use crate::{error::AttributesError, AttrId, AttrView, ConvView, DataView, Error, Multisig}; +use crate::{AttrId, AttrView, ConvView, DataView, Error, Multisig, error::AttributesError}; use multi_codec::Codec; pub(crate) struct View<'a> { diff --git a/src/views/mayo.rs b/src/views/mayo.rs index 74c1558..0d66cee 100644 --- a/src/views/mayo.rs +++ b/src/views/mayo.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 //! MAYO-1/MAYO-2 multisig view; post-quantum multivariate signature. -use crate::{error::AttributesError, AttrId, AttrView, ConvView, DataView, Error, Multisig}; +use crate::{AttrId, AttrView, ConvView, DataView, Error, Multisig, error::AttributesError}; use multi_codec::Codec; pub(crate) struct View<'a> { diff --git a/src/views/ml_dsa.rs b/src/views/ml_dsa.rs index 6a6cfed..7e96d3a 100644 --- a/src/views/ml_dsa.rs +++ b/src/views/ml_dsa.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 //! ML-DSA 44/65/87 multisig view; FIPS 204. -use crate::{error::AttributesError, AttrId, AttrView, ConvView, DataView, Error, Multisig}; +use crate::{AttrId, AttrView, ConvView, DataView, Error, Multisig, error::AttributesError}; use multi_codec::Codec; pub(crate) struct View<'a> { diff --git a/src/views/nist_p.rs b/src/views/nist_p.rs index dfa146e..1484e09 100644 --- a/src/views/nist_p.rs +++ b/src/views/nist_p.rs @@ -2,8 +2,8 @@ //! NIST P-256/P-384/P-521 ECDSA multisig view. use crate::{ - error::{AttributesError, ConversionsError}, AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, + error::{AttributesError, ConversionsError}, }; use multi_codec::Codec; diff --git a/src/views/rsa.rs b/src/views/rsa.rs index 4ccda1c..d0f6ee2 100644 --- a/src/views/rsa.rs +++ b/src/views/rsa.rs @@ -2,8 +2,8 @@ //! RSA-SHA256 multisig view. use crate::{ - error::{AttributesError, ConversionsError}, AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, + error::{AttributesError, ConversionsError}, }; use multi_codec::Codec; diff --git a/src/views/secp256k1.rs b/src/views/secp256k1.rs index 2c9a3c6..9c1f2de 100644 --- a/src/views/secp256k1.rs +++ b/src/views/secp256k1.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - error::{AttributesError, ConversionsError}, AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, + error::{AttributesError, ConversionsError}, }; use multi_codec::Codec; diff --git a/src/views/slh_dsa.rs b/src/views/slh_dsa.rs index 2417489..604e517 100644 --- a/src/views/slh_dsa.rs +++ b/src/views/slh_dsa.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 //! SLH-DSA multisig view; FIPS 205. Supports all 12 parameter sets (Sha2_128f/s through Shake256f/s). -use crate::{error::AttributesError, AttrId, AttrView, ConvView, DataView, Error, Multisig}; +use crate::{AttrId, AttrView, ConvView, DataView, Error, Multisig, error::AttributesError}; use multi_codec::Codec; pub(crate) struct View<'a> { diff --git a/src/views/threshold_meta.rs b/src/views/threshold_meta.rs index d220f59..8ce6b12 100644 --- a/src/views/threshold_meta.rs +++ b/src/views/threshold_meta.rs @@ -18,8 +18,8 @@ use crate::{AttrId, Error, Multisig}; use chacha20poly1305::{ - aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce, + aead::{Aead, KeyInit, Payload}, }; use multi_codec::Codec; use multi_trait::{EncodeInto, TryDecodeFrom}; @@ -27,6 +27,12 @@ use multi_util::Varuint; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; +/// Maximum number of threshold participants (t or n) accepted when decoding +/// from untrusted wire data. The 1024 ceiling comfortably exceeds every +/// legitimate threshold signature configuration while bounding the work a +/// crafted input can force the decoder to perform (mitigates CWE-400). +pub const MAX_THRESHOLD_PARTICIPANTS: usize = 1024; + /// Disclosure mode for threshold parameters (t and n). #[repr(u8)] #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs index 191097a..e61ded1 100644 --- a/tests/edge_case_tests.rs +++ b/tests/edge_case_tests.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 //! Edge case tests for multi-sig +#![allow(clippy::explicit_iter_loop)] use multi_codec::Codec; use multi_sig::{Builder, Multisig, SIG_CODECS};