From c02abd51131fc6f4fb0a6c57e76bf5f208649fa3 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Tue, 14 Jul 2026 21:20:11 -0600 Subject: [PATCH] fix dep and sec issues Signed-off-by: Dave Grantham --- Cargo.toml | 3 +- src/error.rs | 9 + src/lib.rs | 7 +- src/ms.rs | 44 +++- src/views.rs | 28 +-- src/views/bls12381.rs | 54 ++--- src/views/threshold_meta.rs | 456 +++++++++++++++++++++++++++++------- 7 files changed, 468 insertions(+), 133 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30f75f3..790a8e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-sig" -version = "1.0.4" +version = "1.0.5" edition = "2021" authors = ["Dave Grantham "] description = "Multisig self-describing multicodec implementation for digital signatures" @@ -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 } diff --git a/src/error.rs b/src/error.rs index a4dd854..32bac21 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), @@ -200,6 +208,7 @@ impl Error { Self::Vsss(_) => "Vsss", Self::MissingSigil => "MissingSigil", Self::DuplicateAttribute(_) => "DuplicateAttribute", + Self::TooManyAttributes(_, _) => "TooManyAttributes", Self::FailedConversion(_) => "FailedConversion", Self::UnsupportedAlgorithm(_) => "UnsupportedAlgorithm", } diff --git a/src/lib.rs b/src/lib.rs index 303e821..bc04bd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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 diff --git a/src/ms.rs b/src/ms.rs index 5a41123..e61dc71 100644 --- a/src/ms.rs +++ b/src/ms.rs @@ -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, }; @@ -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; @@ -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::::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), @@ -460,6 +473,16 @@ impl Builder { { 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() @@ -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 @@ -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 { @@ -713,6 +744,7 @@ mod tests { let ms1 = Builder::new_from_bls_signature(&sig) .unwrap() + .with_payload_encoding(Codec::Raw) .try_build() .unwrap(); @@ -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); } @@ -819,6 +852,7 @@ mod tests { let ms1 = Builder::new_from_bls_signature(&sig) .unwrap() + .with_payload_encoding(Codec::Raw) .try_build() .unwrap(); @@ -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() diff --git a/src/views.rs b/src/views.rs index 9c81b46..76c23e6 100644 --- a/src/views.rs +++ b/src/views.rs @@ -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; @@ -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 @@ -73,7 +76,7 @@ pub trait ThresholdView { fn shares_with_disclosure( &self, mode: ThresholdDisclosure, - meta_key: Option<&multi_key::Multikey>, + meta_key: Option<&[u8]>, ) -> Result, Error>; /// add a new share and return the Multisig with the share added fn add_share(&self, share: &Multisig) -> Result; @@ -81,15 +84,12 @@ pub trait ThresholdView { fn add_share_with_meta( &self, share: &Multisig, - meta_key: Option<&multi_key::Multikey>, + meta_key: Option<&[u8]>, ) -> Result; /// reconstruct the signature from the shares fn combine(&self) -> Result; /// combine with a meta_key for decrypting threshold params - fn combine_with_meta( - &self, - meta_key: Option<&multi_key::Multikey>, - ) -> Result; + fn combine_with_meta(&self, meta_key: Option<&[u8]>) -> Result; } /// trait for threshold disclosure mode operations on a Multisig @@ -97,16 +97,16 @@ pub trait ThresholdDisclosureView { /// Get the current disclosure mode. Returns Full if no mode attribute is present. fn disclosure_mode(&self) -> Result; /// 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; } diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index 393ea34..66f8a14 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -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}, @@ -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) @@ -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(), } }; @@ -644,9 +646,14 @@ impl<'a> ThresholdView for View<'a> { let threshold_data: Vec = { 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() @@ -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(), } }; @@ -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())), } @@ -804,15 +808,12 @@ impl<'a> ThresholdView for View<'a> { fn shares_with_disclosure( &self, mode: ThresholdDisclosure, - meta_key: Option<&multi_key::Multikey>, + meta_key: Option<&[u8]>, ) -> Result, 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() } @@ -820,10 +821,9 @@ impl<'a> ThresholdView for View<'a> { fn add_share_with_meta( &self, share: &Multisig, - meta_key: Option<&multi_key::Multikey>, + meta_key: Option<&[u8]>, ) -> Result { - 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()?; @@ -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 { - let (threshold, _limit) = - threshold_meta::read_threshold_params(self.ms, meta_key)?; + fn combine_with_meta(&self, meta_key: Option<&[u8]>) -> Result { + let (threshold, _limit) = threshold_meta::read_threshold_params(self.ms, meta_key)?; let threshold_data = { let av = self.ms.threshold_attr_view()?; diff --git a/src/views/threshold_meta.rs b/src/views/threshold_meta.rs index b9a965d..d597ca5 100644 --- a/src/views/threshold_meta.rs +++ b/src/views/threshold_meta.rs @@ -1,22 +1,325 @@ // SPDX-License-Identifier: Apache-2.0 -//! Threshold disclosure helpers for Multisig. +//! Threshold disclosure modes and encrypted metadata helpers. //! -//! Re-exports the disclosure types from `multi_key` and provides -//! Multisig-specific helpers for reading/converting disclosure modes. +//! This module owns the pure crypto types and functions for configurable +//! confidentiality of threshold `t` and share-count `n` values on signature +//! shares. It deliberately depends only on the lower multiformats crates +//! (multi-codec, multi-trait, multi-util) plus `chacha20poly1305`/`getrandom`, +//! so that `multi-sig` does **not** need to depend on `multi-key`. Callers that +//! hold a `multi_key::Multikey` wrapping a symmetric key must extract the raw +//! 32-byte key (e.g. via `Multikey::data_view()?.key_bytes()`) before passing +//! it as the `meta_key: Option<&[u8]>` argument used here. +//! +//! Three disclosure modes are supported: +//! +//! - **[`ThresholdDisclosure::Full`]** — t and n are plaintext attributes (default). +//! - **[`ThresholdDisclosure::Partial`]** — n is plaintext, t is encrypted. +//! - **[`ThresholdDisclosure::FullConfidentialial`]** — both t and n are encrypted. use crate::{AttrId, Error, Multisig}; -use multi_key::{self, ThresholdDisclosure, ThresholdMetaCipher, ThresholdMetadata, - decrypt_threshold_meta, encrypt_threshold_meta, Views as MultikeyViews}; +use chacha20poly1305::{ + aead::{Aead, KeyInit, Payload}, + ChaCha20Poly1305, Nonce, +}; +use multi_codec::Codec; use multi_trait::{EncodeInto, TryDecodeFrom}; use multi_util::Varuint; +use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; +/// Disclosure mode for threshold parameters (t and n). +#[repr(u8)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[non_exhaustive] +pub enum ThresholdDisclosure { + /// t and n are plaintext attributes (default, backward-compatible). + #[default] + Full = 0, + /// n is plaintext, t is encrypted (auditable n, hidden t). + Partial = 1, + /// Both t and n are encrypted. + FullConfidentialial = 2, +} + +impl ThresholdDisclosure { + /// Get the human-readable name for this mode. + #[must_use] + pub fn as_str(&self) -> &'static str { + match self { + Self::Full => "full", + Self::Partial => "partial", + Self::FullConfidentialial => "full-confidentialial", + } + } +} + +impl core::fmt::Display for ThresholdDisclosure { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl From for u8 { + fn from(val: ThresholdDisclosure) -> Self { + val as u8 + } +} + +impl TryFrom for ThresholdDisclosure { + type Error = Error; + + fn try_from(code: u8) -> Result { + match code { + 0 => Ok(Self::Full), + 1 => Ok(Self::Partial), + 2 => Ok(Self::FullConfidentialial), + _ => Err(Error::Shares(crate::error::SharesError::MetaEncryption( + format!("invalid disclosure mode: {code}"), + ))), + } + } +} + +impl EncodeInto for ThresholdDisclosure { + fn encode_into(&self) -> Vec { + let v: u8 = (*self).into(); + v.encode_into() + } +} + +impl<'a> TryDecodeFrom<'a> for ThresholdDisclosure { + type Error = Error; + + fn try_decode_from(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> { + let (code, ptr) = u8::try_decode_from(bytes).map_err(Error::Multitrait)?; + let mode = Self::try_from(code)?; + Ok((mode, ptr)) + } +} + +impl Serialize for ThresholdDisclosure { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8((*self).into()) + } +} + +impl<'de> Deserialize<'de> for ThresholdDisclosure { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let code = u8::deserialize(deserializer)?; + Self::try_from(code).map_err(serde::de::Error::custom) + } +} + +/// The threshold parameters that may be encrypted. +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct ThresholdMetadata { + /// The threshold value `t`, or `None` if not stored in the encrypted blob. + pub threshold: Option, + /// The limit value `n`, or `None` if not stored in the encrypted blob. + pub limit: Option, +} + +impl ThresholdMetadata { + /// Create metadata with both t and n. + #[must_use] + pub fn new(threshold: u16, limit: u16) -> Self { + Self { + threshold: Some(threshold), + limit: Some(limit), + } + } + + /// Create metadata with only the threshold (for Partial mode). + #[must_use] + pub fn threshold_only(threshold: u16) -> Self { + Self { + threshold: Some(threshold), + limit: None, + } + } + + /// Encode to CBOR bytes. + pub fn to_cbor_bytes(&self) -> Result, Error> { + let mut buf = Vec::new(); + ciborium::into_writer(self, &mut buf).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "CBOR encode: {e}" + ))) + })?; + Ok(buf) + } + + /// Decode from CBOR bytes. + pub fn from_cbor_bytes(bytes: &[u8]) -> Result { + ciborium::from_reader(bytes).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "CBOR decode: {e}" + ))) + }) + } +} + +/// Cipher parameters for decrypting [`ThresholdMetadata`]. +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct ThresholdMetaCipher { + /// The multicodec code of the AEAD cipher (e.g. `0x2000` for ChaCha20-Poly1305). + pub cipher_codec: u64, + /// The nonce bytes. + pub nonce: Vec, +} + +impl ThresholdMetaCipher { + /// Create new cipher info from a codec and nonce. + #[must_use] + pub fn new(codec: Codec, nonce: Vec) -> Self { + Self { + cipher_codec: codec.into(), + nonce, + } + } + + /// Get the codec as a [`Codec`]. + pub fn codec(&self) -> Result { + Codec::try_from(self.cipher_codec).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "invalid codec: {e}" + ))) + }) + } + + /// Encode to CBOR bytes. + pub fn to_cbor_bytes(&self) -> Result, Error> { + let mut buf = Vec::new(); + ciborium::into_writer(self, &mut buf).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "CBOR encode cipher: {e}" + ))) + })?; + Ok(buf) + } + + /// Decode from CBOR bytes. + pub fn from_cbor_bytes(bytes: &[u8]) -> Result { + ciborium::from_reader(bytes).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "CBOR decode cipher: {e}" + ))) + }) + } +} + +/// Encrypt threshold metadata using ChaCha20-Poly1305 AEAD. +/// +/// `key` must be 32 bytes. A random nonce is generated and returned in the +/// [`ThresholdMetaCipher`]. +#[allow(deprecated)] +pub fn encrypt_threshold_meta( + meta: &ThresholdMetadata, + key: &[u8], +) -> Result<(Vec, ThresholdMetaCipher), Error> { + if key.len() != 32 { + return Err(Error::Shares(crate::error::SharesError::MetaEncryption( + format!("invalid key length: expected 32, got {}", key.len()), + ))); + } + + let plaintext = meta.to_cbor_bytes()?; + + let mut nonce_bytes = vec![0u8; 12]; + getrandom::getrandom(&mut nonce_bytes).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "RNG failure: {e}" + ))) + })?; + + let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "AEAD key init: {e}" + ))) + })?; + + let ciphertext = cipher + .encrypt( + Nonce::from_slice(&nonce_bytes), + Payload { + msg: &plaintext, + aad: b"threshold-meta", + }, + ) + .map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "AEAD seal: {e}" + ))) + })?; + + let cipher_info = ThresholdMetaCipher::new(Codec::Chacha20Poly1305, nonce_bytes); + Ok((ciphertext, cipher_info)) +} + +/// Decrypt threshold metadata using ChaCha20-Poly1305 AEAD. +#[allow(deprecated)] +pub fn decrypt_threshold_meta( + encrypted: &[u8], + cipher_info: &ThresholdMetaCipher, + key: &[u8], +) -> Result { + if key.len() != 32 { + return Err(Error::Shares(crate::error::SharesError::MetaEncryption( + format!("invalid key length: expected 32, got {}", key.len()), + ))); + } + + let codec = cipher_info.codec()?; + match codec { + Codec::Chacha20Poly1305 => { + let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "AEAD key init: {e}" + ))) + })?; + + let plaintext = cipher + .decrypt( + Nonce::from_slice(&cipher_info.nonce), + Payload { + msg: encrypted, + aad: b"threshold-meta", + }, + ) + .map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(format!( + "AEAD open: {e}" + ))) + })?; + + ThresholdMetadata::from_cbor_bytes(&plaintext) + } + _ => Err(Error::Shares(crate::error::SharesError::MetaEncryption( + format!("unsupported AEAD codec: {codec}"), + ))), + } +} + +/// Generate a random 32-byte ChaCha20-Poly1305 key. +pub fn generate_meta_key() -> Zeroizing> { + let mut key = Zeroizing::new(vec![0u8; 32]); + getrandom::getrandom(key.as_mut_slice()).expect("getrandom failure during meta key generation"); + key +} + /// Read the disclosure mode from a Multisig. Returns Full if no attribute is present. pub fn disclosure_mode(ms: &Multisig) -> Result { match ms.attributes.get(&AttrId::ThresholdDisclosure) { Some(v) => { - let (mode, _) = ThresholdDisclosure::try_decode_from(v.as_slice()) - .map_err(|e| Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())))?; + let (mode, _) = ThresholdDisclosure::try_decode_from(v.as_slice()).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())) + })?; Ok(mode) } None => Ok(ThresholdDisclosure::Full), @@ -24,9 +327,12 @@ pub fn disclosure_mode(ms: &Multisig) -> Result { } /// Read t and n from a Multisig, decrypting if necessary. +/// +/// `meta_key` is the raw 32-byte symmetric key (extracted from a `Multikey` by +/// the caller); it is required for Partial/FullConfidentialial modes. pub fn read_threshold_params( ms: &Multisig, - meta_key: Option<&multi_key::Multikey>, + meta_key: Option<&[u8]>, ) -> Result<(usize, usize), Error> { let mode = disclosure_mode(ms)?; match mode { @@ -56,87 +362,77 @@ pub fn read_threshold_params( .map_err(Error::Multiutil)? .to_inner(); - let encrypted = ms - .attributes - .get(&AttrId::EncryptedThresholdMeta) - .ok_or(crate::error::SharesError::MetaEncryption( + let encrypted = ms.attributes.get(&AttrId::EncryptedThresholdMeta).ok_or( + crate::error::SharesError::MetaEncryption( "missing EncryptedThresholdMeta".to_string(), - ))?; - let cipher_info_bytes = ms - .attributes - .get(&AttrId::ThresholdMetaCipher) - .ok_or(crate::error::SharesError::MetaEncryption( + ), + )?; + let cipher_info_bytes = ms.attributes.get(&AttrId::ThresholdMetaCipher).ok_or( + crate::error::SharesError::MetaEncryption( "missing ThresholdMetaCipher".to_string(), - ))?; - let cipher_info = ThresholdMetaCipher::from_cbor_bytes(cipher_info_bytes) - .map_err(|e| Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())))?; + ), + )?; + let cipher_info = + ThresholdMetaCipher::from_cbor_bytes(cipher_info_bytes).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())) + })?; let meta_key = meta_key.ok_or(crate::error::SharesError::MissingMetaKey)?; - let key = extract_meta_key(meta_key)?; - let meta = decrypt_threshold_meta(encrypted, &cipher_info, &key) - .map_err(|e| Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())))?; - let t = meta.threshold.ok_or(crate::error::SharesError::MetaEncryption( - "threshold not in encrypted metadata".to_string(), - ))? as usize; + let meta = decrypt_threshold_meta(encrypted, &cipher_info, meta_key).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())) + })?; + let t = meta + .threshold + .ok_or(crate::error::SharesError::MetaEncryption( + "threshold not in encrypted metadata".to_string(), + ))? as usize; Ok((t, n)) } ThresholdDisclosure::FullConfidentialial => { - let encrypted = ms - .attributes - .get(&AttrId::EncryptedThresholdMeta) - .ok_or(crate::error::SharesError::MetaEncryption( + let encrypted = ms.attributes.get(&AttrId::EncryptedThresholdMeta).ok_or( + crate::error::SharesError::MetaEncryption( "missing EncryptedThresholdMeta".to_string(), - ))?; - let cipher_info_bytes = ms - .attributes - .get(&AttrId::ThresholdMetaCipher) - .ok_or(crate::error::SharesError::MetaEncryption( + ), + )?; + let cipher_info_bytes = ms.attributes.get(&AttrId::ThresholdMetaCipher).ok_or( + crate::error::SharesError::MetaEncryption( "missing ThresholdMetaCipher".to_string(), - ))?; - let cipher_info = ThresholdMetaCipher::from_cbor_bytes(cipher_info_bytes) - .map_err(|e| Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())))?; + ), + )?; + let cipher_info = + ThresholdMetaCipher::from_cbor_bytes(cipher_info_bytes).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())) + })?; let meta_key = meta_key.ok_or(crate::error::SharesError::MissingMetaKey)?; - let key = extract_meta_key(meta_key)?; - let meta = decrypt_threshold_meta(encrypted, &cipher_info, &key) - .map_err(|e| Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())))?; - let t = meta.threshold.ok_or(crate::error::SharesError::MetaEncryption( - "threshold not in encrypted metadata".to_string(), - ))? as usize; + let meta = decrypt_threshold_meta(encrypted, &cipher_info, meta_key).map_err(|e| { + Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())) + })?; + let t = meta + .threshold + .ok_or(crate::error::SharesError::MetaEncryption( + "threshold not in encrypted metadata".to_string(), + ))? as usize; let n = meta.limit.ok_or(crate::error::SharesError::MetaEncryption( "limit not in encrypted metadata".to_string(), ))? as usize; Ok((t, n)) } - _ => Err(Error::Shares(crate::error::SharesError::MetaEncryption( - format!("unsupported disclosure mode: {mode}"), - ))), } } -/// Extract a 32-byte key from a Multikey containing a symmetric cipher key. -fn extract_meta_key(meta_key: &multi_key::Multikey) -> Result>, Error> { - let dv = meta_key.data_view() - .map_err(|e| Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())))?; - let key = dv.key_bytes() - .map_err(|e| Error::Shares(crate::error::SharesError::MetaEncryption(e.to_string())))?; - if key.len() != 32 { - return Err(Error::Shares(crate::error::SharesError::MetaEncryption( - format!("meta key must be 32 bytes, got {}", key.len()), - ))); - } - Ok(key) -} - /// Stamp disclosure attributes onto a Multisig's attribute map. +/// +/// `meta_key` is the raw 32-byte symmetric key (extracted from a `Multikey` by +/// the caller); required for Partial/FullConfidentialial modes. pub fn stamp_disclosure_attrs( attributes: &mut std::collections::BTreeMap>, mode: ThresholdDisclosure, threshold: usize, limit: usize, - meta_key: Option<&multi_key::Multikey>, + meta_key: Option<&[u8]>, ) -> Result<(), Error> { use crate::error::SharesError; @@ -156,34 +452,37 @@ pub fn stamp_disclosure_attrs( } ThresholdDisclosure::Partial => { let meta_key = meta_key.ok_or(SharesError::MissingMetaKey)?; - let key = extract_meta_key(meta_key)?; let n_bytes: Vec = Varuint(limit).into(); attributes.insert(AttrId::Limit, n_bytes); let meta = ThresholdMetadata::threshold_only(threshold as u16); - let (ciphertext, cipher_info) = encrypt_threshold_meta(&meta, &key) + let (ciphertext, cipher_info) = encrypt_threshold_meta(&meta, meta_key) .map_err(|e| Error::Shares(SharesError::MetaEncryption(e.to_string())))?; attributes.insert(AttrId::EncryptedThresholdMeta, ciphertext); - attributes.insert(AttrId::ThresholdMetaCipher, cipher_info.to_cbor_bytes() - .map_err(|e| Error::Shares(SharesError::MetaEncryption(e.to_string())))?); + attributes.insert( + AttrId::ThresholdMetaCipher, + cipher_info + .to_cbor_bytes() + .map_err(|e| Error::Shares(SharesError::MetaEncryption(e.to_string())))?, + ); attributes.insert(AttrId::ThresholdDisclosure, mode.encode_into()); } ThresholdDisclosure::FullConfidentialial => { let meta_key = meta_key.ok_or(SharesError::MissingMetaKey)?; - let key = extract_meta_key(meta_key)?; let meta = ThresholdMetadata::new(threshold as u16, limit as u16); - let (ciphertext, cipher_info) = encrypt_threshold_meta(&meta, &key) + let (ciphertext, cipher_info) = encrypt_threshold_meta(&meta, meta_key) .map_err(|e| Error::Shares(SharesError::MetaEncryption(e.to_string())))?; attributes.insert(AttrId::EncryptedThresholdMeta, ciphertext); - attributes.insert(AttrId::ThresholdMetaCipher, cipher_info.to_cbor_bytes() - .map_err(|e| Error::Shares(SharesError::MetaEncryption(e.to_string())))?); + attributes.insert( + AttrId::ThresholdMetaCipher, + cipher_info + .to_cbor_bytes() + .map_err(|e| Error::Shares(SharesError::MetaEncryption(e.to_string())))?, + ); attributes.insert(AttrId::ThresholdDisclosure, mode.encode_into()); } - _ => return Err(Error::Shares(SharesError::MetaEncryption( - format!("unsupported disclosure mode: {mode}"), - ))), } Ok(()) } @@ -205,22 +504,19 @@ impl<'a> crate::views::ThresholdDisclosureView for DisclosureView<'a> { disclosure_mode(self.ms) } - fn read_threshold_params( - &self, - meta_key: Option<&multi_key::Multikey>, - ) -> Result<(usize, usize), Error> { + fn read_threshold_params(&self, meta_key: Option<&[u8]>) -> Result<(usize, usize), Error> { read_threshold_params(self.ms, meta_key) } 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 { let (t, n) = read_threshold_params(self.ms, current_meta_key)?; let mut new_ms = self.ms.clone(); stamp_disclosure_attrs(&mut new_ms.attributes, target, t, n, meta_key)?; Ok(new_ms) } -} \ No newline at end of file +}