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
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "multi-sig"
version = "1.0.4"
version = "1.0.5"
edition = "2021"
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]
description = "Multisig self-describing multicodec implementation for digital signatures"
Expand All @@ -21,7 +21,6 @@ getrandom = { version = "0.2" }
# blsful configured per-target below (blst for native, rust for wasm)
multi-base = "1.0"
multi-codec = "1.0"
multi-key = "1.0"
multi-trait = "1.0"
multi-util = "1.0"
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true }
Expand Down
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ pub enum Error {
/// Duplicate attribute error
#[error("Duplicate Multikey attribute: {0}")]
DuplicateAttribute(u8),
/// Attribute count exceeds the configured maximum
///
/// Returned by [`crate::ms::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),
/// Failed Varsig conversion
#[error("Failed Varsig conversion: {0}")]
FailedConversion(String),
Expand Down Expand Up @@ -200,6 +208,7 @@ impl Error {
Self::Vsss(_) => "Vsss",
Self::MissingSigil => "MissingSigil",
Self::DuplicateAttribute(_) => "DuplicateAttribute",
Self::TooManyAttributes(_, _) => "TooManyAttributes",
Self::FailedConversion(_) => "FailedConversion",
Self::UnsupportedAlgorithm(_) => "UnsupportedAlgorithm",
}
Expand Down
7 changes: 4 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub use attrid::AttrId;

/// Multisig implementation
pub mod ms;
pub use ms::{Builder, EncodedMultisig, Multisig, SIG_CODECS, SIG_SHARE_CODECS};
pub use ms::{Builder, EncodedMultisig, Multisig, MAX_ATTRIBUTES, SIG_CODECS, SIG_SHARE_CODECS};

/// Type-safe wrappers for signature components
pub mod types;
Expand All @@ -86,8 +86,9 @@ pub use types::{SignatureBytes, SignatureScheme};
/// Views on the multisig
pub mod views;
pub use views::{
AttrView, ConvView, DataView, ThresholdAttrView, ThresholdDisclosureView, ThresholdView,
Views,
decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, AttrView, ConvView,
DataView, ThresholdAttrView, ThresholdDisclosure, ThresholdDisclosureView, ThresholdMetaCipher,
ThresholdMetadata, ThresholdView, Views,
};

/// Serde serialization
Expand Down
44 changes: 39 additions & 5 deletions src/ms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
views::{
bls12381::{self, SchemeTypeId},
ed25519, ed25519_hybrid, ed25519_mayo2, fn_dsa, mayo, ml_dsa, nist_p, rsa, secp256k1,
slh_dsa, threshold_meta, DisclosureView, ThresholdDisclosureView,
slh_dsa, threshold_meta, DisclosureView, ThresholdDisclosure, ThresholdDisclosureView,
},
AttrId, AttrView, ConvView, DataView, Error, ThresholdAttrView, ThresholdView, Views,
};
Expand Down Expand Up @@ -68,6 +68,14 @@ pub const SIG_SHARE_CODECS: [Codec; 2] = [
/// the multisig sigil
pub const SIGIL: Codec = Codec::Multisig;

/// Maximum number of attributes a single decoded [`Multisig`] will accept.
///
/// Every legitimate multisig carries at most a handful of attributes (signature
/// data, threshold metadata, payload encoding, …). The 256 ceiling comfortably
/// covers every codec this crate emits while bounding the work a crafted input
/// can force the decoder to perform (mitigates CWE-400).
pub const MAX_ATTRIBUTES: usize = 256;

/// a base encoded varsig
pub type EncodedMultisig = BaseEncoded<Multisig>;

Expand Down Expand Up @@ -153,6 +161,11 @@ impl<'a> TryDecodeFrom<'a> for Multisig {
let message = message.to_inner();
// decode the number of signature-specific attributes
let (num_attr, ptr) = Varuint::<usize>::try_decode_from(ptr)?;
// reject attribute counts that exceed the configured maximum to bound
// the work a crafted input can force the decoder to perform (CWE-400)
if *num_attr > MAX_ATTRIBUTES {
return Err(Error::TooManyAttributes(*num_attr, MAX_ATTRIBUTES));
}
// decode the signature-specific attributes
let (attributes, ptr) = match *num_attr {
0 => (Attributes::default(), ptr),
Expand Down Expand Up @@ -460,6 +473,16 @@ impl Builder {
{
let scheme_type_id = SchemeTypeId::from(sig);
let sig_bytes: Vec<u8> = 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()
Expand Down Expand Up @@ -492,6 +515,14 @@ 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
Expand Down Expand Up @@ -575,8 +606,8 @@ impl Builder {
/// `meta_key` is required.
pub fn with_disclosure(
self,
mode: multi_key::ThresholdDisclosure,
meta_key: Option<&multi_key::Multikey>,
mode: ThresholdDisclosure,
meta_key: Option<&[u8]>,
threshold: usize,
limit: usize,
) -> Self {
Expand Down Expand Up @@ -713,6 +744,7 @@ mod tests {

let ms1 = Builder::new_from_bls_signature(&sig)
.unwrap()
.with_payload_encoding(Codec::Raw)
.try_build()
.unwrap();

Expand All @@ -729,13 +761,14 @@ mod tests {
sigs.push(
Builder::new_from_bls_signature_share(3, 4, &sig)
.unwrap()
.with_payload_encoding(Codec::Raw)
.try_build()
.unwrap(),
);
});

// build a new signature from the parts
let mut builder = Builder::new(Codec::Bls12381G2Msig);
let mut builder = Builder::new(Codec::Bls12381G2Msig).with_payload_encoding(Codec::Raw);
for sig in &sigs {
builder = builder.add_signature_share(sig);
}
Expand Down Expand Up @@ -819,6 +852,7 @@ mod tests {

let ms1 = Builder::new_from_bls_signature(&sig)
.unwrap()
.with_payload_encoding(Codec::Raw)
.try_build()
.unwrap();

Expand All @@ -843,7 +877,7 @@ mod tests {
});

// build a new signature from the parts
let mut builder = Builder::new(Codec::Bls12381G2Msig);
let mut builder = Builder::new(Codec::Bls12381G2Msig).with_payload_encoding(Codec::Raw);
for sig in &sigs {
let ms = Builder::new_from_ssh_signature(sig)
.unwrap()
Expand Down
28 changes: 14 additions & 14 deletions src/views.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{Error, Multisig};
use multi_codec::Codec;
use multi_key::ThresholdDisclosure;

/// BLS12 381 G1/G2 signature implementation
pub mod bls12381;
Expand All @@ -27,7 +26,11 @@ pub mod secp256k1;
pub mod slh_dsa;
/// Threshold disclosure modes and encrypted metadata helpers.
pub mod threshold_meta;
pub use threshold_meta::{DisclosureView, disclosure_mode, read_threshold_params, stamp_disclosure_attrs};
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,
};

///
/// Attributes views let you inquire about the Multisig and retrieve data
Expand Down Expand Up @@ -73,40 +76,37 @@ pub trait ThresholdView {
fn shares_with_disclosure(
&self,
mode: ThresholdDisclosure,
meta_key: Option<&multi_key::Multikey>,
meta_key: Option<&[u8]>,
) -> Result<Vec<Multisig>, Error>;
/// add a new share and return the Multisig with the share added
fn add_share(&self, share: &Multisig) -> Result<Multisig, Error>;
/// add a share with a meta_key for decrypting threshold params
fn add_share_with_meta(
&self,
share: &Multisig,
meta_key: Option<&multi_key::Multikey>,
meta_key: Option<&[u8]>,
) -> Result<Multisig, Error>;
/// reconstruct the signature from the shares
fn combine(&self) -> Result<Multisig, Error>;
/// combine with a meta_key for decrypting threshold params
fn combine_with_meta(
&self,
meta_key: Option<&multi_key::Multikey>,
) -> Result<Multisig, Error>;
fn combine_with_meta(&self, meta_key: Option<&[u8]>) -> Result<Multisig, Error>;
}

/// trait for threshold disclosure mode operations on a Multisig
pub trait ThresholdDisclosureView {
/// Get the current disclosure mode. Returns Full if no mode attribute is present.
fn disclosure_mode(&self) -> Result<ThresholdDisclosure, Error>;
/// Read t and n, decrypting if necessary. Requires `meta_key` for encrypted modes.
fn read_threshold_params(
&self,
meta_key: Option<&multi_key::Multikey>,
) -> Result<(usize, usize), Error>;
///
/// `meta_key` is the raw 32-byte symmetric key (callers extract it from a
/// `multi_key::Multikey` before calling).
fn read_threshold_params(&self, meta_key: Option<&[u8]>) -> Result<(usize, usize), Error>;
/// Convert to a target disclosure mode.
fn to_disclosure(
&self,
target: ThresholdDisclosure,
meta_key: Option<&multi_key::Multikey>,
current_meta_key: Option<&multi_key::Multikey>,
meta_key: Option<&[u8]>,
current_meta_key: Option<&[u8]>,
) -> Result<Multisig, Error>;
}

Expand Down
54 changes: 25 additions & 29 deletions src/views/bls12381.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
error::{AttributesError, ConversionsError, SharesError},
views::threshold_meta,
views::threshold_meta::{self, ThresholdDisclosure},
AttrId, AttrView, Builder, ConvView, DataView, Error, Multisig, ThresholdAttrView,
ThresholdView, Views,
};
use multi_key::ThresholdDisclosure;
use blsful::{
inner_types::{G1Projective, G2Projective, Scalar},
vsss_rs::{IdentifierPrimeField, Share, ValueGroup},
Expand Down Expand Up @@ -313,7 +312,9 @@ impl<'a> TryDecodeFrom<'a> for ThresholdData {
let mut p = ptr;
for _ in 0..*num_shares {
let (share, ptr) = SigShare::try_decode_from(p)?;
shares.insert(share.0, share);
if shares.insert(share.0, share).is_some() {
return Err(SharesError::DuplicateShare.into());
}
p = ptr;
}
(shares, p)
Expand Down Expand Up @@ -553,7 +554,8 @@ impl<'a> ThresholdView for View<'a> {
let threshold_data = {
let av = self.ms.threshold_attr_view()?;
match av.threshold_data() {
Ok(b) => ThresholdData::try_from(b).unwrap_or_default(),
Ok(b) => ThresholdData::try_from(b)
.map_err(|e| SharesError::InvalidThresholdData(e.to_string()))?,
Err(_) => ThresholdData::default(),
}
};
Expand Down Expand Up @@ -644,9 +646,14 @@ impl<'a> ThresholdView for View<'a> {
let threshold_data: Vec<u8> = {
let av = self.ms.threshold_attr_view()?;
let mut tdata = match av.threshold_data() {
Ok(b) => ThresholdData::try_from(b).unwrap_or_default(),
Ok(b) => ThresholdData::try_from(b)
.map_err(|e| SharesError::InvalidThresholdData(e.to_string()))?,
Err(_) => ThresholdData::default(),
};
// detect a duplicate share identifier and refuse to silently overwrite it
if tdata.0.contains_key(&identifier) {
return Err(SharesError::DuplicateShare.into());
}
// insert the share data into the list of shares
tdata.0.insert(identifier, sdata);
tdata.into()
Expand Down Expand Up @@ -687,7 +694,8 @@ impl<'a> ThresholdView for View<'a> {
let threshold_data = {
let av = self.ms.threshold_attr_view()?;
match av.threshold_data() {
Ok(b) => ThresholdData::try_from(b).unwrap_or_default(),
Ok(b) => ThresholdData::try_from(b)
.map_err(|e| SharesError::InvalidThresholdData(e.to_string()))?,
Err(_) => ThresholdData::default(),
}
};
Expand Down Expand Up @@ -785,16 +793,12 @@ impl<'a> ThresholdView for View<'a> {
.map_err(|e| SharesError::ShareCombineFailed(e.to_string()))?;
let encoding = {
let av = self.ms.attr_view()?;
av.payload_encoding().ok()
av.payload_encoding()?
};
let builder = Builder::new_from_bls_signature(&sig)?
.with_message_bytes(&self.ms.message.as_slice());

if let Some(encoding) = encoding {
builder.with_payload_encoding(encoding).try_build()
} else {
builder.try_build()
}
Builder::new_from_bls_signature(&sig)?
.with_message_bytes(&self.ms.message.as_slice())
.with_payload_encoding(encoding)
.try_build()
}
_ => Err(Error::UnsupportedAlgorithm(self.ms.codec.to_string())),
}
Expand All @@ -804,26 +808,22 @@ impl<'a> ThresholdView for View<'a> {
fn shares_with_disclosure(
&self,
mode: ThresholdDisclosure,
meta_key: Option<&multi_key::Multikey>,
meta_key: Option<&[u8]>,
) -> Result<Vec<Multisig>, Error> {
let shares = self.shares()?;
shares
.iter()
.map(|s| {
s.disclosure_view()?
.to_disclosure(mode, meta_key, None)
})
.map(|s| s.disclosure_view()?.to_disclosure(mode, meta_key, None))
.collect()
}

/// Add a share with a meta_key for decrypting threshold params.
fn add_share_with_meta(
&self,
share: &Multisig,
meta_key: Option<&multi_key::Multikey>,
meta_key: Option<&[u8]>,
) -> Result<Multisig, Error> {
let (share_t, share_n) =
threshold_meta::read_threshold_params(share, meta_key)?;
let (share_t, share_n) = threshold_meta::read_threshold_params(share, meta_key)?;

let (sdata, identifier, encoding) = {
let av = share.attr_view()?;
Expand Down Expand Up @@ -891,12 +891,8 @@ impl<'a> ThresholdView for View<'a> {
}

/// Combine with a meta_key for decrypting threshold params.
fn combine_with_meta(
&self,
meta_key: Option<&multi_key::Multikey>,
) -> Result<Multisig, Error> {
let (threshold, _limit) =
threshold_meta::read_threshold_params(self.ms, meta_key)?;
fn combine_with_meta(&self, meta_key: Option<&[u8]>) -> Result<Multisig, Error> {
let (threshold, _limit) = threshold_meta::read_threshold_params(self.ms, meta_key)?;

let threshold_data = {
let av = self.ms.threshold_attr_view()?;
Expand Down
Loading
Loading