From 46ee2b3859c164f7d058af4a828cce924854561d Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 13:19:24 -0600 Subject: [PATCH 1/3] update from bettersign Signed-off-by: Dave Grantham --- CHANGELOG.md | 17 +++ Cargo.toml | 50 ++++---- benches/multisig_bench.rs | 120 +++++++++++++++++++ src/attrid.rs | 4 +- src/error.rs | 76 ++++++++++-- src/lib.rs | 87 ++++++++++++-- src/ms.rs | 171 +++++++++++++++++++++++---- src/serde/de.rs | 16 +-- src/serde/mod.rs | 62 +++++----- src/serde/ser.rs | 4 +- src/types.rs | 228 ++++++++++++++++++++++++++++++++++++ src/views.rs | 26 +++- src/views/bls12381.rs | 125 +++++++++++++++----- src/views/ed25519.rs | 10 +- src/views/ed25519_hybrid.rs | 51 ++++++++ src/views/ed25519_mayo2.rs | 50 ++++++++ src/views/fn_dsa.rs | 50 ++++++++ src/views/mayo.rs | 50 ++++++++ src/views/ml_dsa.rs | 50 ++++++++ src/views/nist_p.rs | 72 ++++++++++++ src/views/rsa.rs | 64 ++++++++++ src/views/secp256k1.rs | 10 +- src/views/slh_dsa.rs | 50 ++++++++ tests/edge_case_tests.rs | 101 ++++++++++++++++ tests/proptest_tests.rs | 63 ++++++++++ tests/security_tests.rs | 83 +++++++++++++ 26 files changed, 1537 insertions(+), 153 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 benches/multisig_bench.rs create mode 100644 src/types.rs create mode 100644 src/views/ed25519_hybrid.rs create mode 100644 src/views/ed25519_mayo2.rs create mode 100644 src/views/fn_dsa.rs create mode 100644 src/views/mayo.rs create mode 100644 src/views/ml_dsa.rs create mode 100644 src/views/nist_p.rs create mode 100644 src/views/rsa.rs create mode 100644 src/views/slh_dsa.rs create mode 100644 tests/edge_case_tests.rs create mode 100644 tests/proptest_tests.rs create mode 100644 tests/security_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..23f23d5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +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.0] - 2026-07-13 + +### Changed +- Synced from bettersign workspace (bs-multisig 0.7.0) +- Renamed crate from `bs-multisig` to `multi-sig` +- Added PQC signature views (ML-DSA, FN-DSA, MAYO, SLH-DSA, RSA, NIST-P) +- Added hybrid signature views (Ed25519+MAYO2, Ed25519+ML-DSA-65, Ed25519+FN-DSA-512) +- Added `types.rs` module with type-safe wrappers +- Added comprehensive test suite (edge cases, proptest, security) +- Initial published release on crates.io as `multi-sig` \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9abc32e..b3427d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,43 +1,47 @@ [package] -name = "multisig" -version = "1.0.4" +name = "multi-sig" +version = "1.0.0" edition = "2021" authors = ["Dave Grantham "] description = "Multisig self-describing multicodec implementation for digital signatures" -repository = "https://github.com/cryptidtech/multisig.git" +repository = "https://github.com/cryptidtech/multi-sig.git" readme = "README.md" license = "Apache-2.0" +keywords = ["multiformats", "multisig", "signatures", "crypto"] +categories = ["cryptography", "encoding"] [features] default = ["serde"] [dependencies] -blsful = { version = "2.5" } -elliptic-curve = "0.13" -multibase = { version = "1.0", git = "https://github.com/cryptidtech/rust-multibase.git" } -multicodec = { version = "1.0", git = "https://github.com/cryptidtech/rust-multicodec.git" } -multitrait = { version = "1.0", git = "https://github.com/cryptidtech/multitrait.git" } -multiutil = { version = "1.0", git = "https://github.com/cryptidtech/multiutil.git" } -serde = { version = "1.0", default-features = false, features = [ - "alloc", - "derive", -], optional = true } -ssh-encoding = { version = "0.2" } -thiserror = "1.0" +# blsful configured per-target below (blst for native, rust for wasm) +multi-base = { version = "1.0", path = "../multi-base" } +multi-codec = { version = "1.0", path = "../multi-codec" } +multi-trait = { version = "1.0", path = "../multi-trait" } +multi-util = { version = "1.0", path = "../multi-util" } +elliptic-curve = "0.14" +serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } +ssh-encoding = "0.3" +thiserror = { version = "2.0" } unsigned-varint = { version = "0.8", features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -ssh-key = { version = "0.6", default-features = false, features = [ - "alloc", - "ecdsa", - "ed25519", -] } +blsful = { version = "4.0.0-rc1", default-features = false, features = ["rust"] } +ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["alloc", "ecdsa", "ed25519"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ssh-key = { version = "0.6", features = ["crypto"] } +blsful = { version = "4.0.0-rc1", default-features = false, features = ["blst"] } +ssh-key = { version = "0.7.0-rc.11", features = ["crypto"] } [dev-dependencies] +criterion = { version = "0.8", features = ["html_reports"] } hex = "0.4" -serde_test = "1.0" -serde_json = "1.0" +proptest = "1.4" serde_cbor = "0.11" +serde_json = "1.0" +serde_test = "1.0" + +[[bench]] +name = "multisig_bench" +harness = false +path = "benches/multisig_bench.rs" \ No newline at end of file diff --git a/benches/multisig_bench.rs b/benches/multisig_bench.rs new file mode 100644 index 0000000..bafd98a --- /dev/null +++ b/benches/multisig_bench.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Performance benchmarks for multi-sig + +use multi_codec::Codec; +use multi_sig::{Builder, Multisig, SIG_CODECS}; +use multi_trait::TryDecodeFrom; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use std::hint::black_box; + +/// Benchmark signature creation +fn bench_signature_creation(c: &mut Criterion) { + let mut group = c.benchmark_group("signature_creation"); + let data = black_box(b"benchmark signature data"); + + let algorithms = vec![ + ("Ed25519", Codec::Ed25519Pub), + ("Secp256k1", Codec::Secp256K1Pub), + ]; + + for (name, codec) in algorithms { + group.bench_with_input(BenchmarkId::new("create", name), &codec, |b, &codec| { + b.iter(|| Builder::new(codec).with_signature_bytes(data).try_build()) + }); + } + + group.finish(); +} + +/// Benchmark encoding multisig to bytes +fn bench_encoding(c: &mut Criterion) { + let ms = Builder::new(Codec::Ed25519Pub) + .with_signature_bytes(b"test data") + .try_build() + .unwrap(); + + c.bench_function("multisig_to_bytes", |b| { + b.iter(|| { + let _bytes: Vec = black_box(ms.clone()).into(); + }) + }); +} + +/// Benchmark decoding multisig from bytes +fn bench_decoding(c: &mut Criterion) { + let ms = Builder::new(Codec::Ed25519Pub) + .with_signature_bytes(b"test data") + .try_build() + .unwrap(); + let bytes: Vec = ms.into(); + + c.bench_function("multisig_from_bytes", |b| { + b.iter(|| Multisig::try_from(black_box(bytes.as_ref()))) + }); +} + +/// Benchmark roundtrip operations +fn bench_roundtrip(c: &mut Criterion) { + let mut group = c.benchmark_group("roundtrip"); + + for &codec in SIG_CODECS.iter().take(3) { + let name = format!("{:?}", codec); + group.bench_with_input(BenchmarkId::new("full", &name), &codec, |b, &codec| { + b.iter(|| { + let ms1 = Builder::new(codec) + .with_signature_bytes(b"roundtrip test") + .try_build() + .unwrap(); + let bytes: Vec = ms1.into(); + let _ms2 = Multisig::try_from(bytes.as_ref()).unwrap(); + }) + }); + } + + group.finish(); +} + +/// Benchmark with varying signature sizes +fn bench_signature_sizes(c: &mut Criterion) { + let mut group = c.benchmark_group("signature_sizes"); + + let sizes = vec![32, 64, 128, 256]; + + for size in sizes { + let sig_data = vec![0u8; size]; + group.bench_with_input(BenchmarkId::new("ed25519", size), &sig_data, |b, data| { + b.iter(|| { + Builder::new(Codec::Ed25519Pub) + .with_signature_bytes(black_box(data)) + .try_build() + }) + }); + } + + group.finish(); +} + +/// Benchmark TryDecodeFrom +fn bench_try_decode_from(c: &mut Criterion) { + let ms = Builder::new(Codec::Ed25519Pub) + .with_signature_bytes(b"decode test") + .try_build() + .unwrap(); + let bytes: Vec = ms.into(); + + c.bench_function("try_decode_from", |b| { + b.iter(|| Multisig::try_decode_from(black_box(bytes.as_ref()))) + }); +} + +criterion_group!( + benches, + bench_signature_creation, + bench_encoding, + bench_decoding, + bench_roundtrip, + bench_signature_sizes, + bench_try_decode_from +); + +criterion_main!(benches); diff --git a/src/attrid.rs b/src/attrid.rs index ea76b6a..c8d2f6c 100644 --- a/src/attrid.rs +++ b/src/attrid.rs @@ -1,6 +1,6 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{error::AttributesError, Error}; -use multitrait::{EncodeInto, TryDecodeFrom}; +use multi_trait::{EncodeInto, TryDecodeFrom}; use std::fmt; /// enum of attribute identifiers. this is here to avoid collisions between diff --git a/src/error.rs b/src/error.rs index cb08db7..0be137e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,8 @@ use std::fmt::Display; -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 /// Errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { /// Attributes error @@ -17,16 +17,16 @@ pub enum Error { /// A multibase conversion error #[error(transparent)] - Multibase(#[from] multibase::Error), + Multibase(#[from] multi_base::Error), /// A multicodec decoding error #[error(transparent)] - Multicodec(#[from] multicodec::Error), + Multicodec(#[from] multi_codec::Error), /// A multitrait error #[error(transparent)] - Multitrait(#[from] multitrait::Error), + Multitrait(#[from] multi_trait::Error), /// A multiutil error #[error(transparent)] - Multiutil(#[from] multiutil::Error), + Multiutil(#[from] multi_util::Error), /// Formatting error #[error(transparent)] @@ -52,12 +52,12 @@ pub enum Error { } /// Attributes errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum AttributesError { /// Unsupported signature algorithm #[error("Unsupported signature codec: {0}")] - UnsupportedCodec(multicodec::Codec), + UnsupportedCodec(multi_codec::Codec), /// No key data attribute #[error("Signature data missing")] MissingSignature, @@ -88,7 +88,7 @@ pub enum AttributesError { } /// Shares errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum SharesError { /// Too many shares @@ -124,7 +124,7 @@ pub enum SharesError { } /// Conversion errors -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum ConversionsError { /// Ssh conversion error @@ -133,7 +133,7 @@ pub enum ConversionsError { } /// SSH Errors -#[derive(Clone, Debug)] +#[derive(Debug)] pub enum SshError { /// SSH Sig Sig(ssh_key::Error), @@ -163,3 +163,57 @@ impl From for SshError { SshError::SigLabel(e) } } + +impl Error { + /// Get the error kind as a string + pub fn kind(&self) -> &str { + match self { + Self::Attributes(_) => "Attributes", + Self::Shares(_) => "Shares", + Self::Conversions(_) => "Conversions", + Self::Multibase(_) => "Multibase", + Self::Multicodec(_) => "Multicodec", + Self::Multitrait(_) => "Multitrait", + Self::Multiutil(_) => "Multiutil", + Self::Fmt(_) => "Fmt", + Self::Utf8(_) => "Utf8", + Self::Vsss(_) => "Vsss", + Self::MissingSigil => "MissingSigil", + Self::DuplicateAttribute(_) => "DuplicateAttribute", + Self::FailedConversion(_) => "FailedConversion", + Self::UnsupportedAlgorithm(_) => "UnsupportedAlgorithm", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_kind() { + let err = Error::MissingSigil; + assert_eq!(err.kind(), "MissingSigil"); + + let err = Error::DuplicateAttribute(42); + assert_eq!(err.kind(), "DuplicateAttribute"); + } + + #[test] + fn test_error_display() { + let err = Error::MissingSigil; + assert!(err.to_string().contains("sigil")); + + let err = Error::UnsupportedAlgorithm("test".to_string()); + assert!(err.to_string().contains("test")); + } + + #[test] + fn test_error_is_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + } +} diff --git a/src/lib.rs b/src/lib.rs index b448331..d8471f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,64 @@ -// SPDX-License-Idnetifier: Apache-2.0 -//! multisig +// SPDX-License-Identifier: Apache-2.0 +//! # multi-sig +//! +//! Self-describing digital signature implementation following the multisig specification. +//! +//! ## Overview +//! +//! This crate provides multisig functionality for creating and managing self-describing +//! digital signatures with support for multiple signature schemes including Ed25519, +//! Secp256k1, and BLS12-381. +//! +//! ## Supported Signature Schemes +//! +//! - **Ed25519** - Fast, secure elliptic curve signatures +//! - **Secp256k1** - Bitcoin/Ethereum curve signatures +//! - **BLS12-381** - Pairing-based signatures with aggregation support +//! +//! ## Quick Start +//! +//! ### Creating a Signature +//! +//! ```rust +//! use multi_sig::Builder; +//! use multi_codec::Codec; +//! +//! // Create a signature with Ed25519 +//! let sig_data = vec![0u8; 64]; // Ed25519 signature (64 bytes) +//! let multisig = Builder::new(Codec::Ed25519Pub) +//! .with_signature_bytes(&sig_data) +//! .try_build() +//! .unwrap(); +//! ``` +//! +//! ### Encoding and Decoding +//! +//! ```rust +//! use multi_sig::{Builder, Multisig}; +//! use multi_codec::Codec; +//! +//! let sig_data = vec![0u8; 64]; +//! let ms1 = Builder::new(Codec::Ed25519Pub) +//! .with_signature_bytes(&sig_data) +//! .try_build() +//! .unwrap(); +//! +//! // Encode to bytes +//! let bytes: Vec = ms1.clone().into(); +//! +//! // Decode from bytes +//! let ms2 = Multisig::try_from(bytes.as_ref()).unwrap(); +//! assert_eq!(ms1, ms2); +//! ``` +//! +//! ## Features +//! +//! - **`serde`** (default): Enables serde serialization support +//! +//! ## Thread Safety +//! +//! All types are `Send + Sync` and safe for concurrent use. + #![warn(missing_docs)] #![deny( trivial_casts, @@ -18,7 +77,11 @@ pub use attrid::AttrId; /// Multisig implementation pub mod ms; -pub use ms::{SIG_CODECS, SIG_SHARE_CODECS, Builder, EncodedMultisig, Multisig}; +pub use ms::{Builder, EncodedMultisig, Multisig, SIG_CODECS, SIG_SHARE_CODECS}; + +/// Type-safe wrappers for signature components +pub mod types; +pub use types::{SignatureBytes, SignatureScheme}; /// Views on the multisig pub mod views; @@ -28,11 +91,21 @@ pub use views::{AttrView, ConvView, DataView, ThresholdAttrView, ThresholdView, #[cfg(feature = "serde")] pub mod serde; -/// ...and in the darkness bind them +/// Commonly used items +/// +/// ``` +/// use multi_sig::prelude::*; +/// +/// let sig_data = vec![0u8; 64]; +/// let ms = Builder::new(Codec::Ed25519Pub) +/// .with_signature_bytes(&sig_data) +/// .try_build() +/// .unwrap(); +/// ``` pub mod prelude { pub use super::*; /// re-exports - pub use multibase::Base; - pub use multicodec::Codec; - pub use multiutil::BaseEncoded; + pub use multi_base::Base; + pub use multi_codec::Codec; + pub use multi_util::BaseEncoded; } diff --git a/src/ms.rs b/src/ms.rs index 1b4beea..617a314 100644 --- a/src/ms.rs +++ b/src/ms.rs @@ -1,30 +1,61 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::AttributesError, views::{ bls12381::{self, SchemeTypeId}, - ed25519, secp256k1, + ed25519, ed25519_hybrid, ed25519_mayo2, fn_dsa, mayo, ml_dsa, nist_p, rsa, secp256k1, + slh_dsa, }, AttrId, AttrView, ConvView, DataView, Error, ThresholdAttrView, ThresholdView, Views, }; -use blsful::{inner_types::GroupEncoding, vsss_rs::Share, Signature, SignatureShare}; -use multibase::Base; -use multicodec::Codec; -use multitrait::{Null, TryDecodeFrom}; -use multiutil::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, Varuint}; +use blsful::{ + inner_types::{GroupEncoding, PrimeField}, + vsss_rs::Share, + Signature, SignatureShare, +}; +use multi_base::Base; +use multi_codec::Codec; +use multi_trait::{Null, TryDecodeFrom}; +use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, Varuint}; use std::{collections::BTreeMap, fmt}; /// the list of signature codecs currently supported -pub const SIG_CODECS: [Codec; 4] = [ +pub const SIG_CODECS: [Codec; 35] = [ Codec::Bls12381G1Msig, Codec::Bls12381G2Msig, Codec::EddsaMsig, - // Codec::Es256Msig, - // Codec::Es384Msig, - // Codec::Es521Msig, - // Codec::Rs256Msig, - Codec::Es256KMsig, //, - //Codec::LamportMsig, + Codec::Es256KMsig, + Codec::Es256Msig, + Codec::Es384Msig, + Codec::Es521Msig, + Codec::Rs256Msig, + Codec::SlhDsaSha2128FMsig, + Codec::SlhDsaSha2128SMsig, + Codec::SlhDsaSha2192FMsig, + Codec::SlhDsaSha2192SMsig, + Codec::SlhDsaSha2256FMsig, + Codec::SlhDsaSha2256SMsig, + Codec::SlhDsaShake128FMsig, + Codec::SlhDsaShake128SMsig, + Codec::SlhDsaShake192FMsig, + Codec::SlhDsaShake192SMsig, + Codec::SlhDsaShake256FMsig, + Codec::SlhDsaShake256SMsig, + Codec::MlDsa65Msig, + Codec::MlDsa87Msig, + Codec::FnDsa512Msig, + Codec::FnDsa1024Msig, + Codec::Mayo1Msig, + Codec::Mayo2Msig, + Codec::Mayo3Msig, + Codec::Mayo5Msig, + Codec::Ed25519Mayo2Msig, + Codec::Ed25519Mldsa65Msig, + Codec::Ed25519Fndsa512Msig, + Codec::Bls12381G1Mldsa65Msig, + Codec::Bls12381G1Fndsa512Msig, + Codec::Bls12381G1Mayo1Msig, + Codec::Bls12381G1Mayo2Msig, ]; /// the list of signature share codecs supported @@ -44,7 +75,7 @@ pub type EncodedMultisig = BaseEncoded; pub type Attributes = BTreeMap>; /// The multisig structure -#[derive(Clone, Default, PartialEq)] +#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct Multisig { /// signature codec pub(crate) codec: Codec, @@ -85,13 +116,13 @@ impl From for Vec { // add in the signature codec v.append(&mut val.codec.into()); // add in the message - v.append(&mut Varbytes(val.message.clone()).into()); + v.append(&mut Varbytes::new(val.message.clone()).into()); // add in the number of attributes v.append(&mut Varuint(val.attributes.len()).into()); // add in the attributes val.attributes.iter().for_each(|(id, attr)| { v.append(&mut (*id).into()); - v.append(&mut Varbytes(attr.clone()).into()); + v.append(&mut Varbytes::new(attr.clone()).into()); }); v } @@ -186,6 +217,36 @@ impl Views for Multisig { | Codec::Bls12381G2ShareMsig => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::EddsaMsig => Ok(Box::new(ed25519::View::try_from(self)?)), Codec::Es256KMsig => Ok(Box::new(secp256k1::View::try_from(self)?)), + Codec::Es256Msig | Codec::Es384Msig | Codec::Es521Msig => { + Ok(Box::new(nist_p::View::try_from(self)?)) + } + Codec::Rs256Msig => Ok(Box::new(rsa::View::try_from(self)?)), + Codec::SlhDsaSha2128FMsig + | Codec::SlhDsaSha2128SMsig + | Codec::SlhDsaSha2192FMsig + | Codec::SlhDsaSha2192SMsig + | Codec::SlhDsaSha2256FMsig + | Codec::SlhDsaSha2256SMsig + | Codec::SlhDsaShake128FMsig + | Codec::SlhDsaShake128SMsig + | Codec::SlhDsaShake192FMsig + | Codec::SlhDsaShake192SMsig + | Codec::SlhDsaShake256FMsig + | Codec::SlhDsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Msig | Codec::MlDsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), + Codec::FnDsa512Msig | Codec::FnDsa1024Msig => { + Ok(Box::new(fn_dsa::View::try_from(self)?)) + } + Codec::Mayo1Msig | Codec::Mayo2Msig | Codec::Mayo3Msig | Codec::Mayo5Msig => { + Ok(Box::new(mayo::View::try_from(self)?)) + } + Codec::Ed25519Mayo2Msig => Ok(Box::new(ed25519_mayo2::View::try_from(self)?)), + Codec::Ed25519Mldsa65Msig + | Codec::Ed25519Fndsa512Msig + | Codec::Bls12381G1Mldsa65Msig + | Codec::Bls12381G1Fndsa512Msig + | Codec::Bls12381G1Mayo1Msig + | Codec::Bls12381G1Mayo2Msig => Ok(Box::new(ed25519_hybrid::View::try_from(self)?)), _ => Err(AttributesError::UnsupportedCodec(self.codec).into()), } } @@ -198,6 +259,36 @@ impl Views for Multisig { | Codec::Bls12381G2ShareMsig => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::EddsaMsig => Ok(Box::new(ed25519::View::try_from(self)?)), Codec::Es256KMsig => Ok(Box::new(secp256k1::View::try_from(self)?)), + Codec::Es256Msig | Codec::Es384Msig | Codec::Es521Msig => { + Ok(Box::new(nist_p::View::try_from(self)?)) + } + Codec::Rs256Msig => Ok(Box::new(rsa::View::try_from(self)?)), + Codec::SlhDsaSha2128FMsig + | Codec::SlhDsaSha2128SMsig + | Codec::SlhDsaSha2192FMsig + | Codec::SlhDsaSha2192SMsig + | Codec::SlhDsaSha2256FMsig + | Codec::SlhDsaSha2256SMsig + | Codec::SlhDsaShake128FMsig + | Codec::SlhDsaShake128SMsig + | Codec::SlhDsaShake192FMsig + | Codec::SlhDsaShake192SMsig + | Codec::SlhDsaShake256FMsig + | Codec::SlhDsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Msig | Codec::MlDsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), + Codec::FnDsa512Msig | Codec::FnDsa1024Msig => { + Ok(Box::new(fn_dsa::View::try_from(self)?)) + } + Codec::Mayo1Msig | Codec::Mayo2Msig | Codec::Mayo3Msig | Codec::Mayo5Msig => { + Ok(Box::new(mayo::View::try_from(self)?)) + } + Codec::Ed25519Mayo2Msig => Ok(Box::new(ed25519_mayo2::View::try_from(self)?)), + Codec::Ed25519Mldsa65Msig + | Codec::Ed25519Fndsa512Msig + | Codec::Bls12381G1Mldsa65Msig + | Codec::Bls12381G1Fndsa512Msig + | Codec::Bls12381G1Mayo1Msig + | Codec::Bls12381G1Mayo2Msig => Ok(Box::new(ed25519_hybrid::View::try_from(self)?)), _ => Err(AttributesError::UnsupportedCodec(self.codec).into()), } } @@ -210,6 +301,36 @@ impl Views for Multisig { | Codec::Bls12381G2ShareMsig => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::EddsaMsig => Ok(Box::new(ed25519::View::try_from(self)?)), Codec::Es256KMsig => Ok(Box::new(secp256k1::View::try_from(self)?)), + Codec::Es256Msig | Codec::Es384Msig | Codec::Es521Msig => { + Ok(Box::new(nist_p::View::try_from(self)?)) + } + Codec::Rs256Msig => Ok(Box::new(rsa::View::try_from(self)?)), + Codec::SlhDsaSha2128FMsig + | Codec::SlhDsaSha2128SMsig + | Codec::SlhDsaSha2192FMsig + | Codec::SlhDsaSha2192SMsig + | Codec::SlhDsaSha2256FMsig + | Codec::SlhDsaSha2256SMsig + | Codec::SlhDsaShake128FMsig + | Codec::SlhDsaShake128SMsig + | Codec::SlhDsaShake192FMsig + | Codec::SlhDsaShake192SMsig + | Codec::SlhDsaShake256FMsig + | Codec::SlhDsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Msig | Codec::MlDsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), + Codec::FnDsa512Msig | Codec::FnDsa1024Msig => { + Ok(Box::new(fn_dsa::View::try_from(self)?)) + } + Codec::Mayo1Msig | Codec::Mayo2Msig | Codec::Mayo3Msig | Codec::Mayo5Msig => { + Ok(Box::new(mayo::View::try_from(self)?)) + } + Codec::Ed25519Mayo2Msig => Ok(Box::new(ed25519_mayo2::View::try_from(self)?)), + Codec::Ed25519Mldsa65Msig + | Codec::Ed25519Fndsa512Msig + | Codec::Bls12381G1Mldsa65Msig + | Codec::Bls12381G1Fndsa512Msig + | Codec::Bls12381G1Mayo1Msig + | Codec::Bls12381G1Mayo2Msig => Ok(Box::new(ed25519_hybrid::View::try_from(self)?)), _ => Err(AttributesError::UnsupportedCodec(self.codec).into()), } } @@ -297,7 +418,7 @@ impl Builder { } bls12381::ALGORITHM_NAME_G1_SHARE => { let sig_share = bls12381::SigShare::try_from(sig.as_bytes())?; - attributes.insert(AttrId::ShareIdentifier, Varuint(sig_share.0).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()); @@ -310,7 +431,7 @@ impl Builder { } bls12381::ALGORITHM_NAME_G2_SHARE => { let sig_share = bls12381::SigShare::try_from(sig.as_bytes())?; - attributes.insert(AttrId::ShareIdentifier, Varuint(sig_share.0).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()); @@ -334,7 +455,6 @@ impl Builder { { let scheme_type_id = SchemeTypeId::from(sig); let sig_bytes: Vec = sig.as_raw_value().to_bytes().as_ref().to_vec(); - println!("signature length: {}", sig_bytes.len()); let codec = match sig_bytes.len() { 48 => Codec::Bls12381G1Msig, // G1Projective::to_compressed() 96 => Codec::Bls12381G2Msig, // G2Projective::to_compressed() @@ -365,9 +485,8 @@ impl Builder { { let scheme_type_id = SchemeTypeId::from(sigshare); let sigshare = sigshare.as_raw_value(); - let identifier = sigshare.identifier(); - let value = sigshare.value_vec(); - println!("sigshare len: {}", value.len()); + let identifier = sigshare.identifier().0.to_repr().as_ref().to_vec(); + let value = sigshare.value().0.to_bytes().as_ref().to_vec(); let codec = match value.len() { 48 => Codec::Bls12381G1ShareMsig, // large pubkeys, small signatures 96 => Codec::Bls12381G2ShareMsig, // small pubkeys, large signatures @@ -381,7 +500,7 @@ impl Builder { attributes.insert(AttrId::SigData, value); attributes.insert(AttrId::Threshold, Varuint(threshold).into()); attributes.insert(AttrId::Limit, Varuint(limit).into()); - attributes.insert(AttrId::ShareIdentifier, Varuint(identifier).into()); + attributes.insert(AttrId::ShareIdentifier, identifier); attributes.insert(AttrId::Scheme, scheme_type_id.into()); Ok(Self { codec, @@ -436,8 +555,8 @@ impl Builder { } /// add the threshold signature identifier - pub fn with_identifier(self, identifier: u8) -> Self { - self.with_attribute(AttrId::ShareIdentifier, &Varuint(identifier).into()) + pub fn with_identifier(self, identifier: &impl AsRef<[u8]>) -> Self { + self.with_attribute(AttrId::ShareIdentifier, &identifier.as_ref().to_vec()) } /// add the threshold data diff --git a/src/serde/de.rs b/src/serde/de.rs index a83bf9d..c0d9977 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,11 +1,11 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ ms::{self, Attributes}, AttrId, Multisig, }; +use multi_codec::Codec; +use multi_util::EncodedVarbytes; use core::fmt; -use multicodec::Codec; -use multiutil::EncodedVarbytes; use serde::{ de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, @@ -30,28 +30,28 @@ impl<'de> Deserialize<'de> for AttrId { where E: Error, { - AttrId::try_from(c).map_err(E::custom) + AttrId::try_from(c).map_err(|e| Error::custom(e.to_string())) } fn visit_str(self, s: &str) -> Result where E: Error, { - AttrId::try_from(s).map_err(E::custom) + AttrId::try_from(s).map_err(|e| Error::custom(e.to_string())) } fn visit_borrowed_str(self, s: &'de str) -> Result where E: Error, { - AttrId::try_from(s).map_err(E::custom) + AttrId::try_from(s).map_err(|e| Error::custom(e.to_string())) } fn visit_string(self, s: String) -> Result where E: Error, { - AttrId::try_from(s.as_str()).map_err(E::custom) + AttrId::try_from(s.as_str()).map_err(|e| Error::custom(e.to_string())) } } @@ -144,7 +144,7 @@ impl<'de> Deserialize<'de> for Multisig { deserializer.deserialize_struct(ms::SIGIL.as_str(), FIELDS, MultisigVisitor) } else { let b: &'de [u8] = Deserialize::deserialize(deserializer)?; - Ok(Self::try_from(b).map_err(D::Error::custom)?) + Ok(Self::try_from(b).map_err(|e| Error::custom(e.to_string()))?) } } } diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 6752f59..f2d7bfd 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -1,14 +1,14 @@ -// SPDX-License-Idnetifier: Apache-2.0 -//! Serde (de)serialization for [`crate::Varsig`]. +// SPDX-License-Identifier: Apache-2.0 +//! Serde (de)serialization for Multisig types. mod de; mod ser; #[cfg(test)] mod tests { use crate::{Builder, EncodedMultisig, Multisig}; - use multibase::Base; - use multicodec::Codec; - use multitrait::Null; + use multi_base::Base; + use multi_codec::Codec; + use multi_trait::Null; use serde_test::{assert_tokens, Configure, Token}; #[test] @@ -20,9 +20,11 @@ mod tests { assert_tokens( &ms.compact(), - &[ - Token::BorrowedBytes(&[185, 36, 237, 161, 3, 0, 1, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - ], + &[Token::BorrowedBytes(&[ + 185, 36, 237, 161, 3, 0, 1, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])], ) } @@ -102,9 +104,11 @@ mod tests { assert_tokens( &ms.compact(), - &[ - Token::BorrowedBytes(&[185, 36, 231, 161, 3, 0, 1, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - ], + &[Token::BorrowedBytes(&[ + 185, 36, 231, 161, 3, 0, 1, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])], ) } @@ -194,9 +198,11 @@ mod tests { assert_tokens( &ms.compact(), - &[ - Token::BorrowedBytes(&[185, 36, 129, 166, 192, 6, 0, 1, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - ], + &[Token::BorrowedBytes(&[ + 185, 36, 129, 166, 192, 6, 0, 1, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])], ) } @@ -296,9 +302,12 @@ mod tests { assert_tokens( &ms.compact(), - &[ - Token::BorrowedBytes(&[185, 36, 132, 166, 192, 6, 0, 5, 0, 48, 152, 175, 120, 31, 124, 6, 98, 85, 113, 18, 249, 33, 229, 127, 185, 10, 132, 139, 133, 192, 179, 151, 169, 254, 24, 127, 64, 87, 238, 62, 160, 166, 11, 248, 130, 40, 23, 219, 198, 34, 33, 112, 156, 45, 227, 128, 63, 46, 2, 1, 2, 3, 1, 3, 4, 1, 4, 5, 1, 1]) - ], + &[Token::BorrowedBytes(&[ + 185, 36, 132, 166, 192, 6, 0, 5, 0, 48, 152, 175, 120, 31, 124, 6, 98, 85, 113, 18, + 249, 33, 229, 127, 185, 10, 132, 139, 133, 192, 179, 151, 169, 254, 24, 127, 64, + 87, 238, 62, 160, 166, 11, 248, 130, 40, 23, 219, 198, 34, 33, 112, 156, 45, 227, + 128, 63, 46, 2, 1, 2, 3, 1, 3, 4, 1, 4, 5, 1, 1, + ])], ) } @@ -390,12 +399,7 @@ mod tests { #[test] fn test_null_multisig_serde_compact() { let ms = Multisig::null(); - assert_tokens( - &ms.compact(), - &[ - Token::BorrowedBytes(&[185, 36, 0, 0, 0]) - ], - ); + assert_tokens(&ms.compact(), &[Token::BorrowedBytes(&[185, 36, 0, 0, 0])]); } #[test] @@ -404,7 +408,10 @@ mod tests { assert_tokens( &ms.readable(), &[ - Token::Struct { name: "multisig", len: 3, }, + Token::Struct { + name: "multisig", + len: 3, + }, Token::BorrowedStr("codec"), Token::BorrowedStr("identity"), Token::BorrowedStr("message"), @@ -420,11 +427,6 @@ mod tests { #[test] fn test_encoded_null_multisig_serde_readable() { let ms: EncodedMultisig = Multisig::null().into(); - assert_tokens( - &ms.readable(), - &[ - Token::BorrowedStr("fb924000000"), - ], - ); + assert_tokens(&ms.readable(), &[Token::BorrowedStr("fb924000000")]); } } diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 3cb4bb4..97e7b6c 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -1,6 +1,6 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ms, AttrId, Multisig}; -use multiutil::{EncodedVarbytes, EncodingInfo, Varbytes}; +use multi_util::{EncodedVarbytes, EncodingInfo, Varbytes}; use serde::ser::{self, SerializeStruct}; /// Serialize instance of [`crate::AttrId`] diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..738b1de --- /dev/null +++ b/src/types.rs @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Type-safe wrappers for signature components + +use multi_codec::Codec; +use core::fmt; + +/// A cryptographic signature +/// +/// This newtype provides type safety for signature bytes. +/// +/// # Examples +/// +/// ``` +/// use multi_sig::types::SignatureBytes; +/// +/// let sig = SignatureBytes::new(vec![0u8; 64]); +/// assert_eq!(sig.len(), 64); +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SignatureBytes(Vec); + +impl SignatureBytes { + /// Create a new SignatureBytes + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + /// Get signature as bytes + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Get length in bytes + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Convert into inner bytes + pub fn into_bytes(self) -> Vec { + self.0 + } +} + +impl From> for SignatureBytes { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From for Vec { + fn from(sig: SignatureBytes) -> Vec { + sig.0 + } +} + +impl AsRef<[u8]> for SignatureBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for SignatureBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Display as hex without external dependency + for byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +/// A signature scheme identifier +/// +/// This newtype provides type safety for signature algorithm codecs. +/// +/// # Examples +/// +/// ``` +/// use multi_sig::types::SignatureScheme; +/// use multi_codec::Codec; +/// +/// let scheme = SignatureScheme::new(Codec::Ed25519Pub); +/// assert_eq!(scheme.codec(), Codec::Ed25519Pub); +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SignatureScheme(Codec); + +impl SignatureScheme { + /// Create a new SignatureScheme + pub const fn new(codec: Codec) -> Self { + Self(codec) + } + + /// Get the underlying codec + pub const fn codec(self) -> Codec { + self.0 + } + + /// Get the codec name + pub fn name(self) -> &'static str { + self.0.into() + } + + /// Get the codec code + pub fn code(self) -> u64 { + self.0.code() + } +} + +impl From for SignatureScheme { + fn from(codec: Codec) -> Self { + Self(codec) + } +} + +impl From for Codec { + fn from(scheme: SignatureScheme) -> Codec { + scheme.0 + } +} + +impl fmt::Display for SignatureScheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_signature_bytes_new() { + let sig = SignatureBytes::new(vec![1, 2, 3]); + assert_eq!(sig.as_bytes(), &[1, 2, 3]); + } + + #[test] + fn test_signature_bytes_len() { + let sig = SignatureBytes::new(vec![0u8; 64]); + assert_eq!(sig.len(), 64); + } + + #[test] + fn test_signature_bytes_is_empty() { + let empty = SignatureBytes::new(vec![]); + assert!(empty.is_empty()); + + let sig = SignatureBytes::new(vec![1]); + assert!(!sig.is_empty()); + } + + #[test] + fn test_signature_bytes_conversions() { + let bytes = vec![1, 2, 3, 4]; + let sig = SignatureBytes::from(bytes.clone()); + let back: Vec = sig.into_bytes(); + assert_eq!(back, bytes); + } + + #[test] + fn test_signature_bytes_as_ref() { + let sig = SignatureBytes::new(vec![1, 2, 3]); + let slice: &[u8] = sig.as_ref(); + assert_eq!(slice, &[1, 2, 3]); + } + + #[test] + fn test_signature_bytes_display() { + let sig = SignatureBytes::new(vec![0xDE, 0xAD]); + assert_eq!(sig.to_string(), "dead"); + } + + #[test] + fn test_signature_scheme_new() { + let scheme = SignatureScheme::new(Codec::Ed25519Pub); + assert_eq!(scheme.codec(), Codec::Ed25519Pub); + } + + #[test] + fn test_signature_scheme_name() { + let scheme = SignatureScheme::new(Codec::Ed25519Pub); + assert_eq!(scheme.name(), "ed25519-pub"); + } + + #[test] + fn test_signature_scheme_code() { + let scheme = SignatureScheme::new(Codec::Ed25519Pub); + assert_eq!(scheme.code(), 0xED); + } + + #[test] + fn test_signature_scheme_conversions() { + let codec = Codec::Secp256K1Pub; + let scheme = SignatureScheme::from(codec); + let back: Codec = scheme.into(); + assert_eq!(back, codec); + } + + #[test] + fn test_signature_scheme_display() { + let scheme = SignatureScheme::new(Codec::Ed25519Pub); + assert_eq!(scheme.to_string(), "ed25519-pub"); + } + + #[test] + fn test_signature_scheme_copy() { + let scheme1 = SignatureScheme::new(Codec::Bls12381G1Pub); + let scheme2 = scheme1; + assert_eq!(scheme1, scheme2); + } + + #[test] + fn test_newtypes_are_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + assert_send::(); + assert_sync::(); + } +} diff --git a/src/views.rs b/src/views.rs index 951af69..c3be6c2 100644 --- a/src/views.rs +++ b/src/views.rs @@ -1,14 +1,34 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{Error, Multisig}; -use multicodec::Codec; +use multi_codec::Codec; /// BLS12 381 G1/G2 signature implementation pub mod bls12381; /// Edwards curve 25519 signature implementation pub mod ed25519; +/// Generic Ed25519 hybrid signature view (codec-agnostic holder) +pub(crate) mod ed25519_hybrid; +/// Ed25519-MAYO2 hybrid signature implementation +pub(crate) mod ed25519_mayo2; +/// FN-DSA post-quantum signature implementation; FIPS 206 (draft) +pub mod fn_dsa; +/// MAYO post-quantum multivariate signature implementation +pub mod mayo; +/// ML-DSA post-quantum signature implementation; FIPS 204 +pub mod ml_dsa; +/// NIST P-256/P-384/P-521 ECDSA signature implementation +pub mod nist_p; +/// RSA-SHA256 signature implementation +pub mod rsa; /// Koblitz 256k1 curve implmentation (a.k.a. the Bitcoin curve) pub mod secp256k1; +/// SLH-DSA post-quantum signature implementation; FIPS 205 +pub mod slh_dsa; +/// +/// Attributes views let you inquire about the Multisig and retrieve data +/// associated with the particular view. +/// /// trait for returning the attributes of the Multisig pub trait AttrView { /// get the codec that the signed message was encoded with @@ -36,7 +56,7 @@ pub trait ThresholdAttrView { /// get the limit value for this multisig share fn limit(&self) -> Result; /// get the identifier value for this multisig share - fn identifier(&self) -> Result; + fn identifier(&self) -> Result<&[u8], Error>; /// get the threshold data associated with the signature fn threshold_data(&self) -> Result<&[u8], Error>; } diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index f476402..a827f48 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -1,15 +1,17 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::{AttributesError, ConversionsError, SharesError}, AttrId, AttrView, Builder, ConvView, DataView, Error, Multisig, ThresholdAttrView, ThresholdView, Views, }; use blsful::{ - vsss_rs::Share, Bls12381G1Impl, Bls12381G2Impl, Signature, SignatureSchemes, SignatureShare, + inner_types::{G1Projective, G2Projective, Scalar}, + vsss_rs::{IdentifierPrimeField, Share, ValueGroup}, + Bls12381G1Impl, Bls12381G2Impl, Signature, SignatureSchemes, SignatureShare, }; -use multicodec::Codec; -use multitrait::{EncodeInto, TryDecodeFrom}; -use multiutil::{Varbytes, Varuint}; +use multi_codec::Codec; +use multi_trait::{EncodeInto, TryDecodeFrom}; +use multi_util::{Varbytes, Varuint}; use std::{collections::BTreeMap, fmt}; /// the name used to identify these signatures in non-Multikey formats @@ -173,7 +175,7 @@ impl From for Vec { // add in the signature type id v.append(&mut val.0.into()); // add in the signature bytes - v.append(&mut Varbytes(val.1.clone()).into()); + v.append(&mut Varbytes::new(val.1.clone()).into()); v } } @@ -203,7 +205,7 @@ impl<'a> TryDecodeFrom<'a> for SigCombined { #[derive(Clone)] pub struct SigShare( /// identifier - pub u8, + pub IdentifierPrimeField, /// threshold pub usize, /// limit @@ -218,7 +220,7 @@ impl From for Vec { fn from(val: SigShare) -> Self { let mut v = Vec::default(); // add in the share identifier - v.append(&mut Varuint(val.0).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 @@ -226,7 +228,7 @@ impl From for Vec { // add in the share type id v.append(&mut val.3.into()); // add in the share data - v.append(&mut Varbytes(val.4.clone()).into()); + v.append(&mut Varbytes::new(val.4.clone()).into()); v } } @@ -245,7 +247,10 @@ impl<'a> TryDecodeFrom<'a> for SigShare { fn try_decode_from(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> { // try to decode the identifier - let (id, ptr) = Varuint::::try_decode_from(bytes)?; + let (id_bytes, ptr) = Varuint::<[u8; 32]>::try_decode_from(bytes)?; + let id = Option::::from(Scalar::from_be_bytes(&id_bytes)).ok_or( + Error::FailedConversion("Can't convert identifier to scalar".to_string()), + )?; // try to decode the threshold let (threshold, ptr) = Varuint::::try_decode_from(ptr)?; // try to decode the limit @@ -256,7 +261,7 @@ impl<'a> TryDecodeFrom<'a> for SigShare { let (share_data, ptr) = Varbytes::try_decode_from(ptr)?; Ok(( Self( - id.to_inner(), + IdentifierPrimeField(id), threshold.to_inner(), limit.to_inner(), share_type, @@ -268,7 +273,7 @@ impl<'a> TryDecodeFrom<'a> for SigShare { } #[derive(Clone, Default)] -pub(crate) struct ThresholdData(pub(crate) BTreeMap); +pub(crate) struct ThresholdData(pub(crate) BTreeMap, SigShare>); impl From for Vec { fn from(val: ThresholdData) -> Self { @@ -329,7 +334,7 @@ impl<'a> TryFrom<&'a Multisig> for View<'a> { } } -impl AttrView for View<'_> { +impl<'a> AttrView for View<'a> { /// for Bls Multisigs, the payload encoding is stored using the /// SchemeTypeId::PayloadEncoding attribute id. fn payload_encoding(&self) -> Result { @@ -354,7 +359,7 @@ impl AttrView for View<'_> { } } -impl DataView for View<'_> { +impl<'a> DataView for View<'a> { /// For Bls Multisig values, the sig data is stored using the /// SchemeTypeId::SigData attribute id. fn sig_bytes(&self) -> Result, Error> { @@ -367,7 +372,7 @@ impl DataView for View<'_> { } } -impl ConvView for View<'_> { +impl<'a> ConvView for View<'a> { /// convert to SSH signature format fn to_ssh_signature(&self) -> Result { // get the signature data @@ -410,11 +415,28 @@ impl ConvView for View<'_> { let av = self.ms.threshold_attr_view()?; let threshold = av.threshold()?; let limit = av.limit()?; - let identifier = av.identifier()?; + let identifier_bytes = av.identifier()?; + if identifier_bytes.len() != 32 { + return Err(Error::FailedConversion( + "Insufficient identifier bytes".to_string(), + )); + } + let identifier_array = <[u8; 32]>::try_from(identifier_bytes) + .map_err(|_| Error::FailedConversion("Invalid bytes".to_string()))?; + let id_scalar = Option::::from(Scalar::from_be_bytes(&identifier_array)) + .ok_or(Error::FailedConversion( + "Invalid share identifier bytes".to_string(), + ))?; // create the sig share tuple - let sig_data: Vec = - SigShare(identifier, threshold, limit, scheme_type, sig_bytes).into(); + let sig_data: Vec = SigShare( + IdentifierPrimeField(id_scalar), + threshold, + limit, + scheme_type, + sig_bytes, + ) + .into(); Ok(ssh_key::Signature::new( ssh_key::Algorithm::Other( @@ -430,11 +452,28 @@ impl ConvView for View<'_> { let av = self.ms.threshold_attr_view()?; let threshold = av.threshold()?; let limit = av.limit()?; - let identifier = av.identifier()?; + let identifier_bytes = av.identifier()?; + if identifier_bytes.len() != 32 { + return Err(Error::FailedConversion( + "Insufficient identifier bytes".to_string(), + )); + } + let identifier_array = <[u8; 32]>::try_from(identifier_bytes) + .map_err(|_| Error::FailedConversion("Invalid bytes".to_string()))?; + let id_scalar = Option::::from(Scalar::from_be_bytes(&identifier_array)) + .ok_or(Error::FailedConversion( + "Invalid share identifier bytes".to_string(), + ))?; // create the sig share tuple - let sig_data: Vec = - SigShare(identifier, threshold, limit, scheme_type, sig_bytes).into(); + let sig_data: Vec = SigShare( + IdentifierPrimeField(id_scalar), + threshold, + limit, + scheme_type, + sig_bytes, + ) + .into(); Ok(ssh_key::Signature::new( ssh_key::Algorithm::Other( @@ -450,7 +489,7 @@ impl ConvView for View<'_> { } } -impl ThresholdAttrView for View<'_> { +impl<'a> ThresholdAttrView for View<'a> { /// get the threshold value for this multisig fn threshold(&self) -> Result { let threshold = self @@ -470,7 +509,7 @@ impl ThresholdAttrView for View<'_> { Ok(Varuint::::try_from(limit.as_slice())?.to_inner()) } /// get the share identifier - fn identifier(&self) -> Result { + fn identifier(&self) -> Result<&[u8], Error> { match self.ms.codec { Codec::Bls12381G1ShareMsig | Codec::Bls12381G2ShareMsig => { let identifier = self @@ -478,7 +517,7 @@ impl ThresholdAttrView for View<'_> { .attributes .get(&AttrId::ShareIdentifier) .ok_or(AttributesError::MissingIdentifier)?; - Ok(Varuint::::try_from(identifier.as_slice())?.to_inner()) + Ok(identifier.as_slice()) } _ => Err(SharesError::NotASignatureShare.into()), } @@ -495,7 +534,7 @@ impl ThresholdAttrView for View<'_> { } /// trait for accumulating shares to rebuild a threshold signature -impl ThresholdView for View<'_> { +impl<'a> ThresholdView for View<'a> { /// get the signature shares fn shares(&self) -> Result, Error> { // get the codec for the new share multisigs @@ -533,7 +572,7 @@ impl ThresholdView for View<'_> { // and the payload encoding value let share = Builder::new(codec) .with_message_bytes(&self.ms.message.as_slice()) - .with_identifier(share.0) + .with_identifier(&share.0 .0.to_be_bytes()) .with_threshold(share.1) .with_limit(share.2) .with_signature_bytes(&share.4) @@ -566,7 +605,19 @@ impl ThresholdView for View<'_> { let av = share.threshold_attr_view()?; let threshold = av.threshold()?; let limit = av.limit()?; - let identifier = av.identifier()?; + let identifier_bytes = av.identifier()?; + if identifier_bytes.len() != 32 { + return Err(Error::FailedConversion( + "Insufficient number of identifier bytes".to_string(), + )); + } + let identifier_array = <[u8; 32]>::try_from(identifier_bytes) + .map_err(|_| Error::FailedConversion("Incorrect identifier bytes".to_string()))?; + let identifier = IdentifierPrimeField( + Option::::from(Scalar::from_be_bytes(&identifier_array)).ok_or( + Error::FailedConversion("Incorrect identifier bytes".to_string()), + )?, + ); // get the share's signature data let dv = share.data_view()?; @@ -654,8 +705,14 @@ impl ThresholdView for View<'_> { .0 .iter() .try_for_each(|(id, share)| -> Result<(), Error> { - let vsss = Share::with_identifier_and_value(*id, share.4.as_slice()); - // check to make sure all of the shares are of the same type + let bytes: [u8; 48] = share.4.as_slice().try_into().map_err(|_| { + Error::FailedConversion("Invalid signature share bytes".to_string()) + })?; + let inner = Option::from(G1Projective::from_compressed(&bytes)).ok_or( + Error::FailedConversion("Invalid signature share bytes".to_string()), + )?; + let vsss = Share::with_identifier_and_value(*id, ValueGroup(inner)); + // check to make sure all shares are of the same type if let Some(sti) = share_type_id { if sti != share.3 { return Err(SharesError::ShareTypeMismatch.into()); @@ -694,8 +751,14 @@ impl ThresholdView for View<'_> { .0 .iter() .try_for_each(|(id, share)| -> Result<(), Error> { - let vsss = Share::with_identifier_and_value(*id, share.4.as_slice()); - // check to make sure all of the shares are of the same type + let bytes: [u8; 96] = share.4.as_slice().try_into().map_err(|_| { + Error::FailedConversion("Invalid signature share bytes".to_string()) + })?; + let inner = Option::from(G2Projective::from_compressed(&bytes)).ok_or( + Error::FailedConversion("Invalid signature share bytes".to_string()), + )?; + let vsss = Share::with_identifier_and_value(*id, ValueGroup(inner)); + // check to make sure all shares are of the same type if let Some(sti) = share_type_id { if sti != share.3 { return Err(SharesError::ShareTypeMismatch.into()); diff --git a/src/views/ed25519.rs b/src/views/ed25519.rs index f71a103..d4d37f7 100644 --- a/src/views/ed25519.rs +++ b/src/views/ed25519.rs @@ -1,9 +1,9 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::{AttributesError, ConversionsError}, AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, }; -use multicodec::Codec; +use multi_codec::Codec; pub(crate) struct View<'a> { ms: &'a Multisig, @@ -17,7 +17,7 @@ impl<'a> TryFrom<&'a Multisig> for View<'a> { } } -impl AttrView for View<'_> { +impl<'a> AttrView for View<'a> { /// for EdDSA Multisigs, the payload encoding is stored using the /// AttrId::PayloadEncoding attribute id. fn payload_encoding(&self) -> Result { @@ -35,7 +35,7 @@ impl AttrView for View<'_> { } } -impl DataView for View<'_> { +impl<'a> DataView for View<'a> { /// For EdDSA Multisig values, the sig data is stored using the /// AttrId::SigData attribute id. fn sig_bytes(&self) -> Result, Error> { @@ -48,7 +48,7 @@ impl DataView for View<'_> { } } -impl ConvView for View<'_> { +impl<'a> ConvView for View<'a> { /// convert to SSH signature format fn to_ssh_signature(&self) -> Result { // get the signature data diff --git a/src/views/ed25519_hybrid.rs b/src/views/ed25519_hybrid.rs new file mode 100644 index 0000000..7e9e340 --- /dev/null +++ b/src/views/ed25519_hybrid.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +//! 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 multi_codec::Codec; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + Ok(Codec::try_from(v.as_slice())?) + } + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + Err(Error::UnsupportedAlgorithm( + "Ed25519 hybrid signatures are not supported in SSH signature format".into(), + )) + } +} diff --git a/src/views/ed25519_mayo2.rs b/src/views/ed25519_mayo2.rs new file mode 100644 index 0000000..b8d4cca --- /dev/null +++ b/src/views/ed25519_mayo2.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Ed25519-MAYO2 hybrid multisig view. + +use crate::{error::AttributesError, AttrId, AttrView, ConvView, DataView, Error, Multisig}; +use multi_codec::Codec; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + Ok(Codec::try_from(v.as_slice())?) + } + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + Err(Error::UnsupportedAlgorithm( + "Ed25519-MAYO2 not supported in SSH signature format".into(), + )) + } +} diff --git a/src/views/fn_dsa.rs b/src/views/fn_dsa.rs new file mode 100644 index 0000000..c013347 --- /dev/null +++ b/src/views/fn_dsa.rs @@ -0,0 +1,50 @@ +// 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 multi_codec::Codec; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + Ok(Codec::try_from(v.as_slice())?) + } + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + Err(Error::UnsupportedAlgorithm( + "FN-DSA (Falcon) not supported in SSH signature format".into(), + )) + } +} diff --git a/src/views/mayo.rs b/src/views/mayo.rs new file mode 100644 index 0000000..74c1558 --- /dev/null +++ b/src/views/mayo.rs @@ -0,0 +1,50 @@ +// 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 multi_codec::Codec; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + Ok(Codec::try_from(v.as_slice())?) + } + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + Err(Error::UnsupportedAlgorithm( + "MAYO not supported in SSH signature format".into(), + )) + } +} diff --git a/src/views/ml_dsa.rs b/src/views/ml_dsa.rs new file mode 100644 index 0000000..6a6cfed --- /dev/null +++ b/src/views/ml_dsa.rs @@ -0,0 +1,50 @@ +// 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 multi_codec::Codec; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + Ok(Codec::try_from(v.as_slice())?) + } + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + Err(Error::UnsupportedAlgorithm( + "ML-DSA (Dilithium) not supported in SSH signature format".into(), + )) + } +} diff --git a/src/views/nist_p.rs b/src/views/nist_p.rs new file mode 100644 index 0000000..dfa146e --- /dev/null +++ b/src/views/nist_p.rs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +//! NIST P-256/P-384/P-521 ECDSA multisig view. + +use crate::{ + error::{AttributesError, ConversionsError}, + AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, +}; +use multi_codec::Codec; + +fn algorithm_name(codec: Codec) -> &'static str { + match codec { + Codec::Es256Msig => "ecdsa-sha2-nistp256@multisig", + Codec::Es384Msig => "ecdsa-sha2-nistp384@multisig", + Codec::Es521Msig => "ecdsa-sha2-nistp521@multisig", + _ => "ecdsa@multisig", + } +} + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + let encoding = Codec::try_from(v.as_slice())?; + Ok(encoding) + } + + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + let dv = self.ms.data_view()?; + let sig_bytes = dv.sig_bytes()?; + let algo_name = algorithm_name(self.ms.codec); + Ok(ssh_key::Signature::new( + ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algo_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + sig_bytes, + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} diff --git a/src/views/rsa.rs b/src/views/rsa.rs new file mode 100644 index 0000000..4ccda1c --- /dev/null +++ b/src/views/rsa.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +//! RSA-SHA256 multisig view. + +use crate::{ + error::{AttributesError, ConversionsError}, + AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, +}; +use multi_codec::Codec; + +const ALGORITHM_NAME: &str = "rsa-sha256@multisig"; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + let encoding = Codec::try_from(v.as_slice())?; + Ok(encoding) + } + + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + let dv = self.ms.data_view()?; + let sig_bytes = dv.sig_bytes()?; + Ok(ssh_key::Signature::new( + ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(ALGORITHM_NAME) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + sig_bytes, + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} diff --git a/src/views/secp256k1.rs b/src/views/secp256k1.rs index 06a7c9c..2c9a3c6 100644 --- a/src/views/secp256k1.rs +++ b/src/views/secp256k1.rs @@ -1,9 +1,9 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::{AttributesError, ConversionsError}, AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, }; -use multicodec::Codec; +use multi_codec::Codec; /// the name used to identify these signatures in non-Multikey formats pub const ALGORITHM_NAME: &str = "secp256k1@multisig"; @@ -20,7 +20,7 @@ impl<'a> TryFrom<&'a Multisig> for View<'a> { } } -impl AttrView for View<'_> { +impl<'a> AttrView for View<'a> { /// for Es256K Multisigs, the payload encoding is stored using the /// AttrId::PayloadEncoding attribute id. fn payload_encoding(&self) -> Result { @@ -38,7 +38,7 @@ impl AttrView for View<'_> { } } -impl DataView for View<'_> { +impl<'a> DataView for View<'a> { /// For Secp256K1Pub Multisig values, the sig data is stored using the /// AttrId::SigData attribute id. fn sig_bytes(&self) -> Result, Error> { @@ -51,7 +51,7 @@ impl DataView for View<'_> { } } -impl ConvView for View<'_> { +impl<'a> ConvView for View<'a> { /// convert to SSH signature format fn to_ssh_signature(&self) -> Result { // get the signature data diff --git a/src/views/slh_dsa.rs b/src/views/slh_dsa.rs new file mode 100644 index 0000000..2417489 --- /dev/null +++ b/src/views/slh_dsa.rs @@ -0,0 +1,50 @@ +// 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 multi_codec::Codec; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl<'a> AttrView for View<'a> { + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + Ok(Codec::try_from(v.as_slice())?) + } + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl<'a> DataView for View<'a> { + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl<'a> ConvView for View<'a> { + fn to_ssh_signature(&self) -> Result { + Err(Error::UnsupportedAlgorithm( + "SLH-DSA (SPHINCS+) not supported in SSH signature format".into(), + )) + } +} diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs new file mode 100644 index 0000000..191097a --- /dev/null +++ b/tests/edge_case_tests.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Edge case tests for multi-sig + +use multi_codec::Codec; +use multi_sig::{Builder, Multisig, SIG_CODECS}; +use multi_trait::{Null, TryDecodeFrom}; + +/// Test null multisig +#[test] +fn test_null_multisig() { + let null_ms = Multisig::null(); + assert!(null_ms.is_null()); + assert_eq!(null_ms, Multisig::default()); +} + +/// Test all supported signature codecs +#[test] +fn test_all_signature_codecs() { + for &codec in SIG_CODECS.iter() { + let _builder = Builder::new(codec); + // Successfully creates builder for all supported codecs + } +} + +/// Test builder with minimal data +#[test] +fn test_minimal_data() { + let data = vec![0x42]; + let builder = Builder::new(Codec::Ed25519Pub); + let result = builder.with_signature_bytes(&data).try_build(); + // Should handle minimal data + assert!(result.is_ok()); +} + +/// Test multisig equality +#[test] +fn test_multisig_equality() { + let data = b"test data"; + + let builder1 = Builder::new(Codec::Ed25519Pub); + if let Ok(ms1) = builder1.with_signature_bytes(data).try_build() { + let builder2 = Builder::new(Codec::Ed25519Pub); + if let Ok(ms2) = builder2.with_signature_bytes(data).try_build() { + assert_eq!(ms1, ms2); + } + } +} + +/// Test binary roundtrip +#[test] +fn test_binary_roundtrip() { + let data = b"roundtrip test"; + + for &codec in SIG_CODECS.iter().take(3) { + let builder = Builder::new(codec); + if let Ok(ms1) = builder.with_signature_bytes(data).try_build() { + let bytes: Vec = ms1.clone().into(); + let ms2 = Multisig::try_from(bytes.as_ref()).unwrap(); + assert_eq!(ms1, ms2); + } + } +} + +/// Test Clone trait +#[test] +fn test_clone() { + let data = b"clone test"; + + let builder = Builder::new(Codec::Ed25519Pub); + if let Ok(ms1) = builder.with_signature_bytes(data).try_build() { + let ms2 = ms1.clone(); + assert_eq!(ms1, ms2); + } +} + +/// Test Send and Sync +#[test] +fn test_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + assert_send::(); +} + +/// Test with trailing data in decode +#[test] +fn test_decode_trailing_data() { + let data = b"trailing test"; + + let builder = Builder::new(Codec::Ed25519Pub); + if let Ok(ms1) = builder.with_signature_bytes(data).try_build() { + let mut bytes: Vec = ms1.clone().into(); + bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC]); + + let (ms2, remaining) = Multisig::try_decode_from(&bytes).unwrap(); + assert_eq!(ms1, ms2); + assert_eq!(remaining, &[0xAA, 0xBB, 0xCC]); + } +} diff --git a/tests/proptest_tests.rs b/tests/proptest_tests.rs new file mode 100644 index 0000000..d083999 --- /dev/null +++ b/tests/proptest_tests.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Property-based tests for multi-sig + +use multi_codec::Codec; +use multi_sig::{Builder, Multisig, SIG_CODECS}; +use multi_trait::TryDecodeFrom; +use proptest::prelude::*; + +/// Property: Multisig encoding and decoding should roundtrip +#[test] +fn test_multisig_roundtrip() { + proptest!(|(data in prop::collection::vec(any::(), 1..256))| { + for &codec in SIG_CODECS.iter().take(3) { + let builder = Builder::new(codec); + if let Ok(ms1) = builder.with_signature_bytes(&data).try_build() { + let bytes: Vec = ms1.clone().into(); + let (ms2, remaining) = Multisig::try_decode_from(&bytes).unwrap(); + + prop_assert_eq!(&ms1, &ms2); + prop_assert!(remaining.is_empty()); + } + } + }); +} + +/// Property: Same data should produce same signature structure +#[test] +fn test_deterministic() { + proptest!(|(data in prop::collection::vec(any::(), 1..128))| { + for &codec in SIG_CODECS.iter().take(2) { + let builder1 = Builder::new(codec); + if let Ok(ms1) = builder1.with_signature_bytes(&data).try_build() { + let builder2 = Builder::new(codec); + if let Ok(ms2) = builder2.with_signature_bytes(&data).try_build() { + prop_assert_eq!(&ms1, &ms2); + } + } + } + }); +} + +/// Property: Multisig equality is reflexive +#[test] +fn test_equality_reflexive() { + proptest!(|(data in prop::collection::vec(any::(), 1..256))| { + let builder = Builder::new(Codec::Ed25519Pub); + if let Ok(ms) = builder.with_signature_bytes(&data).try_build() { + prop_assert_eq!(&ms, &ms); + } + }); +} + +/// Property: Clone produces equal value +#[test] +fn test_clone_equality() { + proptest!(|(data in prop::collection::vec(any::(), 1..256))| { + let builder = Builder::new(Codec::Ed25519Pub); + if let Ok(ms1) = builder.with_signature_bytes(&data).try_build() { + let ms2 = ms1.clone(); + prop_assert_eq!(&ms1, &ms2); + } + }); +} diff --git a/tests/security_tests.rs b/tests/security_tests.rs new file mode 100644 index 0000000..a7a9b04 --- /dev/null +++ b/tests/security_tests.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Security-focused tests for multi-sig + +use multi_codec::Codec; +use multi_sig::{Builder, Error, Multisig}; + +/// Test that unsupported codec creates builder +#[test] +fn test_builder_creation() { + // Builder can be created with any codec + let _builder = Builder::new(Codec::Ed25519Pub); + // Success is creating the builder +} + +/// Test that empty signature data is handled +#[test] +fn test_empty_data_handling() { + let builder = Builder::new(Codec::Ed25519Pub); + let result = builder.with_signature_bytes(&[]).try_build(); + // Should handle empty data gracefully + let _ = result; +} + +/// Test malformed multisig data +#[test] +fn test_malformed_data() { + // Invalid varint + let invalid = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]; + let result = Multisig::try_from(invalid.as_ref()); + assert!(result.is_err()); +} + +/// Test truncated multisig +#[test] +fn test_truncated_data() { + let truncated = vec![0x39]; // Just sigil + let result = Multisig::try_from(truncated.as_ref()); + assert!(result.is_err()); +} + +/// Test empty bytes +#[test] +fn test_empty_bytes() { + let result = Multisig::try_from(&[] as &[u8]); + assert!(result.is_err()); +} + +/// Test concurrent signature creation +#[test] +fn test_concurrent_creation() { + use std::sync::Arc; + use std::thread; + + let data = Arc::new(b"concurrent test".to_vec()); + let mut handles = vec![]; + + for _ in 0..4 { + let data_clone = Arc::clone(&data); + let handle = thread::spawn(move || { + for _ in 0..5 { + let builder = Builder::new(Codec::Ed25519Pub); + let _ = builder + .with_signature_bytes(data_clone.as_ref()) + .try_build(); + } + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } +} + +/// Test error types are Send + Sync +#[test] +fn test_error_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); +} From 0989a35911e2629b2ccc394bf12172ee16258439 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 15:55:08 -0600 Subject: [PATCH 2/3] fix codecs Signed-off-by: Dave Grantham --- Cargo.toml | 4 +- benches/multisig_bench.rs | 2 +- src/ms.rs | 106 +++++++++++++++++++------------------- src/serde/de.rs | 2 +- src/types.rs | 2 +- 5 files changed, 58 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b3427d9..24f3e0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-sig" -version = "1.0.0" +version = "1.0.1" edition = "2021" authors = ["Dave Grantham "] description = "Multisig self-describing multicodec implementation for digital signatures" @@ -44,4 +44,4 @@ serde_test = "1.0" [[bench]] name = "multisig_bench" harness = false -path = "benches/multisig_bench.rs" \ No newline at end of file +path = "benches/multisig_bench.rs" diff --git a/benches/multisig_bench.rs b/benches/multisig_bench.rs index bafd98a..b117e1a 100644 --- a/benches/multisig_bench.rs +++ b/benches/multisig_bench.rs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 //! Performance benchmarks for multi-sig +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use multi_codec::Codec; use multi_sig::{Builder, Multisig, SIG_CODECS}; use multi_trait::TryDecodeFrom; -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use std::hint::black_box; /// Benchmark signature creation diff --git a/src/ms.rs b/src/ms.rs index 617a314..f2a1be8 100644 --- a/src/ms.rs +++ b/src/ms.rs @@ -29,20 +29,20 @@ pub const SIG_CODECS: [Codec; 35] = [ Codec::Es384Msig, Codec::Es521Msig, Codec::Rs256Msig, - Codec::SlhDsaSha2128FMsig, - Codec::SlhDsaSha2128SMsig, - Codec::SlhDsaSha2192FMsig, - Codec::SlhDsaSha2192SMsig, - Codec::SlhDsaSha2256FMsig, - Codec::SlhDsaSha2256SMsig, - Codec::SlhDsaShake128FMsig, - Codec::SlhDsaShake128SMsig, - Codec::SlhDsaShake192FMsig, - Codec::SlhDsaShake192SMsig, - Codec::SlhDsaShake256FMsig, - Codec::SlhDsaShake256SMsig, - Codec::MlDsa65Msig, - Codec::MlDsa87Msig, + Codec::SlhdsaSha2128FMsig, + Codec::SlhdsaSha2128SMsig, + Codec::SlhdsaSha2192FMsig, + Codec::SlhdsaSha2192SMsig, + Codec::SlhdsaSha2256FMsig, + Codec::SlhdsaSha2256SMsig, + Codec::SlhdsaShake128FMsig, + Codec::SlhdsaShake128SMsig, + Codec::SlhdsaShake192FMsig, + Codec::SlhdsaShake192SMsig, + Codec::SlhdsaShake256FMsig, + Codec::SlhdsaShake256SMsig, + Codec::Mldsa65Msig, + Codec::Mldsa87Msig, Codec::FnDsa512Msig, Codec::FnDsa1024Msig, Codec::Mayo1Msig, @@ -221,19 +221,19 @@ impl Views for Multisig { Ok(Box::new(nist_p::View::try_from(self)?)) } Codec::Rs256Msig => Ok(Box::new(rsa::View::try_from(self)?)), - Codec::SlhDsaSha2128FMsig - | Codec::SlhDsaSha2128SMsig - | Codec::SlhDsaSha2192FMsig - | Codec::SlhDsaSha2192SMsig - | Codec::SlhDsaSha2256FMsig - | Codec::SlhDsaSha2256SMsig - | Codec::SlhDsaShake128FMsig - | Codec::SlhDsaShake128SMsig - | Codec::SlhDsaShake192FMsig - | Codec::SlhDsaShake192SMsig - | Codec::SlhDsaShake256FMsig - | Codec::SlhDsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Msig | Codec::MlDsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), + Codec::SlhdsaSha2128FMsig + | Codec::SlhdsaSha2128SMsig + | Codec::SlhdsaSha2192FMsig + | Codec::SlhdsaSha2192SMsig + | Codec::SlhdsaSha2256FMsig + | Codec::SlhdsaSha2256SMsig + | Codec::SlhdsaShake128FMsig + | Codec::SlhdsaShake128SMsig + | Codec::SlhdsaShake192FMsig + | Codec::SlhdsaShake192SMsig + | Codec::SlhdsaShake256FMsig + | Codec::SlhdsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Msig | Codec::Mldsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), Codec::FnDsa512Msig | Codec::FnDsa1024Msig => { Ok(Box::new(fn_dsa::View::try_from(self)?)) } @@ -263,19 +263,19 @@ impl Views for Multisig { Ok(Box::new(nist_p::View::try_from(self)?)) } Codec::Rs256Msig => Ok(Box::new(rsa::View::try_from(self)?)), - Codec::SlhDsaSha2128FMsig - | Codec::SlhDsaSha2128SMsig - | Codec::SlhDsaSha2192FMsig - | Codec::SlhDsaSha2192SMsig - | Codec::SlhDsaSha2256FMsig - | Codec::SlhDsaSha2256SMsig - | Codec::SlhDsaShake128FMsig - | Codec::SlhDsaShake128SMsig - | Codec::SlhDsaShake192FMsig - | Codec::SlhDsaShake192SMsig - | Codec::SlhDsaShake256FMsig - | Codec::SlhDsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Msig | Codec::MlDsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), + Codec::SlhdsaSha2128FMsig + | Codec::SlhdsaSha2128SMsig + | Codec::SlhdsaSha2192FMsig + | Codec::SlhdsaSha2192SMsig + | Codec::SlhdsaSha2256FMsig + | Codec::SlhdsaSha2256SMsig + | Codec::SlhdsaShake128FMsig + | Codec::SlhdsaShake128SMsig + | Codec::SlhdsaShake192FMsig + | Codec::SlhdsaShake192SMsig + | Codec::SlhdsaShake256FMsig + | Codec::SlhdsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Msig | Codec::Mldsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), Codec::FnDsa512Msig | Codec::FnDsa1024Msig => { Ok(Box::new(fn_dsa::View::try_from(self)?)) } @@ -305,19 +305,19 @@ impl Views for Multisig { Ok(Box::new(nist_p::View::try_from(self)?)) } Codec::Rs256Msig => Ok(Box::new(rsa::View::try_from(self)?)), - Codec::SlhDsaSha2128FMsig - | Codec::SlhDsaSha2128SMsig - | Codec::SlhDsaSha2192FMsig - | Codec::SlhDsaSha2192SMsig - | Codec::SlhDsaSha2256FMsig - | Codec::SlhDsaSha2256SMsig - | Codec::SlhDsaShake128FMsig - | Codec::SlhDsaShake128SMsig - | Codec::SlhDsaShake192FMsig - | Codec::SlhDsaShake192SMsig - | Codec::SlhDsaShake256FMsig - | Codec::SlhDsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Msig | Codec::MlDsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), + Codec::SlhdsaSha2128FMsig + | Codec::SlhdsaSha2128SMsig + | Codec::SlhdsaSha2192FMsig + | Codec::SlhdsaSha2192SMsig + | Codec::SlhdsaSha2256FMsig + | Codec::SlhdsaSha2256SMsig + | Codec::SlhdsaShake128FMsig + | Codec::SlhdsaShake128SMsig + | Codec::SlhdsaShake192FMsig + | Codec::SlhdsaShake192SMsig + | Codec::SlhdsaShake256FMsig + | Codec::SlhdsaShake256SMsig => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Msig | Codec::Mldsa87Msig => Ok(Box::new(ml_dsa::View::try_from(self)?)), Codec::FnDsa512Msig | Codec::FnDsa1024Msig => { Ok(Box::new(fn_dsa::View::try_from(self)?)) } diff --git a/src/serde/de.rs b/src/serde/de.rs index c0d9977..99457ef 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -3,9 +3,9 @@ use crate::{ ms::{self, Attributes}, AttrId, Multisig, }; +use core::fmt; use multi_codec::Codec; use multi_util::EncodedVarbytes; -use core::fmt; use serde::{ de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, diff --git a/src/types.rs b/src/types.rs index 738b1de..fbbe3c1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 //! Type-safe wrappers for signature components -use multi_codec::Codec; use core::fmt; +use multi_codec::Codec; /// A cryptographic signature /// From 1643688f02fc05465ff8d865adae0ee03ba8e17d Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 16:09:48 -0600 Subject: [PATCH 3/3] update deps Signed-off-by: Dave Grantham --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 24f3e0e..ba2afe6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-sig" -version = "1.0.1" +version = "1.0.2" edition = "2021" authors = ["Dave Grantham "] description = "Multisig self-describing multicodec implementation for digital signatures" @@ -14,12 +14,12 @@ categories = ["cryptography", "encoding"] default = ["serde"] [dependencies] -# blsful configured per-target below (blst for native, rust for wasm) -multi-base = { version = "1.0", path = "../multi-base" } -multi-codec = { version = "1.0", path = "../multi-codec" } -multi-trait = { version = "1.0", path = "../multi-trait" } -multi-util = { version = "1.0", path = "../multi-util" } elliptic-curve = "0.14" +# blsful configured per-target below (blst for native, rust for wasm) +multi-base = "1.0" +multi-codec = "1.0" +multi-trait = "1.0" +multi-util = "1.0" serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } ssh-encoding = "0.3" thiserror = { version = "2.0" }