From 7e689913c41f7fa55cee478a408b049ec1730595 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Fri, 17 Jul 2026 01:08:56 -0600 Subject: [PATCH 1/3] hardening Signed-off-by: Dave Grantham --- .github/workflows/rust.yml | 103 +++++++++++------ Cargo.toml | 13 ++- SECURITY.md | 70 +++++++++++- benches/multikey_bench.rs | 3 +- src/attrid.rs | 2 +- src/cipher.rs | 4 +- src/error.rs | 16 ++- src/kdf.rs | 4 +- src/keysplit.rs | 28 ++--- src/lib.rs | 50 +++++++-- src/mk.rs | 170 +++++++++++++++++++---------- src/nonce.rs | 4 +- src/serde/de.rs | 5 +- src/serde/mod.rs | 4 +- src/serde/ser.rs | 2 +- src/views.rs | 6 +- src/views/aead.rs | 2 +- src/views/bcrypt.rs | 2 +- src/views/bls12381.rs | 26 ++--- src/views/bls12381_g1_fndsa512.rs | 23 ++-- src/views/bls12381_g1_mayo1.rs | 19 ++-- src/views/bls12381_g1_mayo2.rs | 19 ++-- src/views/bls12381_g1_mldsa65.rs | 21 ++-- src/views/bls12381_hybrid.rs | 2 +- src/views/chacha20.rs | 8 +- src/views/classic_mceliece.rs | 28 ++--- src/views/dkg_threshold.rs | 2 +- src/views/ed25519.rs | 8 +- src/views/ed25519_fndsa512.rs | 23 ++-- src/views/ed25519_mayo2.rs | 19 ++-- src/views/ed25519_mldsa65.rs | 21 ++-- src/views/fn_dsa.rs | 12 +- src/views/frodokem.rs | 8 +- src/views/frodokem_helper.rs | 2 +- src/views/mayo.rs | 19 ++-- src/views/ml_dsa.rs | 10 +- src/views/ml_kem.rs | 30 ++--- src/views/nist_p.rs | 35 +++--- src/views/rsa.rs | 13 ++- src/views/secp256k1.rs | 23 ++-- src/views/slh_dsa.rs | 8 +- src/views/sntrup.rs | 28 ++--- src/views/threshold_marker.rs | 2 +- src/views/threshold_meta.rs | 8 +- src/views/x25519.rs | 39 ++++--- src/views/x25519_frodokem640.rs | 6 +- src/views/x25519_mceliece348864.rs | 6 +- src/views/x25519_mlkem768.rs | 41 +++---- src/views/x25519_sntrup761.rs | 39 ++++--- tests/edge_case_tests.rs | 3 +- tests/proptest_tests.rs | 2 +- tests/security_tests.rs | 5 +- 52 files changed, 649 insertions(+), 397 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 46c7d29..677e783 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,11 +1,11 @@ -name: Rust +name: Rust on: workflow_dispatch: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always @@ -37,39 +37,74 @@ jobs: with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} + components: rustfmt, clippy - run: ${{ matrix.deps }} - - run: cargo build --target ${{ matrix.target }} --release --all-features + - name: Check Code Format + run: cargo fmt --all -- --check + - name: Code Lint + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Build + run: cargo build --target ${{ matrix.target }} --release --all-features wasm_builds: - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - 1.73.0 # MSRV - - stable - target: - - wasm32-wasi - - wasm32-unknown-unknown - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ matrix.rust }} - target: ${{ matrix.target }} - - run: cargo build --target ${{ matrix.target }} --features wasm + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.85.0 # MSRV + - stable + target: + - wasm32-wasi + - wasm32-unknown-unknown + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + - run: cargo build --target ${{ matrix.target }} --features wasm test: - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - 1.73.0 # MSRV - - stable - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ matrix.rust }} - - run: cargo test --verbose --release - - run: cargo test --all-features # debug build - - run: cargo test --release --all-features --all-targets + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.85.0 # MSRV + - stable + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + - run: cargo test --verbose --release + - run: cargo test --all-features # debug build + - run: cargo test --release --all-features --all-targets + + msrv: + name: Verify MSRV (1.85) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust Toolchain (1.85) + uses: dtolnay/rust-toolchain@1.85.0 + + - name: Check + run: cargo check --all-features + + audit: + name: Security Audit + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust Toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-audit + run: cargo install cargo-audit + + - name: Run cargo audit + run: cargo audit \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c2d4f8f..956b568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "multi-key" -version = "1.0.7" -edition = "2021" +version = "1.0.8" +edition = "2024" +rust-version = "1.85" authors = ["Dave Grantham "] description = "Multikey self-describing cryptographic key data" repository = "https://github.com/cryptidtech/multi-key.git" @@ -98,3 +99,11 @@ serde_test = "1.0" name = "multikey_bench" harness = false path = "benches/multikey_bench.rs" + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } + +[lints.rust] +unsafe_code = "deny" diff --git a/SECURITY.md b/SECURITY.md index 6f2de83..9ad144f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,4 +8,72 @@ ## Reporting a Vulnerability -Report any vulnerabilities by either emailing dwg@linuxprogrammer.org or redmike7@gmail.com. DO NOT file public issues in this repo. +Report any vulnerabilities by either emailing dwg@linuxprogrammer.org or +redmike7@gmail.com. DO NOT file public issues in this repo. + +## std-only Status + +This crate is **std-only**. It depends on `std::collections::BTreeMap`, +`std::fmt`, and `unsigned-varint` with the `std` feature. The crypto +dependency stack (RSA, SSH, BLS, hybrid KEMs, post-quantum signature +schemes) all require std. A full `no_std` conversion is infeasible given +the crypto dependency stack; this decision is final for the foreseeable +future. + +## Release-Candidate Dependencies + +This crate depends on the following release-candidate (RC) crates: + +- `rsa = "0.10.0-rc.18"` — RSA signature scheme +- `slh-dsa = "0.2.0-rc.5"` — SLH-DSA post-quantum signatures +- `vsss-rs = "6.0.0-rc2"` — verifiable secret sharing (transitive via + `blsful`) +- `blsful = "4.0.0-rc1"` — BLS12-381 signature implementation +- `ssh-key = "0.7.0-rc.11"` — SSH key/signature encoding + +These are pinned to RC versions because stable releases are not yet +available. This is a **tracked acceptance**: the RC versions are reviewed +on each release and will be upgraded to stable when available. Consumers +should be aware that RC APIs may change before stabilisation. Coordinate +with `multi-sig` (which depends on the same `blsful` and `ssh-key` +versions) when upgrading. + +## Comment Field Zeroization (R6) + +The `Multikey` comment field is stored as a plain `String` and is **not** +zeroized on drop. This is a deliberate design decision: + +- **Rationale:** The comment is non-sensitive metadata (e.g. a key label + or human-readable description). Wrapping it in `Zeroizing` would + require deref-coercion shims across ~120 call sites that read the + comment, adding complexity and friction for no security benefit when the + comment does not contain sensitive material. +- **Key material is zeroized:** The actual key material in `attributes` is + wrapped in `Zeroizing>` and is zeroized on drop. +- **Caller responsibility:** If a caller places sensitive material in the + comment field, they must zeroize that material themselves before it + leaves scope. The crate does not assume the comment is sensitive. + +## Decoded-Size Caps + +The decoder enforces the following caps on untrusted wire data to mitigate +CWE-400 (Uncontrolled Resource Consumption): + +- **`MAX_ATTRIBUTES = 256`** — maximum number of attributes per + `Multikey`. +- **`MAX_DECODED_SIZE = 16 MiB`** — maximum total decoded bytes per + `Multikey`. Tracked across the attribute decode loop. +- Per-attribute `Varbytes` payloads are individually capped by + `multi_util::varbytes::MAX_DECODED_SIZE` (16 MiB). + +Exceeding any cap returns a clean `Err` (`Error::TooManyAttributes` or +`Error::InputTooLarge`); the decoder never panics on oversized input. + +## Memory Safety + +- **No unsafe code**: `#![deny(unsafe_code)]` is enforced at compile time. +- **Key material zeroization**: Private key bytes are wrapped in + `Zeroizing>` and zeroized on drop. +- **Constant-time comparison**: `impl ConstantTimeEq for Multikey` compares + the canonical wire encoding in constant time. Use `mk.ct_eq(&other)` in + timing-sensitive contexts instead of `PartialEq`. \ No newline at end of file diff --git a/benches/multikey_bench.rs b/benches/multikey_bench.rs index 9dc2212..70f3b42 100644 --- a/benches/multikey_bench.rs +++ b/benches/multikey_bench.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 //! Performance benchmarks for multi-key +#![allow(clippy::semicolon_if_nothing_returned)] -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use multi_codec::Codec; use multi_key::{Builder, Multikey}; use std::hint::black_box; diff --git a/src/attrid.rs b/src/attrid.rs index cbe87f0..a71b688 100644 --- a/src/attrid.rs +++ b/src/attrid.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{error::AttributesError, Error}; +use crate::{Error, error::AttributesError}; use multi_trait::{EncodeInto, TryDecodeFrom}; use std::fmt; diff --git a/src/cipher.rs b/src/cipher.rs index 28d02cd..935e1dc 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{error::CipherError, mk::Attributes, AttrId, Error, Multikey}; +use crate::{AttrId, Error, Multikey, error::CipherError, mk::Attributes}; use multi_codec::Codec; use rand_core::CryptoRng; use zeroize::Zeroizing; @@ -82,7 +82,7 @@ impl Builder { #[cfg(test)] mod tests { use super::*; - use crate::{kdf, mk, Views}; + use crate::{Views, kdf, mk}; #[test] fn test_chacha20() { diff --git a/src/error.rs b/src/error.rs index cb821f2..213d70b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -62,12 +62,25 @@ pub enum Error { DuplicateAttribute(u8), /// Attribute count exceeds the configured maximum /// - /// Returned by [`crate::mk::Multikey::try_decode_from`] when the number of + /// Returned by `Multikey::try_decode_from` when the number of /// attributes declared in the wire data exceeds /// [`crate::mk::MAX_ATTRIBUTES`]. Bounds the work a crafted input can /// force the decoder to perform and mitigates CWE-400. #[error("attribute count {0} exceeds maximum {1}")] TooManyAttributes(usize, usize), + /// Decoded size exceeds the configured maximum + /// + /// Returned by `Multikey::try_decode_from` when the total decoded byte + /// count exceeds [`crate::mk::MAX_DECODED_SIZE`]. Bounds the worst-case + /// allocation for untrusted wire data and mitigates CWE-400 (Uncontrolled + /// Resource Consumption). + #[error("decoded size {claimed} exceeds maximum {max}")] + InputTooLarge { + /// The number of bytes claimed by the wire data + claimed: usize, + /// The configured maximum decoded size + max: usize, + }, /// Incorrect Multikey sigil #[error("Missing Multikey sigil")] MissingSigil, @@ -408,6 +421,7 @@ impl Error { Self::Utf8(_) => "Utf8", Self::DuplicateAttribute(_) => "DuplicateAttribute", Self::TooManyAttributes(_, _) => "TooManyAttributes", + Self::InputTooLarge { .. } => "InputTooLarge", Self::MissingSigil => "MissingSigil", Self::UnsupportedAlgorithm(_) => "UnsupportedAlgorithm", } diff --git a/src/kdf.rs b/src/kdf.rs index 6896f63..6f6c3c4 100644 --- a/src/kdf.rs +++ b/src/kdf.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{error::KdfError, mk::Attributes, AttrId, Error, Multikey}; +use crate::{AttrId, Error, Multikey, error::KdfError, mk::Attributes}; use multi_codec::Codec; use multi_util::Varuint; use rand_core::CryptoRng; @@ -90,7 +90,7 @@ impl Builder { #[cfg(test)] mod tests { use super::*; - use crate::{cipher, Views}; + use crate::{Views, cipher}; #[test] fn test_bcrypt() { diff --git a/src/keysplit.rs b/src/keysplit.rs index 56fd793..e5392c4 100644 --- a/src/keysplit.rs +++ b/src/keysplit.rs @@ -30,8 +30,8 @@ use multi_util::CodecInfo; use rand_core::CryptoRng; use serde::{Deserialize, Serialize}; use vsss_rs::{ - feldman, DefaultShare, FeldmanVerifierSet, Gf256, IdentifierPrimeField, ReadableShareSet, - ShareVerifierGroup, ValueGroup, + DefaultShare, FeldmanVerifierSet, Gf256, IdentifierPrimeField, ReadableShareSet, + ShareVerifierGroup, ValueGroup, feldman, }; use zeroize::Zeroizing; @@ -515,7 +515,7 @@ mod tests { use super::*; use crate::mk; - fn gen(codec: Codec) -> Multikey { + fn gen_key(codec: Codec) -> Multikey { Builder::new_from_random_bytes(codec, &mut rand::rng()) .unwrap_or_else(|e| panic!("{codec:?} keygen: {e}")) .try_build() @@ -530,7 +530,7 @@ mod tests { /// reconstructed key is byte-identical to the original. Run for both 2-of-3 /// and 3-of-5 with disjoint/non-contiguous subsets. fn assert_roundtrip(codec: Codec) { - let mk = gen(codec); + let mk = gen_key(codec); let original = secret(&mk); // 2-of-3 @@ -634,7 +634,7 @@ mod tests { #[test] fn ed25519_x25519_carry_verifiable_dual() { for &codec in &[Codec::Ed25519Priv, Codec::X25519Priv] { - let shares = split(&gen(codec), 2, 4, rand::rng()).unwrap(); + let shares = split(&gen_key(codec), 2, 4, rand::rng()).unwrap(); for s in &shares { let p = unwrap_share(s).unwrap(); assert_eq!(p.scheme, Scheme::Gf256, "{codec:?} primary is gf256 seed"); @@ -654,7 +654,7 @@ mod tests { Codec::Bls12381G1Priv, Codec::Bls12381G2Priv, ] { - let shares = split(&gen(codec), 2, 3, rand::rng()).unwrap(); + let shares = split(&gen_key(codec), 2, 3, rand::rng()).unwrap(); let p = unwrap_share(&shares[0]).unwrap(); assert_eq!(p.scheme, Scheme::Feldman, "{codec:?}"); assert!(p.verifiers.is_some(), "{codec:?} has Feldman commitments"); @@ -664,7 +664,7 @@ mod tests { // ── Serialization: a KeySplitShare is a Multikey; CBOR + JSON round-trip ── #[test] fn share_survives_cbor_and_json() { - let mk = gen(Codec::P256Priv); + let mk = gen_key(Codec::P256Priv); let original = secret(&mk); let shares = split(&mk, 2, 3, rand::rng()).unwrap(); @@ -700,7 +700,7 @@ mod tests { // ── Negative tests ── #[test] fn tampered_feldman_share_fails_verify() { - let mk = gen(Codec::P256Priv); + let mk = gen_key(Codec::P256Priv); let shares = split(&mk, 2, 3, rand::rng()).unwrap(); let mut p = unwrap_share(&shares[0]).unwrap(); p.value[0] ^= 0xff; @@ -713,7 +713,7 @@ mod tests { #[test] fn tampered_dual_fails_verify() { - let mk = gen(Codec::Ed25519Priv); + let mk = gen_key(Codec::Ed25519Priv); let shares = split(&mk, 2, 3, rand::rng()).unwrap(); let mut p = unwrap_share(&shares[0]).unwrap(); p.dual.as_mut().unwrap().value[0] ^= 0xff; @@ -723,7 +723,7 @@ mod tests { #[test] fn below_threshold_does_not_recover() { - let mk = gen(Codec::P256Priv); + let mk = gen_key(Codec::P256Priv); let original = secret(&mk); let shares = split(&mk, 3, 5, rand::rng()).unwrap(); if let Ok(got) = combine(&shares[0..2]) { @@ -733,14 +733,14 @@ mod tests { #[test] fn mixed_and_empty_and_public_rejected() { - let a = split(&gen(Codec::P256Priv), 2, 3, rand::rng()).unwrap(); - let b = split(&gen(Codec::Secp256K1Priv), 2, 3, rand::rng()).unwrap(); + let a = split(&gen_key(Codec::P256Priv), 2, 3, rand::rng()).unwrap(); + let b = split(&gen_key(Codec::Secp256K1Priv), 2, 3, rand::rng()).unwrap(); assert!( combine(&[a[0].clone(), b[0].clone()]).is_err(), "mixed codecs" ); assert!(combine(&[]).is_err(), "empty set"); - let pk = gen(Codec::P256Priv) + let pk = gen_key(Codec::P256Priv) .conv_view() .unwrap() .to_public_key() @@ -750,7 +750,7 @@ mod tests { #[test] fn invalid_params_rejected() { - let mk = gen(Codec::P256Priv); + let mk = gen_key(Codec::P256Priv); assert!(split(&mk, 1, 3, rand::rng()).is_err(), "threshold < 2"); assert!(split(&mk, 4, 3, rand::rng()).is_err(), "threshold > limit"); } diff --git a/src/lib.rs b/src/lib.rs index 2cf894c..8576555 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,41 @@ unused_import_braces, unused_qualifications )] +// Pedantic/nursery lints are enabled at the workspace level via +// `[lints.clippy]` in Cargo.toml. The following allows suppress stylistic +// lints that would require large-scale churn for minimal security benefit. +#![allow( + clippy::doc_markdown, + clippy::elidable_lifetime_names, + clippy::use_self, + clippy::missing_errors_doc, + clippy::missing_panics_doc, + clippy::must_use_candidate, + clippy::return_self_not_must_use, + clippy::semicolon_if_nothing_returned, + clippy::or_fun_call, + clippy::missing_const_for_fn, + clippy::multiple_crate_versions, + clippy::too_many_lines, + clippy::option_if_let_else, + clippy::single_match_else, + clippy::uninlined_format_args, + clippy::cast_possible_truncation, + clippy::redundant_pub_crate, + clippy::redundant_clone, + clippy::items_after_statements, + clippy::if_not_else, + clippy::explicit_iter_loop, + clippy::enum_glob_use, + clippy::branches_sharing_code, + clippy::too_long_first_doc_paragraph, + clippy::useless_let_if_seq, + clippy::large_stack_frames, + clippy::match_same_arms, + clippy::similar_names, + clippy::single_char_pattern, + clippy::needless_pass_by_value +)] /// Errors produced by this library pub mod error; @@ -96,13 +131,13 @@ pub mod kdf; /// Key views pub mod views; pub use views::threshold_marker::{ - self, group_public_key, participants, set_group_public_key, set_participants, threshold_kind, - threshold_params, MarkerView, ThresholdParticipant, ThresholdScheme, + self, MarkerView, ThresholdParticipant, ThresholdScheme, group_public_key, participants, + set_group_public_key, set_participants, threshold_kind, threshold_params, }; pub use views::threshold_meta::{ - self, decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, read_threshold_params, - stamp_disclosure_attrs, DisclosureView, ThresholdDisclosure, ThresholdMetaCipher, - ThresholdMetadata, + self, DisclosureView, ThresholdDisclosure, ThresholdMetaCipher, ThresholdMetadata, + decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, read_threshold_params, + stamp_disclosure_attrs, }; pub use views::{ AttrView, CipherAttrView, CipherView, ConvView, DataView, FingerprintView, KdfAttrView, @@ -116,8 +151,9 @@ pub mod keysplit; /// Multikey type and functions pub mod mk; pub use mk::{ - Builder, EncodedMultikey, Multikey, FN_DSA_KEY_CODECS, KEY_CODECS, KEY_SHARE_CODECS, - MAX_ATTRIBUTES, ML_DSA_KEY_CODECS, ML_KEM_KEY_CODECS, SLH_DSA_KEY_CODECS, X25519_KEY_CODECS, + Builder, EncodedMultikey, FN_DSA_KEY_CODECS, KEY_CODECS, KEY_SHARE_CODECS, MAX_ATTRIBUTES, + MAX_DECODED_SIZE, ML_DSA_KEY_CODECS, ML_KEM_KEY_CODECS, Multikey, SLH_DSA_KEY_CODECS, + X25519_KEY_CODECS, }; /// Nonce type diff --git a/src/mk.rs b/src/mk.rs index 3781e0e..b1f501a 100644 --- a/src/mk.rs +++ b/src/mk.rs @@ -1,5 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ + AttrId, AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, KdfView, OpenView, SealView, SignView, ThresholdAttrView, ThresholdDisclosureView, + ThresholdKeyView, ThresholdView, VerifyView, Views, error::{AttributesError, CipherError, ConversionsError, KdfError, SealError}, views::{ bcrypt, bls12381, bls12381_g1_fndsa512, bls12381_g1_mayo1, bls12381_g1_mayo2, @@ -8,26 +11,23 @@ use crate::{ sntrup, threshold_meta, x25519, x25519_frodokem640, x25519_mceliece348864, x25519_mlkem768, x25519_sntrup761, }, - AttrId, AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, KdfView, OpenView, SealView, SignView, ThresholdAttrView, ThresholdDisclosureView, - ThresholdKeyView, ThresholdView, VerifyView, Views, }; use ::fn_dsa::{ - sign_key_size, vrfy_key_size, KeyPairGenerator, KeyPairGeneratorStandard, FN_DSA_LOGN_1024, - FN_DSA_LOGN_512, + FN_DSA_LOGN_512, FN_DSA_LOGN_1024, KeyPairGenerator, KeyPairGeneratorStandard, sign_key_size, + vrfy_key_size, }; -use elliptic_curve::sec1::ToSec1Point; use elliptic_curve::Generate; +use elliptic_curve::sec1::ToSec1Point; use multi_base::Base; use multi_codec::Codec; use multi_trait::{EncodeInto, Null, TryDecodeFrom}; use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, Varuint}; use rand_core::CryptoRng; use ssh_key::{ + EcdsaCurve, PrivateKey, PublicKey, private::{EcdsaKeypair, KeypairData}, public::{EcdsaPublicKey, KeyData}, - EcdsaCurve, PrivateKey, PublicKey, }; use std::{collections::BTreeMap, fmt}; use subtle::ConstantTimeEq; @@ -137,6 +137,16 @@ pub const SIGIL: Codec = Codec::Multikey; /// input can force the decoder to perform (mitigates CWE-400). pub const MAX_ATTRIBUTES: usize = 256; +/// Maximum total decoded size (in bytes) a single [`Multikey`] will accept +/// when decoding from untrusted wire data. +/// +/// The 16 MiB ceiling comfortably exceeds every legitimate multikey payload +/// in this stack while bounding the worst-case allocation an attacker can +/// trigger with a crafted length prefix. Each `Varbytes` attribute payload +/// is also individually capped by [`multi_util::varbytes::MAX_DECODED_SIZE`] +/// via the `Varbytes::try_decode_from` path. Mitigates CWE-400. +pub const MAX_DECODED_SIZE: usize = 16 * 1024 * 1024; + /// A base encoded Multikey structure pub type EncodedMultikey = BaseEncoded; @@ -242,6 +252,9 @@ impl<'a> TryDecodeFrom<'a> for Multikey { type Error = Error; fn try_decode_from(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> { + // Track total consumed bytes to enforce MAX_DECODED_SIZE (CWE-400). + let start_len = bytes.len(); + // decode the sigil let (sigil, ptr) = Codec::try_decode_from(bytes)?; if sigil != SIGIL { @@ -268,9 +281,20 @@ impl<'a> TryDecodeFrom<'a> for Multikey { for _ in 0..*num_attr { let (id, ptr) = AttrId::try_decode_from(p)?; let (attr, ptr) = Varbytes::try_decode_from(ptr)?; + // Per-attribute size is already capped by Varbytes' + // MAX_DECODED_SIZE (16 MiB). The total decoded-size cap + // below provides a second layer of protection. if attributes.insert(id, (*attr).clone().into()).is_some() { return Err(Error::DuplicateAttribute(id.code())); } + // Enforce total decoded size cap + let consumed = start_len - ptr.len(); + if consumed > MAX_DECODED_SIZE { + return Err(Error::InputTooLarge { + claimed: consumed, + max: MAX_DECODED_SIZE, + }); + } p = ptr; } (attributes, p) @@ -1610,7 +1634,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1629,7 +1653,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1648,11 +1672,11 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = key_share.0 .0.to_be_bytes().into(); + let identifier: Vec = key_share.0.0.to_be_bytes().into(); let threshold: Vec = Varuint(key_share.1).into(); let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); @@ -1674,7 +1698,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1693,11 +1717,11 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = key_share.0 .0.to_be_bytes().into(); + let identifier: Vec = key_share.0.0.to_be_bytes().into(); let threshold: Vec = Varuint(key_share.1).into(); let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); @@ -1720,7 +1744,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1739,7 +1763,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1759,7 +1783,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1778,7 +1802,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1798,7 +1822,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1817,7 +1841,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1836,7 +1860,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1855,7 +1879,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1874,7 +1898,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1893,7 +1917,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1912,7 +1936,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1931,7 +1955,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1950,7 +1974,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1969,7 +1993,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -1988,7 +2012,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2007,7 +2031,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2028,7 +2052,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2105,7 +2129,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2124,7 +2148,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2143,11 +2167,11 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = key_share.0 .0.to_be_bytes().into(); + let identifier: Vec = key_share.0.0.to_be_bytes().into(); let threshold: Vec = Varuint(key_share.1).into(); let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); @@ -2169,7 +2193,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2188,11 +2212,11 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = key_share.0 .0.to_be_bytes().into(); + let identifier: Vec = key_share.0.0.to_be_bytes().into(); let threshold: Vec = Varuint(key_share.1).into(); let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); @@ -2215,7 +2239,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2234,7 +2258,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2254,7 +2278,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2273,7 +2297,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2293,7 +2317,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2312,7 +2336,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2331,7 +2355,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2350,7 +2374,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2369,7 +2393,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2388,7 +2412,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2407,7 +2431,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2426,7 +2450,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2445,7 +2469,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2464,7 +2488,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2483,7 +2507,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2502,7 +2526,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -2523,7 +2547,7 @@ impl Builder { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), ) - .into()) + .into()); } }; let mut attributes = Attributes::new(); @@ -3578,4 +3602,40 @@ mod tests { assert!(kd.key_bytes().is_ok()); assert!(kd.secret_bytes().is_ok()); } + + #[test] + fn test_too_many_attributes_rejected() { + use multi_trait::EncodeInto; + // Craft a multikey that claims more than MAX_ATTRIBUTES attributes. + let mut bad = Vec::new(); + let sigil_bytes: Vec = Codec::Multikey.into(); + bad.extend(sigil_bytes); // sigil + let codec_bytes: Vec = Codec::Ed25519Priv.into(); + bad.extend(codec_bytes); // codec + let msg = Varbytes::new(Vec::new()); + bad.extend(msg.encode_into()); // empty comment + bad.extend(Varuint(MAX_ATTRIBUTES + 1).encode_into()); // too many attrs + + let result = Multikey::try_from(bad.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + Error::TooManyAttributes(n, max) => { + assert_eq!(n, MAX_ATTRIBUTES + 1); + assert_eq!(max, MAX_ATTRIBUTES); + } + e => panic!("Expected TooManyAttributes, got: {e:?}"), + } + } + + #[test] + fn test_valid_roundtrip_with_caps() { + // Sanity: a well-formed multikey still round-trips with the caps. + let mk = Builder::new(Codec::Ed25519Pub) + .with_key_bytes(&[0u8; 32]) + .try_build() + .unwrap(); + let v: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(v.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } } diff --git a/src/nonce.rs b/src/nonce.rs index 6a96bfa..37a3db1 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{error::NonceError, Error}; +use crate::{Error, error::NonceError}; use core::fmt; use multi_base::Base; use multi_codec::Codec; @@ -168,7 +168,7 @@ impl Builder { #[cfg(test)] mod tests { use super::*; - use crate::{mk, Views}; + use crate::{Views, mk}; #[test] fn test_random() { diff --git a/src/serde/de.rs b/src/serde/de.rs index 37f4cdf..92f8c6c 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,14 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ + AttrId, Multikey, Nonce, mk::{self, Attributes}, - nonce, AttrId, Multikey, Nonce, + nonce, }; use core::fmt; use multi_codec::Codec; use multi_util::EncodedVarbytes; use serde::{ - de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, + de::{Error, MapAccess, Visitor}, }; use zeroize::Zeroizing; diff --git a/src/serde/mod.rs b/src/serde/mod.rs index c96a0b9..9e780e8 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -5,14 +5,14 @@ mod ser; #[cfg(test)] mod tests { - use crate::{cipher, kdf, nonce, Builder, EncodedMultikey, Multikey, Views}; + use crate::{Builder, EncodedMultikey, Multikey, Views, cipher, kdf, nonce}; use multi_base::Base; use multi_codec::Codec; use multi_hash::EncodedMultihash; use multi_trait::Null; use multi_util::BaseEncoded; use serde::{Deserialize, Serialize}; - use serde_test::{assert_tokens, Configure, Token}; + use serde_test::{Configure, Token, assert_tokens}; use std::collections::BTreeMap; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 6ebab92..c1e2940 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{mk, nonce, AttrId, Multikey, Nonce}; +use crate::{AttrId, Multikey, Nonce, mk, nonce}; use multi_util::{EncodedVarbytes, EncodingInfo, Varbytes}; use serde::ser::{self, SerializeStruct}; diff --git a/src/views.rs b/src/views.rs index 1cb91d3..cbf1172 100644 --- a/src/views.rs +++ b/src/views.rs @@ -36,9 +36,9 @@ pub mod threshold_marker; /// Threshold disclosure modes and encrypted metadata helpers. pub mod threshold_meta; pub use threshold_meta::{ - decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, read_threshold_params, - stamp_disclosure_attrs, DisclosureView, ThresholdDisclosure, ThresholdDisclosureView, - ThresholdMetaCipher, ThresholdMetadata, + DisclosureView, ThresholdDisclosure, ThresholdDisclosureView, ThresholdMetaCipher, + ThresholdMetadata, decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, + read_threshold_params, stamp_disclosure_attrs, }; pub(crate) mod x25519; pub(crate) mod x25519_frodokem640; diff --git a/src/views/aead.rs b/src/views/aead.rs index 13e09d9..dc02cf3 100644 --- a/src/views/aead.rs +++ b/src/views/aead.rs @@ -3,8 +3,8 @@ use crate::error::SealError; use aes_gcm::{ - aead::{Aead, KeyInit, Payload}, Aes128Gcm, Aes256Gcm, + aead::{Aead, KeyInit, Payload}, }; use chacha20poly1305::{ChaCha20Poly1305, XChaCha20Poly1305}; use hkdf::Hkdf; diff --git a/src/views/bcrypt.rs b/src/views/bcrypt.rs index ab393a2..0e21f42 100644 --- a/src/views/bcrypt.rs +++ b/src/views/bcrypt.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{error::KdfError, AttrId, Error, KdfAttrView, KdfView, Multikey, Views}; +use crate::{AttrId, Error, KdfAttrView, KdfView, Multikey, Views, error::KdfError}; use multi_codec::Codec; use multi_util::Varuint; use zeroize::Zeroizing; diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index b0c4db7..70c9f7d 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -1,24 +1,24 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ + AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, Multikey, OpenView, SealView, SignView, ThresholdAttrView, ThresholdView, + VerifyView, Views, error::{ AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, ThresholdError, VerifyError, }, views::threshold_meta::{self, ThresholdDisclosure}, - AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, Multikey, OpenView, SealView, SignView, ThresholdAttrView, ThresholdView, - VerifyView, Views, }; use blsful::{ + Bls12381G1Impl, Bls12381G2Impl, PublicKey, PublicKeyShare, SECRET_KEY_BYTES, SecretKey, + SecretKeyShare, Signature, SignatureSchemes, SignatureShare, TimeCryptCiphertext, inner_types::{G1Projective, G2Projective, Scalar}, vsss_rs::{IdentifierPrimeField, Share, ValueGroup}, - Bls12381G1Impl, Bls12381G2Impl, PublicKey, PublicKeyShare, SecretKey, SecretKeyShare, - Signature, SignatureSchemes, SignatureShare, TimeCryptCiphertext, SECRET_KEY_BYTES, }; use elliptic_curve::group::GroupEncoding; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, views::bls12381::SchemeTypeId, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms, views::bls12381::SchemeTypeId}; use multi_trait::TryDecodeFrom; use multi_util::{Varbytes, Varuint}; use rand_core::Rng; @@ -81,7 +81,7 @@ impl From for Vec { fn from(val: KeyShare) -> Self { let mut v = Vec::default(); // add in the share identifier - v.extend_from_slice(&val.0 .0.to_be_bytes()); + v.extend_from_slice(&val.0.0.to_be_bytes()); // add in the threshold v.append(&mut Varuint(val.1).into()); // add in the limit @@ -418,13 +418,13 @@ impl<'a> ConvView for View<'a> { let public_key = secret_key .public_key() .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; - let key_bytes = public_key.0 .0.value().0.to_bytes().as_ref().to_vec(); + let key_bytes = public_key.0.0.value().0.to_bytes().as_ref().to_vec(); Builder::new(Codec::Bls12381G1PubShare) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) .with_threshold(threshold) .with_limit(limit) - .with_identifier(&public_key.0 .0.identifier().0.to_be_bytes()) + .with_identifier(&public_key.0.0.identifier().0.to_be_bytes()) .try_build() } Codec::Bls12381G2Priv => { @@ -463,13 +463,13 @@ impl<'a> ConvView for View<'a> { let public_key = secret_key .public_key() .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; - let key_bytes = public_key.0 .0.value().0.to_bytes().as_ref().to_vec(); + let key_bytes = public_key.0.0.value().0.to_bytes().as_ref().to_vec(); Builder::new(Codec::Bls12381G2PubShare) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) .with_threshold(threshold) .with_limit(limit) - .with_identifier(&public_key.0 .0.identifier().0.to_be_bytes()) + .with_identifier(&public_key.0.0.identifier().0.to_be_bytes()) .try_build() } _ => Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), @@ -895,7 +895,7 @@ impl<'a> ThresholdView for View<'a> { match self.mk.codec { Codec::Bls12381G1Priv | Codec::Bls12381G2Priv => {} Codec::Bls12381G1Pub | Codec::Bls12381G2Pub => { - return Err(ThresholdError::NotASecretKey.into()) + return Err(ThresholdError::NotASecretKey.into()); } Codec::Bls12381G1PubShare | Codec::Bls12381G1PrivShare diff --git a/src/views/bls12381_g1_fndsa512.rs b/src/views/bls12381_g1_fndsa512.rs index 8f3fea1..20cfab2 100644 --- a/src/views/bls12381_g1_fndsa512.rs +++ b/src/views/bls12381_g1_fndsa512.rs @@ -8,18 +8,18 @@ use super::bls12381_hybrid as bls; use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use fn_dsa::{ - sign_key_size, signature_size, vrfy_key_size, SigningKey as _, SigningKeyStandard, - VerifyingKey as _, VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, + DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, SigningKey as _, SigningKeyStandard, + VerifyingKey as _, VerifyingKeyStandard, sign_key_size, signature_size, vrfy_key_size, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use zeroize::Zeroizing; const LOGN: u32 = FN_DSA_LOGN_512; @@ -261,10 +261,11 @@ mod tests { let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"wrong message")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err() + ); } } diff --git a/src/views/bls12381_g1_mayo1.rs b/src/views/bls12381_g1_mayo1.rs index 9cc9241..87dea29 100644 --- a/src/views/bls12381_g1_mayo1.rs +++ b/src/views/bls12381_g1_mayo1.rs @@ -8,15 +8,15 @@ use super::bls12381_hybrid as bls; use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use ml_dsa::signature::{Signer, Verifier}; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use pq_mayo::{KeyPair, Mayo1, Signature, VerifyingKey}; use zeroize::Zeroizing; @@ -248,10 +248,11 @@ mod tests { let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"wrong message")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err() + ); } } diff --git a/src/views/bls12381_g1_mayo2.rs b/src/views/bls12381_g1_mayo2.rs index e5f293b..43f9533 100644 --- a/src/views/bls12381_g1_mayo2.rs +++ b/src/views/bls12381_g1_mayo2.rs @@ -8,15 +8,15 @@ use super::bls12381_hybrid as bls; use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use ml_dsa::signature::{Signer, Verifier}; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use pq_mayo::{KeyPair, Mayo2, Signature, VerifyingKey}; use zeroize::Zeroizing; @@ -248,10 +248,11 @@ mod tests { let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"wrong message")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err() + ); } } diff --git a/src/views/bls12381_g1_mldsa65.rs b/src/views/bls12381_g1_mldsa65.rs index 05a20c4..d5cc0bf 100644 --- a/src/views/bls12381_g1_mldsa65.rs +++ b/src/views/bls12381_g1_mldsa65.rs @@ -7,19 +7,19 @@ use super::bls12381_hybrid as bls; use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use ml_dsa::{ - signature::{Keypair, Signer as MldsaSigner, Verifier as MldsaVerifier}, EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MldsaSeed, Signature as MldsaSig, SigningKey as MldsaSigningKey, VerifyingKey as MldsaVerifyingKey, + signature::{Keypair, Signer as MldsaSigner, Verifier as MldsaVerifier}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use zeroize::Zeroizing; const MLDSA65_SEED_LEN: usize = 32; @@ -271,11 +271,12 @@ mod tests { pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); // wrong message must fail - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"wrong message")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err() + ); } #[test] diff --git a/src/views/bls12381_hybrid.rs b/src/views/bls12381_hybrid.rs index 3878b3f..838a6c3 100644 --- a/src/views/bls12381_hybrid.rs +++ b/src/views/bls12381_hybrid.rs @@ -5,8 +5,8 @@ //! public key = 96 bytes (G2 point), signature = 48 bytes (G1 point). The hybrid //! schemes use a fixed `Basic` signature scheme. -use crate::error::{ConversionsError, SignError, VerifyError}; use crate::Error; +use crate::error::{ConversionsError, SignError, VerifyError}; use blsful::inner_types::{G1Projective, GroupEncoding}; use blsful::{Bls12381G1Impl, PublicKey, SecretKey, Signature, SignatureSchemes}; diff --git a/src/views/chacha20.rs b/src/views/chacha20.rs index 1ecc44c..9dfacc1 100644 --- a/src/views/chacha20.rs +++ b/src/views/chacha20.rs @@ -1,16 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - error::{AttributesError, CipherError, KdfError}, AttrId, AttrView, CipherAttrView, CipherView, DataView, Error, FingerprintView, KdfAttrView, Multikey, Views, + error::{AttributesError, CipherError, KdfError}, }; #[cfg(feature = "legacy_chacha20_fallback")] -use chacha20::cipher::{KeyIvInit, StreamCipher}; -#[cfg(feature = "legacy_chacha20_fallback")] use chacha20::ChaCha20; use chacha20::Nonce; +#[cfg(feature = "legacy_chacha20_fallback")] +use chacha20::cipher::{KeyIvInit, StreamCipher}; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varuint; use zeroize::Zeroizing; diff --git a/src/views/classic_mceliece.rs b/src/views/classic_mceliece.rs index e3173f0..54abd0c 100644 --- a/src/views/classic_mceliece.rs +++ b/src/views/classic_mceliece.rs @@ -8,13 +8,13 @@ //! compiled in a single dependency graph. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use rand_chacha::ChaCha20Rng; @@ -390,11 +390,12 @@ mod tests { .unwrap(); // seal with private key should fail - assert!(sk - .seal_view() - .unwrap() - .seal(b"data", Codec::Xchacha20Poly1305, b"") - .is_err()); + assert!( + sk.seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err() + ); } #[test] @@ -426,10 +427,11 @@ mod tests { let pk = sk.conv_view().unwrap().to_public_key().unwrap(); // AES-128-GCM is not allowed for mceliece (not PQ-safe) - assert!(pk - .seal_view() - .unwrap() - .seal(b"data", Codec::AesGcm128, b"") - .is_err()); + assert!( + pk.seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err() + ); } } diff --git a/src/views/dkg_threshold.rs b/src/views/dkg_threshold.rs index 5d20164..1c479d1 100644 --- a/src/views/dkg_threshold.rs +++ b/src/views/dkg_threshold.rs @@ -10,8 +10,8 @@ //! - [`AttrId::DkgIdentifier`] → [`identifier`](ThresholdAttrView::identifier) //! - [`AttrId::DkgGroupPublicKey`] → [`threshold_data`](ThresholdAttrView::threshold_data) -use crate::error::AttributesError; use crate::Error; +use crate::error::AttributesError; use crate::{AttrId, AttrView, Multikey, ThresholdAttrView, ThresholdKeyView}; use multi_trait::TryDecodeFrom; use multi_util::Varuint; diff --git a/src/views/ed25519.rs b/src/views/ed25519.rs index 12b5528..90c2887 100644 --- a/src/views/ed25519.rs +++ b/src/views/ed25519.rs @@ -1,15 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - error::{AttributesError, CipherError, ConversionsError, KdfError, SignError, VerifyError}, AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, KdfAttrView, Multikey, SignView, VerifyView, Views, + error::{AttributesError, CipherError, ConversionsError, KdfError, SignError, VerifyError}, }; use ed25519_dalek::{ - Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, + PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, Signature, Signer, SigningKey, VerifyingKey, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use multi_trait::TryDecodeFrom; use multi_util::Varuint; use zeroize::Zeroizing; diff --git a/src/views/ed25519_fndsa512.rs b/src/views/ed25519_fndsa512.rs index fbe79d4..3625eb2 100644 --- a/src/views/ed25519_fndsa512.rs +++ b/src/views/ed25519_fndsa512.rs @@ -6,19 +6,19 @@ //! Public encoding is classical-first: ed25519_pub (32) || fn_dsa_verifying_key. use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; use fn_dsa::{ - sign_key_size, signature_size, vrfy_key_size, SigningKey as _, SigningKeyStandard, - VerifyingKey as _, VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, + DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, SigningKey as _, SigningKeyStandard, + VerifyingKey as _, VerifyingKeyStandard, sign_key_size, signature_size, vrfy_key_size, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use zeroize::Zeroizing; const LOGN: u32 = FN_DSA_LOGN_512; @@ -285,10 +285,11 @@ mod tests { pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); // wrong message must fail - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"wrong message")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err() + ); } } diff --git a/src/views/ed25519_mayo2.rs b/src/views/ed25519_mayo2.rs index d216ecc..def33af 100644 --- a/src/views/ed25519_mayo2.rs +++ b/src/views/ed25519_mayo2.rs @@ -4,15 +4,15 @@ //! Verify: verify Ed25519(m, s1) && verify Mayo2(m || s1, s2) use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use pq_mayo::{KeyPair, Mayo2}; use zeroize::Zeroizing; @@ -394,11 +394,12 @@ mod tests { let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); // Tamper with message - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"wrong message")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err() + ); } #[test] diff --git a/src/views/ed25519_mldsa65.rs b/src/views/ed25519_mldsa65.rs index 75d3908..e36ff40 100644 --- a/src/views/ed25519_mldsa65.rs +++ b/src/views/ed25519_mldsa65.rs @@ -5,20 +5,20 @@ //! Public encoding is classical-first: ed25519_pub (32) || mldsa65_pub. use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; use ml_dsa::{ - signature::{Keypair, Signer as MldsaSigner, Verifier as MldsaVerifier}, EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MldsaSeed, Signature as MldsaSig, SigningKey as MldsaSigningKey, VerifyingKey as MldsaVerifyingKey, + signature::{Keypair, Signer as MldsaSigner, Verifier as MldsaVerifier}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use zeroize::Zeroizing; const ED25519_SEED_LEN: usize = 32; @@ -295,11 +295,12 @@ mod tests { pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); // wrong message must fail - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"wrong message")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err() + ); } #[test] diff --git a/src/views/fn_dsa.rs b/src/views/fn_dsa.rs index cf61454..721d3d7 100644 --- a/src/views/fn_dsa.rs +++ b/src/views/fn_dsa.rs @@ -2,18 +2,18 @@ //! FN-DSA (Falcon) 512/1024 multikey view; FIPS 206 (draft). use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use fn_dsa::{ - signature_size, vrfy_key_size, SigningKey, SigningKeyStandard, VerifyingKey, - VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_1024, FN_DSA_LOGN_512, HASH_ID_RAW, + DOMAIN_NONE, FN_DSA_LOGN_512, FN_DSA_LOGN_1024, HASH_ID_RAW, SigningKey, SigningKeyStandard, + VerifyingKey, VerifyingKeyStandard, signature_size, vrfy_key_size, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Views as _}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Views as _, ms}; use ssh_encoding::{Decode, Encode}; use zeroize::Zeroizing; diff --git a/src/views/frodokem.rs b/src/views/frodokem.rs index 6fb3f6a..f3abdad 100644 --- a/src/views/frodokem.rs +++ b/src/views/frodokem.rs @@ -2,13 +2,13 @@ //! FrodoKEM AES/SHAKE multikey view. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use zeroize::Zeroizing; @@ -81,7 +81,7 @@ fn public_from_private(codec: Codec, secret_bytes: &[u8]) -> Result, Err _ => { return Err( ConversionsError::SecretKeyFailure("not a FrodoKEM private key".into()).into(), - ) + ); } } .map_err(|e| ConversionsError::SecretKeyFailure(e).into()) diff --git a/src/views/frodokem_helper.rs b/src/views/frodokem_helper.rs index 59219d2..dc5754b 100644 --- a/src/views/frodokem_helper.rs +++ b/src/views/frodokem_helper.rs @@ -4,8 +4,8 @@ //! Keys and ciphertexts are passed as raw byte slices; shared secrets are //! returned as `Vec` wrapped in [`zeroize::Zeroizing`]. use frodo_kem_rs::Algorithm; -use getrandom::rand_core::UnwrapErr; use getrandom::SysRng; +use getrandom::rand_core::UnwrapErr; use zeroize::Zeroizing; fn rng() -> UnwrapErr { diff --git a/src/views/mayo.rs b/src/views/mayo.rs index 68367b6..0f948bd 100644 --- a/src/views/mayo.rs +++ b/src/views/mayo.rs @@ -2,14 +2,14 @@ //! MAYO-1/2/3/5 multikey view; post-quantum multivariate signature. use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Views as _}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Views as _, ms}; use pq_mayo::{KeyPair, Mayo1, Mayo2, Mayo3, Mayo5, Signature, VerifyingKey}; use ssh_encoding::{Decode, Encode}; use zeroize::Zeroizing; @@ -420,11 +420,12 @@ mod tests { pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); // wrong message must fail - assert!(pk - .verify_view() - .unwrap() - .verify(&sig, Some(b"tampered")) - .is_err()); + assert!( + pk.verify_view() + .unwrap() + .verify(&sig, Some(b"tampered")) + .is_err() + ); } #[test] diff --git a/src/views/ml_dsa.rs b/src/views/ml_dsa.rs index a16fafa..1244752 100644 --- a/src/views/ml_dsa.rs +++ b/src/views/ml_dsa.rs @@ -2,19 +2,19 @@ //! ML-DSA 65/87 multikey view; FIPS 204. use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, - views::Views, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, }; use ml_dsa::{ - signature::{Keypair, Signer, Verifier}, EncodedSignature, EncodedVerifyingKey, MlDsa65, MlDsa87, Seed, Signature, SigningKey, VerifyingKey, + signature::{Keypair, Signer, Verifier}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Views as _}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Views as _, ms}; use ssh_encoding::{Decode, Encode}; use zeroize::Zeroizing; diff --git a/src/views/ml_kem.rs b/src/views/ml_kem.rs index 3c64398..346aabf 100644 --- a/src/views/ml_kem.rs +++ b/src/views/ml_kem.rs @@ -2,17 +2,17 @@ //! ML-KEM 768/1024 multikey view; FIPS 203 key encapsulation. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use ml_kem::{ + MlKem768, MlKem1024, kem::{Decapsulate, Encapsulate, FromSeed, Kem, KeyExport}, - MlKem1024, MlKem768, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use zeroize::Zeroizing; @@ -428,11 +428,12 @@ mod tests { .unwrap(); // seal with private key should fail - assert!(sk - .seal_view() - .unwrap() - .seal(b"data", Codec::Xchacha20Poly1305, b"") - .is_err()); + assert!( + sk.seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err() + ); } #[test] @@ -464,10 +465,11 @@ mod tests { let pk = sk.conv_view().unwrap().to_public_key().unwrap(); // AES-128-GCM is not allowed for ML-KEM (not PQ-safe) - assert!(pk - .seal_view() - .unwrap() - .seal(b"data", Codec::AesGcm128, b"") - .is_err()); + assert!( + pk.seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err() + ); } } diff --git a/src/views/nist_p.rs b/src/views/nist_p.rs index 3d6f2d7..be34374 100644 --- a/src/views/nist_p.rs +++ b/src/views/nist_p.rs @@ -2,19 +2,19 @@ //! NIST P-256, P-384, P-521 ECDSA multikey view. use crate::{ + AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, error::{ AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, VerifyError, }, views::aead, - AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, }; -use elliptic_curve::sec1::ToSec1Point; use elliptic_curve::Generate; +use elliptic_curve::sec1::ToSec1Point; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use multi_util::Varuint; @@ -207,7 +207,7 @@ impl<'a> ConvView for View<'a> { _ => { return Err( ConversionsError::SecretKeyFailure("not a NIST-P secret key".into()).into(), - ) + ); } }; @@ -344,7 +344,7 @@ impl<'a> SignView for View<'a> { let sig_bytes = match self.mk.codec { Codec::P256Priv => { - use p256::ecdsa::{signature::Signer, Signature, SigningKey}; + use p256::ecdsa::{Signature, SigningKey, signature::Signer}; let sk = SigningKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; let signature: Signature = sk @@ -353,7 +353,7 @@ impl<'a> SignView for View<'a> { signature.to_bytes().to_vec() } Codec::P384Priv => { - use p384::ecdsa::{signature::Signer, Signature, SigningKey}; + use p384::ecdsa::{Signature, SigningKey, signature::Signer}; let sk = SigningKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; let signature: Signature = sk @@ -362,7 +362,7 @@ impl<'a> SignView for View<'a> { signature.to_bytes().to_vec() } Codec::P521Priv => { - use p521::ecdsa::{signature::Signer, Signature, SigningKey}; + use p521::ecdsa::{Signature, SigningKey, signature::Signer}; let sk = SigningKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; let signature: Signature = sk @@ -410,7 +410,7 @@ impl<'a> VerifyView for View<'a> { match pubmk.codec { Codec::P256Pub => { - use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = VerifyingKey::from_sec1_bytes(&key_bytes) .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; let signature = Signature::from_slice(&sig) @@ -419,7 +419,7 @@ impl<'a> VerifyView for View<'a> { .map_err(|e| VerifyError::BadSignature(e.to_string()))?; } Codec::P384Pub => { - use p384::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = VerifyingKey::from_sec1_bytes(&key_bytes) .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; let signature = Signature::from_slice(&sig) @@ -428,7 +428,7 @@ impl<'a> VerifyView for View<'a> { .map_err(|e| VerifyError::BadSignature(e.to_string()))?; } Codec::P521Pub => { - use p521::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + use p521::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = VerifyingKey::from_sec1_bytes(&key_bytes) .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; let signature = Signature::from_slice(&sig) @@ -737,10 +737,11 @@ mod ecies_tests { .unwrap() .seal(b"secret", Codec::Chacha20Poly1305, b"") .unwrap(); - assert!(sk2 - .open_view() - .unwrap() - .open(&sealed, ephemeral.as_ref(), b"") - .is_err()); + assert!( + sk2.open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err() + ); } } diff --git a/src/views/rsa.rs b/src/views/rsa.rs index 2a803d5..acb8ed5 100644 --- a/src/views/rsa.rs +++ b/src/views/rsa.rs @@ -2,22 +2,23 @@ //! RSA-2048/3072/4096 multikey view — signing (PSS SHA-256) + encryption (RSA-OAEP + AEAD). use crate::{ + AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, error::{ AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, VerifyError, }, - views::{aead, Views}, - AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, + views::{Views, aead}, }; use ::rsa::sha2::Sha256; use ::rsa::{ + Oaep, RsaPrivateKey, RsaPublicKey, pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPublicKey}, - pss, Oaep, RsaPrivateKey, RsaPublicKey, + pss, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use multi_trait::TryDecodeFrom; use multi_util::{Varbytes, Varuint}; use ssh_encoding::{Decode, Encode}; diff --git a/src/views/secp256k1.rs b/src/views/secp256k1.rs index 4dfff3c..c9e6dd4 100644 --- a/src/views/secp256k1.rs +++ b/src/views/secp256k1.rs @@ -1,22 +1,22 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ + AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, error::{ AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, VerifyError, }, views::aead, - AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, }; -use elliptic_curve::sec1::ToSec1Point; use elliptic_curve::Generate; +use elliptic_curve::sec1::ToSec1Point; use k256::ecdsa::{ - signature::{Signer, Verifier}, Signature, SigningKey, VerifyingKey, + signature::{Signer, Verifier}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_hash::{Multihash, mh}; +use multi_sig::{Multisig, Views as SigViews, ms}; use multi_trait::TryDecodeFrom; use multi_util::{Varbytes, Varuint}; use ssh_encoding::{Decode, Encode}; @@ -558,10 +558,11 @@ mod ecies_tests { .unwrap() .seal(b"secret", Codec::Chacha20Poly1305, b"") .unwrap(); - assert!(sk2 - .open_view() - .unwrap() - .open(&sealed, ephemeral.as_ref(), b"") - .is_err()); + assert!( + sk2.open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err() + ); } } diff --git a/src/views/slh_dsa.rs b/src/views/slh_dsa.rs index dcfe2da..7c98d51 100644 --- a/src/views/slh_dsa.rs +++ b/src/views/slh_dsa.rs @@ -2,16 +2,16 @@ //! SLH-DSA multikey view; FIPS 205. Supports all 12 parameter sets (Sha2_128f/s through Shake256f/s). use crate::{ - error::{AttributesError, ConversionsError, SignError, VerifyError}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, Views, + error::{AttributesError, ConversionsError, SignError, VerifyError}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::ms; +use multi_hash::{Multihash, mh}; use multi_sig::Views as _; -use slh_dsa::signature::{Keypair, Signer, Verifier}; +use multi_sig::ms; use slh_dsa::ParameterSet; +use slh_dsa::signature::{Keypair, Signer, Verifier}; use slh_dsa::{ Sha2_128f, Sha2_128s, Sha2_192f, Sha2_192s, Sha2_256f, Sha2_256s, Shake128f, Shake128s, Shake192f, Shake192s, Shake256f, Shake256s, Signature, SigningKey, VerifyingKey, diff --git a/src/views/sntrup.rs b/src/views/sntrup.rs index fa8cfb1..bec7ab1 100644 --- a/src/views/sntrup.rs +++ b/src/views/sntrup.rs @@ -4,13 +4,13 @@ //! Supports sntrup761, sntrup857, sntrup953, sntrup1013, and sntrup1277. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use zeroize::Zeroizing; @@ -535,11 +535,12 @@ mod tests { .unwrap(); // seal with private key should fail - assert!(sk - .seal_view() - .unwrap() - .seal(b"data", Codec::Xchacha20Poly1305, b"") - .is_err()); + assert!( + sk.seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err() + ); } #[test] @@ -571,10 +572,11 @@ mod tests { let pk = sk.conv_view().unwrap().to_public_key().unwrap(); // AES-128-GCM is not allowed for sntrup (not PQ-safe) - assert!(pk - .seal_view() - .unwrap() - .seal(b"data", Codec::AesGcm128, b"") - .is_err()); + assert!( + pk.seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err() + ); } } diff --git a/src/views/threshold_marker.rs b/src/views/threshold_marker.rs index e39fd04..5c3b0c4 100644 --- a/src/views/threshold_marker.rs +++ b/src/views/threshold_marker.rs @@ -17,7 +17,7 @@ //! split codecs (`Bls12381G1PrivShare`/`PubShare`, `Bls12381G2PrivShare`/ //! `PubShare`) and the DKG codecs (`*ThreshPrivShare`/`*ThreshPubShare`). -use crate::{error::AttributesError, AttrId, Error, Multikey}; +use crate::{AttrId, Error, Multikey, error::AttributesError}; use multi_codec::Codec; use multi_util::Varuint; use serde::{Deserialize, Serialize}; diff --git a/src/views/threshold_meta.rs b/src/views/threshold_meta.rs index 501dfaa..0a500e5 100644 --- a/src/views/threshold_meta.rs +++ b/src/views/threshold_meta.rs @@ -17,14 +17,14 @@ // historical `multi_key::ThresholdDisclosure` / `multi_key::encrypt_threshold_meta` // public API keeps working for downstream callers. pub use multi_sig::{ - decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, ThresholdDisclosure, - ThresholdMetaCipher, ThresholdMetadata, + ThresholdDisclosure, ThresholdMetaCipher, ThresholdMetadata, decrypt_threshold_meta, + encrypt_threshold_meta, generate_meta_key, }; use crate::{ + AttrId, Error, Multikey, Views, error::{AttributesError, ThresholdError}, mk::Attributes, - AttrId, Error, Multikey, Views, }; use multi_trait::{EncodeInto, TryDecodeFrom}; use multi_util::Varuint; @@ -205,7 +205,7 @@ pub fn stamp_disclosure_attrs( _ => { return Err(Error::Threshold(ThresholdError::MetaEncryption(format!( "unsupported disclosure mode: {mode}" - )))) + )))); } } Ok(()) diff --git a/src/views/x25519.rs b/src/views/x25519.rs index 0cbb988..e3677f3 100644 --- a/src/views/x25519.rs +++ b/src/views/x25519.rs @@ -2,13 +2,13 @@ //! X25519 ECIES multikey view; Curve25519 Diffie-Hellman key agreement + AEAD. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use x25519_dalek::{PublicKey, StaticSecret}; @@ -377,11 +377,12 @@ mod tests { .unwrap(); // Opening with wrong key should fail - assert!(sk2 - .open_view() - .unwrap() - .open(&sealed, ephemeral.as_ref(), b"") - .is_err()); + assert!( + sk2.open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err() + ); } #[test] @@ -392,11 +393,12 @@ mod tests { .try_build() .unwrap(); - assert!(sk - .seal_view() - .unwrap() - .seal(b"data", Codec::Chacha20Poly1305, b"") - .is_err()); + assert!( + sk.seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .is_err() + ); } #[test] @@ -414,10 +416,11 @@ mod tests { .seal(b"data", Codec::Chacha20Poly1305, b"") .unwrap(); - assert!(pk - .open_view() - .unwrap() - .open(&sealed, ephemeral.as_ref(), b"") - .is_err()); + assert!( + pk.open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err() + ); } } diff --git a/src/views/x25519_frodokem640.rs b/src/views/x25519_frodokem640.rs index 39bdd0a..18beb8e 100644 --- a/src/views/x25519_frodokem640.rs +++ b/src/views/x25519_frodokem640.rs @@ -7,13 +7,13 @@ //! Public key layout (classical-first): `x25519_pub (32) || frodokem_public_key`. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use x25519_dalek::{PublicKey, StaticSecret}; diff --git a/src/views/x25519_mceliece348864.rs b/src/views/x25519_mceliece348864.rs index 55b3026..e900734 100644 --- a/src/views/x25519_mceliece348864.rs +++ b/src/views/x25519_mceliece348864.rs @@ -7,13 +7,13 @@ //! Public key layout (classical-first): `x25519_pub (32) || mceliece_public_key`. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use rand_chacha::ChaCha20Rng; diff --git a/src/views/x25519_mlkem768.rs b/src/views/x25519_mlkem768.rs index c31eb23..08d2ce1 100644 --- a/src/views/x25519_mlkem768.rs +++ b/src/views/x25519_mlkem768.rs @@ -3,17 +3,17 @@ //! ChaCha20-Poly1305 AEAD, and SHA-512 KDF. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use ml_kem::{ - kem::{Decapsulate, Encapsulate, FromSeed, Kem, KeyExport}, MlKem768, + kem::{Decapsulate, Encapsulate, FromSeed, Kem, KeyExport}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use sha2::{Digest, Sha512}; @@ -459,11 +459,12 @@ mod tests { .try_build() .unwrap(); - assert!(sk - .seal_view() - .unwrap() - .seal(b"data", Codec::Chacha20Poly1305, b"") - .is_err()); + assert!( + sk.seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .is_err() + ); } #[test] @@ -493,15 +494,17 @@ mod tests { .unwrap(); let pk = sk.conv_view().unwrap().to_public_key().unwrap(); - assert!(pk - .seal_view() - .unwrap() - .seal(b"data", Codec::AesGcm128, b"") - .is_err()); - assert!(pk - .seal_view() - .unwrap() - .seal(b"data", Codec::Xchacha20Poly1305, b"") - .is_err()); + assert!( + pk.seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err() + ); + assert!( + pk.seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err() + ); } } diff --git a/src/views/x25519_sntrup761.rs b/src/views/x25519_sntrup761.rs index e0f4ec6..4b436b3 100644 --- a/src/views/x25519_sntrup761.rs +++ b/src/views/x25519_sntrup761.rs @@ -3,13 +3,13 @@ //! ChaCha20-Poly1305 AEAD, and a BLAKE3 combiner feeding HKDF-SHA512. use crate::{ - error::{AttributesError, ConversionsError, SealError}, - views::{aead, Views}, AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, + error::{AttributesError, ConversionsError, SealError}, + views::{Views, aead}, }; use multi_codec::Codec; -use multi_hash::{mh, Multihash}; +use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use sntrup::sntrup761::{self, EncapsulationKey}; @@ -466,11 +466,12 @@ mod tests { .try_build() .unwrap(); - assert!(sk - .seal_view() - .unwrap() - .seal(b"data", Codec::Chacha20Poly1305, b"") - .is_err()); + assert!( + sk.seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .is_err() + ); } #[test] @@ -501,15 +502,17 @@ mod tests { let pk = sk.conv_view().unwrap().to_public_key().unwrap(); // Only ChaCha20-Poly1305 is allowed - assert!(pk - .seal_view() - .unwrap() - .seal(b"data", Codec::AesGcm128, b"") - .is_err()); - assert!(pk - .seal_view() - .unwrap() - .seal(b"data", Codec::Xchacha20Poly1305, b"") - .is_err()); + assert!( + pk.seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err() + ); + assert!( + pk.seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err() + ); } } diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs index 390feae..bd1bfa9 100644 --- a/tests/edge_case_tests.rs +++ b/tests/edge_case_tests.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 //! Edge case tests for multi-key +#![allow(clippy::explicit_iter_loop, clippy::uninlined_format_args)] use multi_codec::Codec; -use multi_key::{Builder, Multikey, KEY_CODECS}; +use multi_key::{Builder, KEY_CODECS, Multikey}; use multi_trait::Null; /// Test null multikey diff --git a/tests/proptest_tests.rs b/tests/proptest_tests.rs index 6313deb..635f383 100644 --- a/tests/proptest_tests.rs +++ b/tests/proptest_tests.rs @@ -2,7 +2,7 @@ //! Property-based tests for multi-key use multi_codec::Codec; -use multi_key::{Builder, Multikey, KEY_CODECS}; +use multi_key::{Builder, KEY_CODECS, Multikey}; use multi_trait::TryDecodeFrom; use proptest::prelude::*; diff --git a/tests/security_tests.rs b/tests/security_tests.rs index ef4e5db..27d6da6 100644 --- a/tests/security_tests.rs +++ b/tests/security_tests.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 //! Security-focused tests for multi-key +#![allow(clippy::redundant_clone, clippy::doc_markdown)] use multi_codec::Codec; use multi_key::{Builder, Error, Multikey}; @@ -86,7 +87,7 @@ fn test_private_key_zeroization() { #[cfg(not(feature = "legacy_chacha20_fallback"))] #[test] fn test_chacha20_aead_tamper_rejected() { - use multi_key::{cipher, kdf, Builder, Views}; + use multi_key::{Builder, Views, cipher, kdf}; let plain = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2").unwrap(); @@ -147,7 +148,7 @@ fn test_chacha20_aead_tamper_rejected() { #[cfg(feature = "legacy_chacha20_fallback")] #[test] fn test_chacha20_legacy_fallback_downgrades_on_tamper() { - use multi_key::{cipher, kdf, Builder, Views}; + use multi_key::{Builder, Views, cipher, kdf}; let plain = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2").unwrap(); From 8aaab47118e750ab614d85ad648377b5777f8a61 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Fri, 17 Jul 2026 01:49:38 -0600 Subject: [PATCH 2/3] audit fixes Signed-off-by: Dave Grantham --- Cargo.toml | 6 ++---- SECURITY.md | 19 ++++++++++++++++++- src/keysplit.rs | 27 +++++++++++++++++---------- src/mk.rs | 21 ++++++++++++--------- src/serde/de.rs | 33 +++++++++++++++++++++++++++++---- src/serde/mod.rs | 11 +++++++++-- src/views/bls12381.rs | 26 ++++++++++++++++++++------ src/views/rsa.rs | 28 ++++++++++++++-------------- src/views/threshold_marker.rs | 13 ++++++++----- 9 files changed, 129 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 956b568..f61f4e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,10 +63,9 @@ rand_core = "0.10" # 0.6 (fn-dsa exposes its own RngCore/CryptoRng traits); keep a 0.6 OsRng for # those sites while the curve stack above uses rand_core 0.10. rand_core_06 = { package = "rand_core", version = "0.6", features = ["getrandom"] } -rsa = { version = "0.10.0-rc.18", features = ["sha2"] } +sad-rsa = { version = "0.2.3", features = ["sha2"] } sec1 = "0.8" serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } -serde_cbor = "0.11" sha2 = "0.11" signature = "3" slh-dsa = "0.2.0-rc.5" @@ -86,12 +85,11 @@ ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["allo [target.'cfg(not(target_arch = "wasm32"))'.dependencies] blsful = { version = "4.0.0-rc1", default-features = false, features = ["blst"] } -ssh-key = { version = "0.7.0-rc.11", features = ["alloc", "crypto", "ed25519"] } +ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["alloc", "ecdsa", "ed25519", "p256", "p384", "p521"] } [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } proptest = "1.4" -serde_cbor = "0.11" serde_json = "1.0" serde_test = "1.0" diff --git a/SECURITY.md b/SECURITY.md index 9ad144f..9f47daf 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -24,7 +24,6 @@ future. This crate depends on the following release-candidate (RC) crates: -- `rsa = "0.10.0-rc.18"` — RSA signature scheme - `slh-dsa = "0.2.0-rc.5"` — SLH-DSA post-quantum signatures - `vsss-rs = "6.0.0-rc2"` — verifiable secret sharing (transitive via `blsful`) @@ -38,6 +37,24 @@ should be aware that RC APIs may change before stabilisation. Coordinate with `multi-sig` (which depends on the same `blsful` and `ssh-key` versions) when upgrading. +## RSA Implementation: `sad-rsa` (Marvin Attack Mitigation) + +This crate uses [`sad-rsa`](https://crates.io/crates/sad-rsa) (`0.2.3`) +instead of the upstream `rsa` crate to mitigate RUSTSEC-2023-0071 (Marvin +Attack: potential key recovery through timing sidechannels). + +`sad-rsa` is a hardened fork of `rsa` that implements **implicit rejection** +for PKCS#1 v1.5 decryption, making valid and invalid ciphertexts +indistinguishable to attackers. The API is fully compatible with `rsa`. + +This crate uses RSA for: +- RSA key generation (`RsaPrivateKey::new` for 2048/3072/4096-bit keys) +- PKCS#1 encoding (`pkcs1::EncodeRsaPrivateKey`) +- RSA-PSS signing and verification (`pss::SigningKey`, `pss::VerifyingKey`) +- RSA-OAEP encryption/decryption for hybrid key encapsulation + +RSA key material is wrapped in `Zeroizing>` and zeroized on drop. + ## Comment Field Zeroization (R6) The `Multikey` comment field is stored as a plain `String` and is **not** diff --git a/src/keysplit.rs b/src/keysplit.rs index e5392c4..57a5347 100644 --- a/src/keysplit.rs +++ b/src/keysplit.rs @@ -41,6 +41,19 @@ fn err(e: E) -> Error { Error::KeySplit(e.to_string()) } +/// Serialize a value to CBOR bytes using `ciborium` (replaces the +/// unmaintained `serde_cbor` dependency). +fn cbor_to_vec(value: &T) -> Result, Error> { + let mut buf = Vec::new(); + ciborium::into_writer(value, &mut buf).map_err(err)?; + Ok(buf) +} + +/// Deserialize a value from CBOR bytes using `ciborium`. +fn cbor_from_slice(bytes: &[u8]) -> Result { + ciborium::from_reader(bytes).map_err(err) +} + /// Sharing scheme tag stored in each share payload. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] enum Scheme { @@ -392,7 +405,7 @@ fn build_payloads( } fn wrap_share(orig: &Multikey, payload: &SharePayload) -> Result { - let cbor = serde_cbor::to_vec(payload).map_err(err)?; + let cbor = cbor_to_vec(payload)?; let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, Zeroizing::new(cbor)); Ok(Multikey { @@ -410,7 +423,7 @@ fn unwrap_share(mk: &Multikey) -> Result { .attributes .get(&AttrId::KeyData) .ok_or_else(|| err("share missing key data"))?; - serde_cbor::from_slice(kd.as_slice()).map_err(err) + cbor_from_slice(kd.as_slice()) } // ---- public API ------------------------------------------------------------- @@ -668,14 +681,8 @@ mod tests { let original = secret(&mk); let shares = split(&mk, 2, 3, rand::rng()).unwrap(); - let cbor: Vec> = shares - .iter() - .map(|s| serde_cbor::to_vec(s).unwrap()) - .collect(); - let from_cbor: Vec = cbor - .iter() - .map(|b| serde_cbor::from_slice(b).unwrap()) - .collect(); + let cbor: Vec> = shares.iter().map(|s| cbor_to_vec(s).unwrap()).collect(); + let from_cbor: Vec = cbor.iter().map(|b| cbor_from_slice(b).unwrap()).collect(); assert_eq!( secret(&combine(&from_cbor[0..2]).unwrap()), original, diff --git a/src/mk.rs b/src/mk.rs index b1f501a..6310d2c 100644 --- a/src/mk.rs +++ b/src/mk.rs @@ -1515,9 +1515,10 @@ impl Builder { // error instead of a panic inside UnwrapErr during keygen. rsa::OsRng::check_entropy() .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; - let key = ::rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(rsa::OsRng), 2048) - .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; - ::rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) + let key = + ::sad_rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(rsa::OsRng), 2048) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + ::sad_rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))? .as_bytes() .to_vec() @@ -1525,9 +1526,10 @@ impl Builder { Codec::Rsa3072Priv => { rsa::OsRng::check_entropy() .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; - let key = ::rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(rsa::OsRng), 3072) - .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; - ::rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) + let key = + ::sad_rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(rsa::OsRng), 3072) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + ::sad_rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))? .as_bytes() .to_vec() @@ -1535,9 +1537,10 @@ impl Builder { Codec::Rsa4096Priv => { rsa::OsRng::check_entropy() .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; - let key = ::rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(rsa::OsRng), 4096) - .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; - ::rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) + let key = + ::sad_rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(rsa::OsRng), 4096) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + ::sad_rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))? .as_bytes() .to_vec() diff --git a/src/serde/de.rs b/src/serde/de.rs index 92f8c6c..feb1fee 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -13,6 +13,31 @@ use serde::{ }; use zeroize::Zeroizing; +/// Visitor that accepts borrowed bytes, owned bytes, and byte buffers. +/// Used by the non-human-readable `Deserialize` paths so that deserializers +/// which cannot lend borrowed slices (e.g. `ciborium`) still work. +struct ByteBufVisitor; + +impl<'de> Visitor<'de> for ByteBufVisitor { + type Value = Vec; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("byte buffer") + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } +} + /// Deserialize instance of [`crate::Nonce`] impl<'de> Deserialize<'de> for Nonce { fn deserialize(deserializer: D) -> Result @@ -60,8 +85,8 @@ impl<'de> Deserialize<'de> for Nonce { if deserializer.is_human_readable() { deserializer.deserialize_struct(nonce::SIGIL.as_str(), FIELDS, NonceVisitor) } else { - let b: &'de [u8] = Deserialize::deserialize(deserializer)?; - Ok(Self::try_from(b).map_err(D::Error::custom)?) + let b = deserializer.deserialize_byte_buf(ByteBufVisitor)?; + Ok(Self::try_from(b.as_slice()).map_err(D::Error::custom)?) } } } @@ -198,8 +223,8 @@ impl<'de> Deserialize<'de> for Multikey { if deserializer.is_human_readable() { deserializer.deserialize_struct(mk::SIGIL.as_str(), FIELDS, MultikeyVisitor) } else { - let b: &'de [u8] = Deserialize::deserialize(deserializer)?; - Ok(Self::try_from(b).map_err(D::Error::custom)?) + let b = deserializer.deserialize_byte_buf(ByteBufVisitor)?; + Ok(Self::try_from(b.as_slice()).map_err(D::Error::custom)?) } } } diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 9e780e8..b9857c9 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -13,6 +13,13 @@ mod tests { use multi_util::BaseEncoded; use serde::{Deserialize, Serialize}; use serde_test::{Configure, Token, assert_tokens}; + + /// Serialize a value to CBOR bytes using `ciborium`. + fn cbor_to_vec(value: &T) -> Vec { + let mut buf = Vec::new(); + ciborium::into_writer(value, &mut buf).expect("CBOR serialize"); + buf + } use std::collections::BTreeMap; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -46,8 +53,8 @@ mod tests { w1.map.insert(skh, sk); w1.map.insert(pkh, pk); - let b = serde_cbor::to_vec(&w1).unwrap(); - let w2 = serde_cbor::from_slice(b.as_slice()).unwrap(); + let b = cbor_to_vec(&w1); + let w2 = ciborium::from_reader(b.as_slice()).unwrap(); assert_eq!(w1, w2); let s = serde_json::to_string(&w1).unwrap(); let w3 = serde_json::from_str(&s).unwrap(); diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index 70c9f7d..2d694b1 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -705,7 +705,10 @@ impl<'a> SignView for View<'a> { .sign(sig_scheme, msg) .map_err(|e| SignError::SigningFailed(e.to_string()))?; - let mut ms = ms::Builder::new_from_bls_signature(&signature)?; + let mut ms = ms::Builder::new_from_bls_signature_with_codec( + Codec::Bls12381G1Msig, + &signature, + )?; if combined { ms = ms.with_message_bytes(&msg); } @@ -726,8 +729,12 @@ impl<'a> SignView for View<'a> { .sign(sig_scheme, msg) .map_err(|e| SignError::SigningFailed(e.to_string()))?; - let mut ms = - ms::Builder::new_from_bls_signature_share(threshold, limit, &signature)?; + let mut ms = ms::Builder::new_from_bls_signature_share_with_codec( + Codec::Bls12381G1ShareMsig, + threshold, + limit, + &signature, + )?; if combined { ms = ms.with_message_bytes(&msg); } @@ -752,7 +759,10 @@ impl<'a> SignView for View<'a> { .sign(sig_scheme, msg) .map_err(|e| SignError::SigningFailed(e.to_string()))?; - let mut ms = ms::Builder::new_from_bls_signature(&signature)?; + let mut ms = ms::Builder::new_from_bls_signature_with_codec( + Codec::Bls12381G2Msig, + &signature, + )?; if combined { ms = ms.with_message_bytes(&msg); } @@ -772,8 +782,12 @@ impl<'a> SignView for View<'a> { let signature = secret_key .sign(sig_scheme, msg) .map_err(|e| SignError::SigningFailed(e.to_string()))?; - let mut ms = - ms::Builder::new_from_bls_signature_share(threshold, limit, &signature)?; + let mut ms = ms::Builder::new_from_bls_signature_share_with_codec( + Codec::Bls12381G2ShareMsig, + threshold, + limit, + &signature, + )?; if combined { ms = ms.with_message_bytes(&msg); } diff --git a/src/views/rsa.rs b/src/views/rsa.rs index acb8ed5..5ce91ee 100644 --- a/src/views/rsa.rs +++ b/src/views/rsa.rs @@ -10,8 +10,8 @@ use crate::{ views::{Views, aead}, }; -use ::rsa::sha2::Sha256; -use ::rsa::{ +use ::sad_rsa::sha2::Sha256; +use ::sad_rsa::{ Oaep, RsaPrivateKey, RsaPublicKey, pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPublicKey}, pss, @@ -27,9 +27,9 @@ use zeroize::Zeroizing; /// Error returned by [`OsRng`] when [`getrandom`] fails to produce entropy. /// -/// `OsRng` implements [`rsa::rand_core::TryRng`] with this as its -/// [`TryRng::Error`](rsa::rand_core::TryRng::Error) so that RNG failures are -/// surfaced through the `Result` returned by the rsa signing/encryption APIs +/// `OsRng` implements [`sad_rsa::rand_core::TryRng`] with this as its +/// [`TryRng::Error`](sad_rsa::rand_core::TryRng::Error) so that RNG failures are +/// surfaced through the `Result` returned by the sad_rsa signing/encryption APIs /// instead of panicking. #[derive(Debug)] pub(crate) struct RngError(pub String); @@ -42,7 +42,7 @@ impl fmt::Display for RngError { impl std::error::Error for RngError {} -/// OsRng compatible with rsa 0.10 (rand_core 0.10) using getrandom 0.4. +/// OsRng compatible with sad_rsa 0.10 (rand_core 0.10) using getrandom 0.4. /// /// Unlike an `Infallible` RNG, this propagates [`getrandom`] failures via /// [`RngError`] so callers (e.g. [`SignView::sign`], [`SealView::seal`]) @@ -50,7 +50,7 @@ impl std::error::Error for RngError {} /// instead of panicking. pub(crate) struct OsRng; -impl ::rsa::rand_core::TryRng for OsRng { +impl ::sad_rsa::rand_core::TryRng for OsRng { type Error = RngError; fn try_next_u32(&mut self) -> Result { @@ -71,11 +71,11 @@ impl ::rsa::rand_core::TryRng for OsRng { } } -impl ::rsa::rand_core::TryCryptoRng for OsRng {} +impl ::sad_rsa::rand_core::TryCryptoRng for OsRng {} impl OsRng { /// Probe the OS RNG with a single byte so callers that need an infallible - /// [`rsa::rand_core::CryptoRng`] (e.g. [`rsa::RsaPrivateKey::new`]) can + /// [`sad_rsa::rand_core::CryptoRng`] (e.g. [`sad_rsa::RsaPrivateKey::new`]) can /// detect an entropy failure up front and surface it as an error instead /// of panicking inside [`rand_core::UnwrapErr`]. pub(crate) fn check_entropy() -> Result<(), RngError> { @@ -349,7 +349,7 @@ impl<'a> SignView for View<'a> { let private_key = RsaPrivateKey::from_pkcs1_der(&secret_bytes) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; - use ::rsa::signature::RandomizedSigner; + use ::sad_rsa::signature::RandomizedSigner; let signing_key = pss::SigningKey::::new(private_key); let signature = signing_key .try_sign_with_rng(&mut OsRng, msg) @@ -369,7 +369,7 @@ impl<'a> SignView for View<'a> { SignError::SigningFailed(e.to_string()) })?; - use ::rsa::signature::SignatureEncoding; + use ::sad_rsa::signature::SignatureEncoding; let sig_bytes = signature.to_bytes(); let mut ms = ms::Builder::new(Codec::Rs256Msig).with_signature_bytes(&sig_bytes); @@ -409,7 +409,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - use ::rsa::signature::Verifier; + use ::sad_rsa::signature::Verifier; let verifying_key = pss::VerifyingKey::::new(public_key); let signature = pss::Signature::try_from(sig.as_slice()) .map_err(|e| VerifyError::BadSignature(e.to_string()))?; @@ -484,7 +484,7 @@ impl<'a> SealView for View<'a> { // Generate random AEAD key let key_len = aead::key_size(aead_codec)?; let mut aead_key = vec![0u8; key_len]; - use ::rsa::rand_core::TryRng; + use ::sad_rsa::rand_core::TryRng; OsRng .try_fill_bytes(&mut aead_key) .map_err(|e| SealError::RngFailure(e.to_string()))?; @@ -496,7 +496,7 @@ impl<'a> SealView for View<'a> { let padding = Oaep::::new(); let rsa_ct = public_key .encrypt( - &mut ::rsa::rand_core::UnwrapErr(&mut OsRng), + &mut ::sad_rsa::rand_core::UnwrapErr(&mut OsRng), padding, &aead_key, ) diff --git a/src/views/threshold_marker.rs b/src/views/threshold_marker.rs index 5c3b0c4..0e1d36b 100644 --- a/src/views/threshold_marker.rs +++ b/src/views/threshold_marker.rs @@ -141,11 +141,11 @@ impl<'a> MarkerView<'a> { .attributes .get(&AttrId::ThresholdParticipants) .ok_or(AttributesError::MissingThresholdParticipants)?; - let map: BTreeMap, ThresholdParticipant> = serde_cbor::from_slice(v.as_slice()) + let map: BTreeMap, ThresholdParticipant> = ciborium::from_reader(v.as_slice()) .map_err(|e| AttributesError::ThresholdMarkerCbor(e.to_string()))?; // CBOR-01: bound the registry size. A t-of-n threshold key has at most // `Limit` participants; reject an oversized map (decode amplification / - // attacker-influenced blob) well below serde_cbor's generic map cap. + // attacker-influenced blob) well below ciborium's generic map cap. const MAX_THRESHOLD_PARTICIPANTS: usize = 1024; if map.len() > MAX_THRESHOLD_PARTICIPANTS { return Err(AttributesError::ThresholdMarkerCbor( @@ -203,7 +203,8 @@ pub fn set_participants( mk: &mut Multikey, participants: &BTreeMap, ThresholdParticipant>, ) -> Result<(), Error> { - let bytes = serde_cbor::to_vec(participants) + let mut bytes = Vec::new(); + ciborium::into_writer(participants, &mut bytes) .map_err(|e| AttributesError::ThresholdMarkerCbor(e.to_string()))?; mk.attributes .insert(AttrId::ThresholdParticipants, bytes.into()); @@ -232,8 +233,10 @@ pub fn canonical_marker_bytes(mk: &Multikey) -> Result, Error> { let threshold = mv.threshold()?; let limit = mv.participant_count()?; let payload = (group, participants, threshold, limit); - serde_cbor::to_vec(&payload) - .map_err(|e| AttributesError::ThresholdMarkerCbor(e.to_string()).into()) + let mut buf = Vec::new(); + ciborium::into_writer(&payload, &mut buf) + .map_err(|e| Error::Attributes(AttributesError::ThresholdMarkerCbor(e.to_string())))?; + Ok(buf) } /// Authenticate the marker bundle: sign `canonical_marker_bytes(mk)` with From 80880ba9a8db91001229825d70c1c0fef823c8b7 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Fri, 17 Jul 2026 01:55:45 -0600 Subject: [PATCH 3/3] update changelog Signed-off-by: Dave Grantham --- CHANGELOG.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4611432..78919de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,136 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.8] - 2026-07-17 + +### Security +- Replaced the vulnerable `rsa = "0.10.0-rc.18"` crate (RUSTSEC-2023-0071, + Marvin Attack: timing side-channel key recovery) with + `sad-rsa = "0.2.3"`, a hardened fork that implements implicit rejection + to mitigate the Marvin Attack. `sad-rsa` is API-compatible with `rsa`. + All `::rsa::` references updated to `::sad_rsa::` in `src/views/rsa.rs` + and `src/mk.rs`. +- Removed unmaintained `serde_cbor` dependency (RUSTSEC-2021-0127). Replaced + with `ciborium` in both production code (`src/keysplit.rs`, + `src/views/threshold_marker.rs`) and test code (`src/serde/mod.rs`). +- Dropped `ssh-key` `crypto` feature on native target (was + `["alloc", "crypto", "ed25519"]`, now + `["alloc", "ecdsa", "ed25519", "p256", "p384", "p521"]` — matching the + wasm target). This removed the transitive `rsa` dependency via `ssh-key`; + `rsa`/`sad-rsa` is now only a direct dependency. +- Added `ByteBufVisitor` to `src/serde/de.rs` — `Nonce` and `Multikey` + non-human-readable `Deserialize` paths now use `deserialize_byte_buf` + with a visitor that accepts borrowed and owned bytes (compatible with + `serde_test`, `serde_cbor`, and `ciborium`). +- Updated `src/views/bls12381.rs` to use `new_from_bls_signature_with_codec`/ + `new_from_bls_signature_share_with_codec` (fixing deprecation warnings from + `multi-sig`'s deprecated constructors). + +### Changed +- `src/keysplit.rs`: Added `cbor_to_vec`/`cbor_from_slice` helpers; replaced + `serde_cbor::` calls with `ciborium` equivalents. +- `src/views/threshold_marker.rs`: Replaced `serde_cbor::` calls with + `ciborium::from_reader`/`into_writer`. + +### Dependencies +- `rsa = "0.10.0-rc.18"` → `sad-rsa = "0.2.3"` (both with `features = ["sha2"]`) +- Removed `serde_cbor = "0.11"` from `[dependencies]` and `[dev-dependencies]` +- Added `ciborium = "0.2"` to `[dependencies]` +- `ssh-key` (native target): dropped `crypto` feature, added `p256`/`p384`/`p521` + +### Documentation +- Updated `SECURITY.md`: removed "Known Vulnerability: `rsa`" section, added + "RSA Implementation: `sad-rsa`" section documenting the Marvin Attack + mitigation. Removed `rsa` from RC dependencies list. + +## [1.0.7] - 2026-07-16 + +### Security +- Added `MAX_DECODED_SIZE = 16 MiB` total decoded-size cap to + `Multikey::try_decode_from` (tracks consumed bytes across the attribute + decode loop, returns `Error::InputTooLarge`). Per-attribute payloads are + also individually capped by `Varbytes::MAX_DECODED_SIZE` via `multi_util`. + Mitigates CWE-400. +- Added `impl ConstantTimeEq for Multikey` — compares the canonical wire + encoding in constant time. Use `mk.ct_eq(&other)` in timing-sensitive + contexts instead of `PartialEq`. +- Documented comment field zeroization decision (R6): the `comment` field is + a plain `String` (not zeroized); key material in `attributes` is wrapped in + `Zeroizing>`. + +### Changed +- Upgraded to Edition 2024 (`edition = "2024"`, `rust-version = "1.85"`). +- Renamed test helper `gen` → `gen_key` in `src/keysplit.rs` (12 call sites) + to avoid the `gen` reserved keyword in Edition 2024. +- Added `[lints.clippy]` (pedantic/nursery/cargo at warn) and + `[lints.rust] unsafe_code = "deny"` with targeted `#![allow(...)]` for + stylistic lints. +- Added `Error::InputTooLarge { claimed, max }` error variant. +- Exported `MAX_DECODED_SIZE` from crate root. +- Major dependency upgrades: `aes-gcm` 0.10→0.11, `bcrypt-pbkdf` 0.10→0.11, + `chacha20` 0.9→0.10, `chacha20poly1305` 0.10→0.11, `hkdf` 0.12→0.13, + `ml-kem` 0.2→0.3, `poly1305` 0.8→0.9, `sha2` 0.10→0.11. +- Fixed AEAD fallback for legacy ChaCha20-encrypted keys. +- Added `AlgorithmName`/`KeyType` attributes and fixed builder + `try_from_multikey`. + +### CI +- Expanded CI from build+test to include: fmt check, clippy `-D warnings`, + MSRV (1.85) check, and cargo audit job. Updated MSRV from 1.73.0 to 1.85.0. + +### Documentation +- Added `SECURITY.md` documenting std-only status, RC dependencies, comment + zeroization, decoded-size caps, and constant-time comparison. + +### Tests +- Added `test_too_many_attributes_rejected` and `test_valid_roundtrip_with_caps`. + +## [1.0.6] - 2026-07-14 + +### Fixed +- Fixed AEAD fallback for legacy ChaCha20-encrypted keys (added + `legacy_chacha20_fallback` feature, disabled by default — AEAD failure is + a hard error so unauthenticated ciphertext is never returned as valid). +- Added security tests for AEAD fallback behavior. + +## [1.0.5] - 2026-07-14 + +### Added +- Added `AlgorithmName`/`KeyType` attributes to `AttrId`. +- Fixed builder `try_from_multikey` and added algorithm name/key type + attribute support. + +### Fixed +- Fixed builder `try_from_multikey` conversion. + +## [1.0.4] - 2026-07-14 + +### Fixed +- Fixed codec values in signature views. + +## [1.0.3] - 2026-07-14 + +### Added +- Added threshold hardening: `ThresholdKeyView` trait, DKG metadata support, + threshold marker module (`threshold_meta.rs`), encrypted threshold + parameters with ChaCha20-Poly1305 AEAD. +- Added `AttrId` variants for threshold disclosure and metadata. +- Updated `README.md` with comprehensive documentation. + +### Changed +- Bumped version for threshold hardening release. + +## [1.0.2] - 2026-07-13 + +### Changed +- Updated dependencies to published crates.io versions. + +## [1.0.1] - 2026-07-13 + +### Fixed +- Fixed codec name references after multicodec table sync. +- Bumped `p521` dependency. + ## [1.0.0] - 2026-07-13 ### Changed @@ -22,4 +152,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `frodokem_helper.rs` module (inlined from former bs-frodokem wrapper) - Added comprehensive test suite (edge cases, proptest, security) - Major dependency updates: ed25519-dalek 3, blsful 4, elliptic-curve 0.14, vsss-rs 6, ssh-key 0.7 -- Initial published release on crates.io as `multi-key` \ No newline at end of file +- Initial published release on crates.io as `multi-key` + +[1.0.8]: https://github.com/cryptidtech/multi-key/compare/v1.0.7...v1.0.8 +[1.0.7]: https://github.com/cryptidtech/multi-key/compare/v1.0.6...v1.0.7 +[1.0.6]: https://github.com/cryptidtech/multi-key/releases/tag/v1.0.6 +[1.0.5]: https://github.com/cryptidtech/multi-key/releases/tag/v1.0.5 +[1.0.4]: https://github.com/cryptidtech/multi-key/releases/tag/v1.0.4 +[1.0.3]: https://github.com/cryptidtech/multi-key/releases/tag/v1.0.3 +[1.0.2]: https://github.com/cryptidtech/multi-key/releases/tag/v1.0.2 +[1.0.1]: https://github.com/cryptidtech/multi-key/releases/tag/v1.0.1 +[1.0.0]: https://github.com/cryptidtech/multi-key/releases/tag/v1.0.0 \ No newline at end of file