Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
50 changes: 27 additions & 23 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
[package]
name = "multisig"
version = "1.0.4"
name = "multi-sig"
version = "1.0.2"
edition = "2021"
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]
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"
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" }
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"
120 changes: 120 additions & 0 deletions benches/multisig_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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);
4 changes: 2 additions & 2 deletions src/attrid.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
76 changes: 65 additions & 11 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)]
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -133,7 +133,7 @@ pub enum ConversionsError {
}

/// SSH Errors
#[derive(Clone, Debug)]
#[derive(Debug)]
pub enum SshError {
/// SSH Sig
Sig(ssh_key::Error),
Expand Down Expand Up @@ -163,3 +163,57 @@ impl From<ssh_encoding::LabelError> 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<T: Send>() {}
fn assert_sync<T: Sync>() {}

assert_send::<Error>();
assert_sync::<Error>();
}
}
Loading
Loading