diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 98ef144..ea69a58 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,14 +11,14 @@ env: jobs: msrv: - name: Verify MSRV (1.95) + name: Verify MSRV (1.96) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Rust Toolchain (1.95) - uses: dtolnay/rust-toolchain@1.95.0 + - name: Install Rust Toolchain (1.96) + uses: dtolnay/rust-toolchain@1.96.0 - name: Check run: cargo check --all-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f82810..ae72b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-09-01 + +### Added + +- Merkle-tree Lamport multikey view (`views::lamport_merkle`, behind the default `lamport` feature) wrapping the `lamport_signature_plus` 0.5.0 `Mt*` API. Supports 11 digest variants (SHA3-512/384/256, SHA2-512/384/256, BLAKE2b-512, BLAKE2s-256, BLAKE3-256, SHAKE-128/256) with 55 new codecs (`LamportMerkle*Pub/Priv/PrivShare`, codes `0x1a64`-`0x1a8e`). +- Depth attribute (`AttrId::Depth`, code 29) stamped on every merkle artifact and strictly cross-checked against the depth byte embedded in the `Mt*` wire data; mismatches return `AttributesError::DepthMismatch`. +- `MerkleStateView` trait and `Views::merkle_state_view()`: `depth`, `capacity`, `next_index`, and `remaining_signatures` introspection parsed from the tree state. +- `Builder::new_from_random_bytes_with_depth(codec, depth, rng)` (depth 1..=3 validated and stamped) and `Builder::with_depth(depth)`. +- `LAMPORT_MERKLE_KEY_CODECS` and `LAMPORT_MERKLE_KEY_SHARE_CODECS` codec lists; the 11 `LamportMerkle*PrivShare` codecs added to `KEY_SHARE_CODECS`. +- Threshold support mirroring the one-time Lamport view: `split(t, n)` produces `LamportMerkle*PrivShare` multikeys (GF(256) shares of the whole tree state); share signing produces `LamportMerkle*SigShare` multisigs; accumulation/combination happens in multi-sig. Keys are never reconstructed. +- `sign_advance` method on the `SignView` trait. Stateful signature schemes (XMSS, merkle-tree Lamport) override it to return the `Multisig` AND the advanced `Multikey` (codec, comment, and attributes preserved; only `KeyData` replaced). The caller must persist the advanced key so the consumed one-time slot is never reused. The default implementation returns `UnsupportedAlgorithm`, so stateless schemes and existing external trait impls stay source-compatible. +- `AttrId::Depth` attribute (code 29, name `depth`, one raw byte) for merkle-tree signature schemes. +- `AttributesError::DepthMismatch { expected, found }` error variant. + +### Changed + +- **Breaking:** removed the free function `multi_key::views::xmss::sign_advance`. Use the `SignView::sign_advance` trait method instead. The trait method returns the advanced key as a full `Multikey` rather than raw secret-key bytes. +- Bumped `lamport_signature_plus` from `0.5.0-rc2` to `0.5.0` (adds the merkle-tree API used by the next release). +- Raised `rust-version` from `1.95` to `1.96` (required by `lamport_signature_plus` 0.5.0). +- `Builder::with_key_bytes` now accepts unsized byte slices (`&(impl AsRef<[u8]> + ?Sized)`). +- `multi-sig` dependency raised to `1.3` (adds `AttrId::Depth`, `Builder::with_depth`, and `Multisig::depth()`). + ## [1.1.1] - 2026-08-18 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 4c45278..e8368ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "multi-key" -version = "1.1.1" +version = "1.2.0" edition = "2024" -rust-version = "1.95" +rust-version = "1.96" authors = ["Dave Grantham "] description = "Multikey self-describing cryptographic key data" repository = "https://github.com/cryptidtech/multi-key.git" @@ -47,7 +47,7 @@ mceliece348864 = { version = "1.0" } ml-dsa = "0.1.1" ml-kem = "0.3" multi-base = { version = "1.0", default-features = false } -multi-codec = "1.2" +multi-codec = "1.3" multi-hash = "1.1" multi-sig = "1.2" multi-trait = { version = "1.0", default-features = false } @@ -77,13 +77,19 @@ vsss-rs = "6.0" x25519-dalek = { version = "3.0", features = ["static_secrets", "getrandom"] } zeroize = "1" # Lamport support (feature-gated, default-enabled) -lamport_signature_plus = { version = "0.5.0-rc2", optional = true } +lamport_signature_plus = { version = "0.5.0", optional = true } sha3 = { version = "0.12", optional = true } blake2 = { version = "0.11.0-rc.6", optional = true } shake = { version = "0.1", optional = true } # XMSS support (feature-gated, default-enabled) xmss = { version = "0.1.0-pre.0", optional = true } +# Local multi-codec 1.3.0 and multi-sig with `AttrId::Depth` until published +# (remove at release). +[patch.crates-io] +multi-codec = { path = "../multi-codec" } +multi-sig = { path = "../multi-sig" } + [target.'cfg(target_arch = "wasm32")'.dependencies] blsful = { version = "4.0.0", default-features = false, features = ["rust"] } getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] } diff --git a/src/attrid.rs b/src/attrid.rs index a71b688..e3d21fb 100644 --- a/src/attrid.rs +++ b/src/attrid.rs @@ -82,6 +82,10 @@ pub enum AttrId { AlgorithmName, /// An arbitrary key type for non-standard keys (optional) KeyType, + /// Merkle-tree signature scheme depth (one raw byte; the tree holds + /// 2^depth one-time leaves). Cross-checked against the depth byte embedded + /// in the key/signature wire data. + Depth, } impl AttrId { @@ -122,6 +126,7 @@ impl AttrId { AttrId::ThresholdMetaCipher => "threshold-meta-cipher", AttrId::AlgorithmName => "algorithm-name", AttrId::KeyType => "key-type", + AttrId::Depth => "depth", } } } @@ -166,6 +171,7 @@ impl TryFrom for AttrId { 26 => Ok(AttrId::ThresholdMetaCipher), 27 => Ok(AttrId::AlgorithmName), 28 => Ok(AttrId::KeyType), + 29 => Ok(AttrId::Depth), _ => Err(AttributesError::InvalidAttributeValue(c).into()), } } @@ -230,6 +236,7 @@ impl TryFrom<&str> for AttrId { "threshold-meta-cipher" => Ok(AttrId::ThresholdMetaCipher), "algorithm-name" => Ok(AttrId::AlgorithmName), "key-type" => Ok(AttrId::KeyType), + "depth" => Ok(AttrId::Depth), _ => Err(AttributesError::InvalidAttributeName(s.to_string()).into()), } } diff --git a/src/error.rs b/src/error.rs index 213d70b..02f3e0c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -138,6 +138,15 @@ pub enum AttributesError { /// The threshold marker signature failed verification (tampered marker). #[error("Threshold marker signature verification failed")] ThresholdMarkerSigInvalid, + /// A merkle-tree depth attribute does not match the depth byte embedded in + /// the key or signature wire data. + #[error("Depth mismatch: attribute says {expected}, wire data says {found}")] + DepthMismatch { + /// depth declared by the `depth` attribute + expected: u8, + /// depth byte embedded in the wire data + found: u8, + }, } /// Conversions errors created by this library diff --git a/src/lib.rs b/src/lib.rs index 8576555..8f7ff6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,8 +141,8 @@ pub use views::threshold_meta::{ }; pub use views::{ AttrView, CipherAttrView, CipherView, ConvView, DataView, FingerprintView, KdfAttrView, - KdfView, OpenView, SealView, SignView, ThresholdAttrView, ThresholdDisclosureView, - ThresholdKeyView, ThresholdView, VerifyView, Views, + KdfView, MerkleStateView, OpenView, SealView, SignView, ThresholdAttrView, + ThresholdDisclosureView, ThresholdKeyView, ThresholdView, VerifyView, Views, }; /// Key splitting / recombination (verifiable threshold shares) @@ -155,6 +155,8 @@ pub use mk::{ MAX_DECODED_SIZE, ML_DSA_KEY_CODECS, ML_KEM_KEY_CODECS, Multikey, SLH_DSA_KEY_CODECS, X25519_KEY_CODECS, }; +#[cfg(feature = "lamport")] +pub use mk::{LAMPORT_MERKLE_KEY_CODECS, LAMPORT_MERKLE_KEY_SHARE_CODECS}; /// Nonce type pub mod nonce; diff --git a/src/mk.rs b/src/mk.rs index 373751f..78ddac9 100644 --- a/src/mk.rs +++ b/src/mk.rs @@ -1,13 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 #[cfg(feature = "lamport")] use crate::views::lamport; +#[cfg(feature = "lamport")] +use crate::views::lamport_merkle; #[cfg(feature = "xmss")] use crate::views::xmss; 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}, + KdfAttrView, KdfView, MerkleStateView, OpenView, SealView, SignView, ThresholdAttrView, + ThresholdDisclosureView, ThresholdKeyView, ThresholdView, VerifyView, Views, + error::{AttributesError, CipherError, ConversionsError, KdfError, SealError, ThresholdError}, views::{ bcrypt, bls12381, bls12381_g1_fndsa512, bls12381_g1_mayo1, bls12381_g1_mayo2, bls12381_g1_mldsa65, chacha20, classic_mceliece, ed25519, ed25519_fndsa512, ed25519_mayo2, @@ -119,15 +121,69 @@ pub const X25519_MLKEM768_KEY_CODECS: [Codec; 1] = [Codec::X25519Mlkem768Priv]; pub const ED25519_MAYO2_KEY_CODECS: [Codec; 1] = [Codec::Ed25519Mayo2Priv]; /// the list of key share codecs supported -pub const KEY_SHARE_CODECS: [Codec; 4] = [ +pub const KEY_SHARE_CODECS: [Codec; 15] = [ Codec::Bls12381G1PubShare, Codec::Bls12381G1PrivShare, Codec::Bls12381G2PubShare, - Codec::Bls12381G2PrivShare, /* - Codec::LamportSha3256PrivShare, - Codec::LamportSha3384PrivShare, - Codec::LamportSha3512PrivShare, - */ + Codec::Bls12381G2PrivShare, + Codec::LamportMerkleSha3256PrivShare, + Codec::LamportMerkleSha3384PrivShare, + Codec::LamportMerkleSha3512PrivShare, + Codec::LamportMerkleSha2256PrivShare, + Codec::LamportMerkleSha2384PrivShare, + Codec::LamportMerkleSha2512PrivShare, + Codec::LamportMerkleBlake2B512PrivShare, + Codec::LamportMerkleBlake2S256PrivShare, + Codec::LamportMerkleBlake3256PrivShare, + Codec::LamportMerkleShake128PrivShare, + Codec::LamportMerkleShake256PrivShare, /* + Codec::LamportSha3256PrivShare, + Codec::LamportSha3384PrivShare, + Codec::LamportSha3512PrivShare, + */ +]; + +/// the list of merkle-tree Lamport public and private key codecs +#[cfg(feature = "lamport")] +pub const LAMPORT_MERKLE_KEY_CODECS: [Codec; 22] = [ + Codec::LamportMerkleSha3512Pub, + Codec::LamportMerkleSha3512Priv, + Codec::LamportMerkleSha3384Pub, + Codec::LamportMerkleSha3384Priv, + Codec::LamportMerkleSha3256Pub, + Codec::LamportMerkleSha3256Priv, + Codec::LamportMerkleSha2512Pub, + Codec::LamportMerkleSha2512Priv, + Codec::LamportMerkleSha2384Pub, + Codec::LamportMerkleSha2384Priv, + Codec::LamportMerkleSha2256Pub, + Codec::LamportMerkleSha2256Priv, + Codec::LamportMerkleBlake2B512Pub, + Codec::LamportMerkleBlake2B512Priv, + Codec::LamportMerkleBlake2S256Pub, + Codec::LamportMerkleBlake2S256Priv, + Codec::LamportMerkleBlake3256Pub, + Codec::LamportMerkleBlake3256Priv, + Codec::LamportMerkleShake128Pub, + Codec::LamportMerkleShake128Priv, + Codec::LamportMerkleShake256Pub, + Codec::LamportMerkleShake256Priv, +]; + +/// the list of merkle-tree Lamport private-key-share codecs +#[cfg(feature = "lamport")] +pub const LAMPORT_MERKLE_KEY_SHARE_CODECS: [Codec; 11] = [ + Codec::LamportMerkleSha3512PrivShare, + Codec::LamportMerkleSha3384PrivShare, + Codec::LamportMerkleSha3256PrivShare, + Codec::LamportMerkleSha2512PrivShare, + Codec::LamportMerkleSha2384PrivShare, + Codec::LamportMerkleSha2256PrivShare, + Codec::LamportMerkleBlake2B512PrivShare, + Codec::LamportMerkleBlake2S256PrivShare, + Codec::LamportMerkleBlake3256PrivShare, + Codec::LamportMerkleShake128PrivShare, + Codec::LamportMerkleShake256PrivShare, ]; /// the multicodec sigil for multikey @@ -508,6 +564,42 @@ impl Views for Multikey { | Codec::LamportShake256Pub | Codec::LamportShake256Priv | Codec::LamportShake256PrivShare => Ok(Box::new(lamport::View::try_from(self)?)), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } #[cfg(feature = "xmss")] Codec::XmssSha210256Pub | Codec::XmssSha210256Priv @@ -699,6 +791,42 @@ impl Views for Multikey { | Codec::LamportShake256Pub | Codec::LamportShake256Priv | Codec::LamportShake256PrivShare => Ok(Box::new(lamport::View::try_from(self)?)), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } #[cfg(feature = "xmss")] Codec::XmssSha210256Pub | Codec::XmssSha210256Priv @@ -935,6 +1063,42 @@ impl Views for Multikey { | Codec::LamportShake256Pub | Codec::LamportShake256Priv | Codec::LamportShake256PrivShare => Ok(Box::new(lamport::View::try_from(self)?)), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } #[cfg(feature = "xmss")] Codec::XmssSha210256Pub | Codec::XmssSha210256Priv @@ -1113,6 +1277,42 @@ impl Views for Multikey { | Codec::LamportShake256Pub | Codec::LamportShake256Priv | Codec::LamportShake256PrivShare => Ok(Box::new(lamport::View::try_from(self)?)), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } #[cfg(feature = "xmss")] Codec::XmssSha210256Pub | Codec::XmssSha210256Priv @@ -1395,6 +1595,42 @@ impl Views for Multikey { | Codec::LamportShake256Pub | Codec::LamportShake256Priv | Codec::LamportShake256PrivShare => Ok(Box::new(lamport::View::try_from(self)?)), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } #[cfg(feature = "xmss")] Codec::XmssSha210256Pub | Codec::XmssSha210256Priv @@ -1446,6 +1682,42 @@ impl Views for Multikey { | Codec::LamportShake256Pub | Codec::LamportShake256Priv | Codec::LamportShake256PrivShare => Ok(Box::new(lamport::View::try_from(self)?)), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), } } @@ -1571,6 +1843,42 @@ impl Views for Multikey { | Codec::LamportShake256Pub | Codec::LamportShake256Priv | Codec::LamportShake256PrivShare => Ok(Box::new(lamport::View::try_from(self)?)), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } #[cfg(feature = "xmss")] Codec::XmssSha210256Pub | Codec::XmssSha210256Priv @@ -1586,6 +1894,40 @@ impl Views for Multikey { fn disclosure_view<'a>(&'a self) -> Result, Error> { Ok(Box::new(threshold_meta::DisclosureView::new(self))) } + + /// Provide an interface for merkle-tree state introspection + fn merkle_state_view<'a>(&'a self) -> Result, Error> { + #[cfg(feature = "lamport")] + match self.codec { + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv => { + Ok(Box::new(lamport_merkle::View::try_from(self)?)) + } + _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), + } + #[cfg(not(feature = "lamport"))] + Err(ConversionsError::UnsupportedCodec(self.codec).into()) + } } /// Multikey builder constructs private keys only. If you need a public key you @@ -1853,6 +2195,20 @@ impl Builder { | Codec::LamportBlake3256Priv | Codec::LamportShake128Priv | Codec::LamportShake256Priv => lamport::generate_private_key(codec)?.to_vec(), + #[cfg(feature = "lamport")] + Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake256Priv => { + lamport_merkle::generate_private_key(codec)?.to_vec() + } #[cfg(feature = "xmss")] Codec::XmssSha210256Priv | Codec::XmssSha216256Priv | Codec::XmssSha220256Priv => { xmss::generate_private_key(codec)?.to_vec() @@ -1868,6 +2224,45 @@ impl Builder { }) } + /// new builder for a merkle-tree Lamport key of the given depth (1..=3) + /// + /// The depth is validated against the scheme's limit and stamped as the + /// `depth` attribute on the built key. + #[cfg(feature = "lamport")] + pub fn new_from_random_bytes_with_depth( + codec: Codec, + depth: u8, + _rng: &mut impl CryptoRng, + ) -> Result { + if !(1..=3).contains(&depth) { + return Err(ThresholdError::InvalidThresholdLimit(depth as usize, 3).into()); + } + let key_bytes = match codec { + Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake256Priv => { + lamport_merkle::generate_private_key_with_depth(codec, depth)?.to_vec() + } + _ => return Err(ConversionsError::UnsupportedCodec(codec).into()), + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + attributes.insert(AttrId::Depth, Zeroizing::new(vec![depth])); + Ok(Builder { + codec, + attributes: Some(attributes), + ..Default::default() + }) + } + /// new builder from ssh_key::PublicKey source pub fn new_from_ssh_public_key(sshkey: &PublicKey) -> Result { use ssh_key::Algorithm::*; @@ -2906,10 +3301,15 @@ impl Builder { } /// add in the key bytes directly - pub fn with_key_bytes(self, bytes: &impl AsRef<[u8]>) -> Self { + pub fn with_key_bytes(self, bytes: &(impl AsRef<[u8]> + ?Sized)) -> Self { self.with_attribute(AttrId::KeyData, &bytes.as_ref().to_vec()) } + /// add in the merkle-tree depth value (one raw byte, 1..=3) + pub fn with_depth(self, depth: u8) -> Self { + self.with_attribute(AttrId::Depth, &vec![depth]) + } + /// add in the threshold value pub fn with_threshold(self, threshold: usize) -> Self { self.with_attribute(AttrId::Threshold, &Varuint(threshold).into()) diff --git a/src/views.rs b/src/views.rs index 98377e1..4c4ecd7 100644 --- a/src/views.rs +++ b/src/views.rs @@ -25,6 +25,8 @@ pub(crate) mod frodokem; pub(crate) mod frodokem_helper; #[cfg(feature = "lamport")] pub(crate) mod lamport; +#[cfg(feature = "lamport")] +pub(crate) mod lamport_merkle; pub(crate) mod mayo; pub(crate) mod ml_dsa; pub(crate) mod ml_kem; @@ -201,6 +203,24 @@ pub trait OpenView { pub trait SignView { /// try to create a Multisig by siging the passed-in data with the Multikey fn sign(&self, msg: &[u8], combined: bool, scheme: Option) -> Result; + /// Sign `msg` with a stateful signature scheme that consumes one leaf (or + /// equivalent one-time slot) per signature. Returns the Multisig AND the + /// advanced Multikey. The caller MUST persist the advanced key so the + /// consumed slot is never reused. + /// + /// The default implementation returns an error: stateless schemes sign + /// with [`SignView::sign`] and do not produce an advanced key. + fn sign_advance( + &self, + msg: &[u8], + combined: bool, + scheme: Option, + ) -> Result<(Multisig, Multikey), Error> { + let _ = (msg, combined, scheme); + Err(Error::UnsupportedAlgorithm( + "sign_advance requires a stateful signature scheme".into(), + )) + } } /// trait for doing threshold operations on multikeys @@ -254,6 +274,23 @@ pub trait VerifyView { fn verify(&self, sig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error>; } +/// trait exposing merkle-tree state introspection on a Multikey +/// (merkle-tree Lamport views; XMSS may adopt it later). +/// +/// Public keys expose [`depth`](MerkleStateView::depth) and +/// [`capacity`](MerkleStateView::capacity); the secret-only fields return an +/// error for public keys. +pub trait MerkleStateView { + /// Merkle tree depth (leaves = 2^depth). + fn depth(&self) -> Result; + /// Total number of one-time leaves (2^depth). + fn capacity(&self) -> Result; + /// Index of the next leaf that signing will consume (secret keys only). + fn next_index(&self) -> Result; + /// Number of unused leaves (secret keys only). + fn remaining_signatures(&self) -> Result; +} + /// trait for getting the other views pub trait Views { /// Provide a read-only view of the basic attributes in the viewed Multikey @@ -288,4 +325,6 @@ pub trait Views { fn verify_view<'a>(&'a self) -> Result, Error>; /// Provide an interface for threshold disclosure mode operations fn disclosure_view<'a>(&'a self) -> Result, Error>; + /// Provide an interface for merkle-tree state introspection + fn merkle_state_view<'a>(&'a self) -> Result, Error>; } diff --git a/src/views/classic_mceliece.rs b/src/views/classic_mceliece.rs index 8554704..ea38142 100644 --- a/src/views/classic_mceliece.rs +++ b/src/views/classic_mceliece.rs @@ -254,7 +254,7 @@ impl<'a> OpenView for View<'a> { } } -#[cfg(test)] +#[cfg(all(test, feature = "slow-tests"))] mod tests { use super::*; use crate::mk::MCELIECE_KEY_CODECS; @@ -285,7 +285,6 @@ mod tests { } } - #[cfg(feature = "slow-tests")] #[test] fn test_mceliece_public_key_derivation() { for codec in MCELIECE_KEY_CODECS { diff --git a/src/views/lamport.rs b/src/views/lamport.rs index 131b0e9..a8a57f1 100644 --- a/src/views/lamport.rs +++ b/src/views/lamport.rs @@ -30,19 +30,19 @@ use zeroize::Zeroizing; // These generic functions replicate the bs-lamport crate's macro-generated // wrappers, calling lamport_signature_plus directly. -type Sha3_256Digest = LamportFixedDigest; -type Sha3_384Digest = LamportFixedDigest; -type Sha3_512Digest = LamportFixedDigest; -type Sha2_256Digest = LamportFixedDigest; -type Sha2_384Digest = LamportFixedDigest; -type Sha2_512Digest = LamportFixedDigest; -type Blake2b512Digest = LamportFixedDigest; -type Blake2s256Digest = LamportFixedDigest; -type Shake128Digest = LamportExtendableDigest; -type Shake256Digest = LamportExtendableDigest; +pub(crate) type Sha3_256Digest = LamportFixedDigest; +pub(crate) type Sha3_384Digest = LamportFixedDigest; +pub(crate) type Sha3_512Digest = LamportFixedDigest; +pub(crate) type Sha2_256Digest = LamportFixedDigest; +pub(crate) type Sha2_384Digest = LamportFixedDigest; +pub(crate) type Sha2_512Digest = LamportFixedDigest; +pub(crate) type Blake2b512Digest = LamportFixedDigest; +pub(crate) type Blake2s256Digest = LamportFixedDigest; +pub(crate) type Shake128Digest = LamportExtendableDigest; +pub(crate) type Shake256Digest = LamportExtendableDigest; #[derive(Copy, Clone, Debug, Default)] -pub struct Blake3_256Digest; +pub(crate) struct Blake3_256Digest; impl LamportDigest for Blake3_256Digest { fn digest_size_in_bits() -> usize { diff --git a/src/views/lamport_merkle.rs b/src/views/lamport_merkle.rs new file mode 100644 index 0000000..0eb05b9 --- /dev/null +++ b/src/views/lamport_merkle.rs @@ -0,0 +1,1241 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Merkle-tree Lamport multikey view (`lamport_signature_plus` 0.5.0 `Mt*` API). +//! +//! ⚠️ The tree holds `2^depth` ONE-TIME leaves (depth 1, 2, or 3): every leaf +//! is a one-time Lamport key. [`SignView::sign_advance`] consumes one leaf and +//! returns the advanced key state; the caller MUST persist it. Restoring an +//! older copy of the key state can reuse a leaf and destroy the tree's +//! security. +//! +//! Depth discipline: every Multikey/Multisig this view builds carries the +//! [`AttrId::Depth`] attribute (one raw byte). Every read cross-checks that +//! byte against the depth byte embedded in the `Mt*` wire data and fails with +//! [`AttributesError::DepthMismatch`] on a mismatch. The `Mt*` wire layouts: +//! `MtVerifyingKey`/`MtSigningKey`/`MtSigningKeyShare` embed depth at byte 0 +//! and 1 respectively (`[depth, root]`, `[version, depth, next_index, …]`); +//! `MtSignature`/`MtSignatureShare` embed depth at byte 0 (`[depth, index, …]`). + +use crate::{ + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, MerkleStateView, + Multikey, SignView, ThresholdView, VerifyView, + error::{AttributesError, ConversionsError, SignError, ThresholdError, VerifyError}, + views::Views, + views::lamport::{ + Blake2b512Digest, Blake2s256Digest, Blake3_256Digest, Sha2_256Digest, Sha2_384Digest, + Sha2_512Digest, Sha3_256Digest, Sha3_384Digest, Sha3_512Digest, Shake128Digest, + Shake256Digest, + }, +}; +use lamport_signature_plus::{ + LamportDigest, MtSignature, MtSigningKey, MtSigningKeyShare, MtVerifyingKey, generate_mt_keys, +}; +use multi_codec::Codec; +use multi_hash::{Multihash, mh}; +use multi_sig::{AttrId as MsAttrId, Views as _, ms}; +use zeroize::Zeroizing; + +/// Mt state wire format version byte (lamport_signature_plus 0.5.0). +const MT_STATE_FORMAT_VERSION: u8 = 1; + +// ---- wrappers over lamport_signature_plus::merkle ---- + +mod merkle_wrapper { + use super::{ + Blake2b512Digest, Blake2s256Digest, Blake3_256Digest, LamportDigest, MtSignature, + MtSigningKey, MtSigningKeyShare, MtVerifyingKey, Sha2_256Digest, Sha2_384Digest, + Sha2_512Digest, Sha3_256Digest, Sha3_384Digest, Sha3_512Digest, Shake128Digest, + Shake256Digest, Zeroizing, generate_mt_keys, + }; + + pub fn keypair(depth: u8) -> Result<(Vec, Zeroizing>), String> { + let (sk, pk) = generate_mt_keys::(depth, rand::rng()).map_err(|e| e.to_string())?; + Ok((pk.to_bytes(), Zeroizing::new(sk.to_bytes()))) + } + + pub fn public_from_private( + secret_key_bytes: &[u8], + ) -> Result, String> { + let sk = MtSigningKey::::from_bytes(secret_key_bytes).map_err(|e| e.to_string())?; + Ok(MtVerifyingKey::from(&sk).to_bytes()) + } + + /// Sign at the next leaf. Returns the MtSignature bytes and the advanced + /// state bytes (the caller must persist them). + pub fn sign( + secret_key_bytes: &[u8], + msg: &[u8], + ) -> Result<(Vec, Zeroizing>), String> { + let mut sk = MtSigningKey::::from_bytes(secret_key_bytes).map_err(|e| e.to_string())?; + let sig = sk.sign(msg).map_err(|e| e.to_string())?; + let sig_bytes = sig.to_bytes(); + let advanced = Zeroizing::new(sk.to_bytes()); + Ok((sig_bytes, advanced)) + } + + pub fn verify( + public_key_bytes: &[u8], + signature_bytes: &[u8], + msg: &[u8], + ) -> Result<(), String> { + let pk = MtVerifyingKey::::from_bytes(public_key_bytes).map_err(|e| e.to_string())?; + let sig = MtSignature::::from_bytes(signature_bytes).map_err(|e| e.to_string())?; + pk.verify(&sig, msg).map_err(|e| e.to_string()) + } + + pub fn split( + secret_key_bytes: &[u8], + threshold: usize, + limit: usize, + ) -> Result>>, String> { + let sk = MtSigningKey::::from_bytes(secret_key_bytes).map_err(|e| e.to_string())?; + let shares = sk + .split(threshold, limit, rand::rng()) + .map_err(|e| e.to_string())?; + Ok(shares + .iter() + .map(|s| Zeroizing::new(s.to_bytes())) + .collect()) + } + + pub fn share_sign(share_bytes: &[u8], msg: &[u8]) -> Result, String> { + let mut share = + MtSigningKeyShare::::from_bytes(share_bytes).map_err(|e| e.to_string())?; + Ok(share.sign(msg).map_err(|e| e.to_string())?.to_bytes()) + } + + macro_rules! variant { + ($digest:ty, $kp:ident, $pfp:ident, $sign:ident, $verify:ident, $split:ident, $share_sign:ident) => { + pub fn $kp(depth: u8) -> Result<(Vec, Zeroizing>), String> { + keypair::<$digest>(depth) + } + pub fn $pfp(s: &[u8]) -> Result, String> { + public_from_private::<$digest>(s) + } + pub fn $sign(s: &[u8], m: &[u8]) -> Result<(Vec, Zeroizing>), String> { + sign::<$digest>(s, m) + } + pub fn $verify(p: &[u8], s: &[u8], m: &[u8]) -> Result<(), String> { + verify::<$digest>(p, s, m) + } + pub fn $split(s: &[u8], t: usize, l: usize) -> Result>>, String> { + split::<$digest>(s, t, l) + } + pub fn $share_sign(s: &[u8], m: &[u8]) -> Result, String> { + share_sign::<$digest>(s, m) + } + }; + } + + variant!( + Sha3_256Digest, + keypair_256, + public_from_private_256, + sign_256, + verify_256, + split_256, + share_sign_256 + ); + variant!( + Sha3_384Digest, + keypair_384, + public_from_private_384, + sign_384, + verify_384, + split_384, + share_sign_384 + ); + variant!( + Sha3_512Digest, + keypair_512, + public_from_private_512, + sign_512, + verify_512, + split_512, + share_sign_512 + ); + variant!( + Sha2_256Digest, + keypair_sha2_256, + public_from_private_sha2_256, + sign_sha2_256, + verify_sha2_256, + split_sha2_256, + share_sign_sha2_256 + ); + variant!( + Sha2_384Digest, + keypair_sha2_384, + public_from_private_sha2_384, + sign_sha2_384, + verify_sha2_384, + split_sha2_384, + share_sign_sha2_384 + ); + variant!( + Sha2_512Digest, + keypair_sha2_512, + public_from_private_sha2_512, + sign_sha2_512, + verify_sha2_512, + split_sha2_512, + share_sign_sha2_512 + ); + variant!( + Blake2b512Digest, + keypair_blake2b_512, + public_from_private_blake2b_512, + sign_blake2b_512, + verify_blake2b_512, + split_blake2b_512, + share_sign_blake2b_512 + ); + variant!( + Blake2s256Digest, + keypair_blake2s_256, + public_from_private_blake2s_256, + sign_blake2s_256, + verify_blake2s_256, + split_blake2s_256, + share_sign_blake2s_256 + ); + variant!( + Blake3_256Digest, + keypair_blake3_256, + public_from_private_blake3_256, + sign_blake3_256, + verify_blake3_256, + split_blake3_256, + share_sign_blake3_256 + ); + variant!( + Shake128Digest, + keypair_shake_128, + public_from_private_shake_128, + sign_shake_128, + verify_shake_128, + split_shake_128, + share_sign_shake_128 + ); + variant!( + Shake256Digest, + keypair_shake_256, + public_from_private_shake_256, + sign_shake_256, + verify_shake_256, + split_shake_256, + share_sign_shake_256 + ); +} + +fn is_merkle_priv(codec: Codec) -> bool { + matches!( + codec, + Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake256Priv + ) +} + +fn is_merkle_pub(codec: Codec) -> bool { + matches!( + codec, + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake256Pub + ) +} + +fn is_merkle_priv_share(codec: Codec) -> bool { + matches!( + codec, + Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake256PrivShare + ) +} + +fn public_codec(codec: Codec) -> Result { + match codec { + Codec::LamportMerkleSha3256Priv => Ok(Codec::LamportMerkleSha3256Pub), + Codec::LamportMerkleSha3384Priv => Ok(Codec::LamportMerkleSha3384Pub), + Codec::LamportMerkleSha3512Priv => Ok(Codec::LamportMerkleSha3512Pub), + Codec::LamportMerkleSha2256Priv => Ok(Codec::LamportMerkleSha2256Pub), + Codec::LamportMerkleSha2384Priv => Ok(Codec::LamportMerkleSha2384Pub), + Codec::LamportMerkleSha2512Priv => Ok(Codec::LamportMerkleSha2512Pub), + Codec::LamportMerkleBlake2B512Priv => Ok(Codec::LamportMerkleBlake2B512Pub), + Codec::LamportMerkleBlake2S256Priv => Ok(Codec::LamportMerkleBlake2S256Pub), + Codec::LamportMerkleBlake3256Priv => Ok(Codec::LamportMerkleBlake3256Pub), + Codec::LamportMerkleShake128Priv => Ok(Codec::LamportMerkleShake128Pub), + Codec::LamportMerkleShake256Priv => Ok(Codec::LamportMerkleShake256Pub), + _ => Err( + ConversionsError::SecretKeyFailure("not a merkle-Lamport private key".into()).into(), + ), + } +} + +fn sig_codec(codec: Codec) -> Result { + match codec { + Codec::LamportMerkleSha3256Priv => Ok(Codec::LamportMerkleSha3256Sig), + Codec::LamportMerkleSha3384Priv => Ok(Codec::LamportMerkleSha3384Sig), + Codec::LamportMerkleSha3512Priv => Ok(Codec::LamportMerkleSha3512Sig), + Codec::LamportMerkleSha2256Priv => Ok(Codec::LamportMerkleSha2256Sig), + Codec::LamportMerkleSha2384Priv => Ok(Codec::LamportMerkleSha2384Sig), + Codec::LamportMerkleSha2512Priv => Ok(Codec::LamportMerkleSha2512Sig), + Codec::LamportMerkleBlake2B512Priv => Ok(Codec::LamportMerkleBlake2B512Sig), + Codec::LamportMerkleBlake2S256Priv => Ok(Codec::LamportMerkleBlake2S256Sig), + Codec::LamportMerkleBlake3256Priv => Ok(Codec::LamportMerkleBlake3256Sig), + Codec::LamportMerkleShake128Priv => Ok(Codec::LamportMerkleShake128Sig), + Codec::LamportMerkleShake256Priv => Ok(Codec::LamportMerkleShake256Sig), + _ => Err(SignError::NotSigningKey.into()), + } +} + +fn priv_share_codec(codec: Codec) -> Result { + match codec { + Codec::LamportMerkleSha3256Priv => Ok(Codec::LamportMerkleSha3256PrivShare), + Codec::LamportMerkleSha3384Priv => Ok(Codec::LamportMerkleSha3384PrivShare), + Codec::LamportMerkleSha3512Priv => Ok(Codec::LamportMerkleSha3512PrivShare), + Codec::LamportMerkleSha2256Priv => Ok(Codec::LamportMerkleSha2256PrivShare), + Codec::LamportMerkleSha2384Priv => Ok(Codec::LamportMerkleSha2384PrivShare), + Codec::LamportMerkleSha2512Priv => Ok(Codec::LamportMerkleSha2512PrivShare), + Codec::LamportMerkleBlake2B512Priv => Ok(Codec::LamportMerkleBlake2B512PrivShare), + Codec::LamportMerkleBlake2S256Priv => Ok(Codec::LamportMerkleBlake2S256PrivShare), + Codec::LamportMerkleBlake3256Priv => Ok(Codec::LamportMerkleBlake3256PrivShare), + Codec::LamportMerkleShake128Priv => Ok(Codec::LamportMerkleShake128PrivShare), + Codec::LamportMerkleShake256Priv => Ok(Codec::LamportMerkleShake256PrivShare), + _ => Err(ThresholdError::NotASecretKey.into()), + } +} + +fn share_sig_codec(codec: Codec) -> Result { + match codec { + Codec::LamportMerkleSha3256PrivShare => Ok(Codec::LamportMerkleSha3256SigShare), + Codec::LamportMerkleSha3384PrivShare => Ok(Codec::LamportMerkleSha3384SigShare), + Codec::LamportMerkleSha3512PrivShare => Ok(Codec::LamportMerkleSha3512SigShare), + Codec::LamportMerkleSha2256PrivShare => Ok(Codec::LamportMerkleSha2256SigShare), + Codec::LamportMerkleSha2384PrivShare => Ok(Codec::LamportMerkleSha2384SigShare), + Codec::LamportMerkleSha2512PrivShare => Ok(Codec::LamportMerkleSha2512SigShare), + Codec::LamportMerkleBlake2B512PrivShare => Ok(Codec::LamportMerkleBlake2B512SigShare), + Codec::LamportMerkleBlake2S256PrivShare => Ok(Codec::LamportMerkleBlake2S256SigShare), + Codec::LamportMerkleBlake3256PrivShare => Ok(Codec::LamportMerkleBlake3256SigShare), + Codec::LamportMerkleShake128PrivShare => Ok(Codec::LamportMerkleShake128SigShare), + Codec::LamportMerkleShake256PrivShare => Ok(Codec::LamportMerkleShake256SigShare), + _ => Err(SignError::NotSigningKey.into()), + } +} + +/// Validate the depth byte embedded in `Mt*` wire data at the given offset +/// against the depth attribute stamped on the artifact. +/// The depth attribute bytes: `Some(Zeroizing>)` on a Multikey, +/// `Option>` on a Multisig. +fn depth_attr_value(mk: &Multikey) -> Result { + let attr = mk + .attributes + .get(&AttrId::Depth) + .ok_or(AttributesError::MissingThreshold)?; + if attr.len() != 1 { + return Err(AttributesError::InvalidAttributeValue(attr.len() as u8).into()); + } + Ok(attr[0]) +} + +/// The depth attribute value of a Multisig, if present. +fn multisig_depth(ms: &ms::Multisig) -> Result { + let attr = ms + .attributes + .get(&MsAttrId::Depth) + .ok_or(AttributesError::MissingThreshold)?; + if attr.len() != 1 { + return Err(AttributesError::InvalidAttributeValue(attr.len() as u8).into()); + } + Ok(attr[0]) +} + +fn check_depth_attribute(mk: &Multikey, wire_depth: u8) -> Result<(), Error> { + let expected = depth_attr_value(mk)?; + if expected != wire_depth { + return Err(AttributesError::DepthMismatch { + expected, + found: wire_depth, + } + .into()); + } + Ok(()) +} + +/// Extract and validate the depth byte embedded at `offset` of the wire data. +fn wire_depth_at(bytes: &[u8], offset: usize) -> Result { + bytes + .get(offset) + .copied() + .ok_or(AttributesError::MissingKey.into()) +} + +/// Stamp the depth attribute on a Multikey builder (kept for symmetry with +/// the multisig depth helpers). +#[allow(dead_code)] +fn with_depth(builder: Builder, depth: u8) -> Builder { + builder.with_depth(depth) +} + +/// Parse the depth byte from merkle key-state bytes: `[version, depth, next_index, …]`. +fn state_depth(state: &[u8]) -> Result { + if state.len() < 3 || state[0] != MT_STATE_FORMAT_VERSION { + return Err( + AttributesError::InvalidAttributeValue(state.first().copied().unwrap_or(0)).into(), + ); + } + Ok(state[1]) +} + +fn public_from_private(codec: Codec, secret: &[u8]) -> Result, Error> { + match codec { + Codec::LamportMerkleSha3256Priv => merkle_wrapper::public_from_private_256(secret), + Codec::LamportMerkleSha3384Priv => merkle_wrapper::public_from_private_384(secret), + Codec::LamportMerkleSha3512Priv => merkle_wrapper::public_from_private_512(secret), + Codec::LamportMerkleSha2256Priv => merkle_wrapper::public_from_private_sha2_256(secret), + Codec::LamportMerkleSha2384Priv => merkle_wrapper::public_from_private_sha2_384(secret), + Codec::LamportMerkleSha2512Priv => merkle_wrapper::public_from_private_sha2_512(secret), + Codec::LamportMerkleBlake2B512Priv => { + merkle_wrapper::public_from_private_blake2b_512(secret) + } + Codec::LamportMerkleBlake2S256Priv => { + merkle_wrapper::public_from_private_blake2s_256(secret) + } + Codec::LamportMerkleBlake3256Priv => merkle_wrapper::public_from_private_blake3_256(secret), + Codec::LamportMerkleShake128Priv => merkle_wrapper::public_from_private_shake_128(secret), + Codec::LamportMerkleShake256Priv => merkle_wrapper::public_from_private_shake_256(secret), + _ => { + return Err(ConversionsError::SecretKeyFailure( + "not a merkle-Lamport private key".into(), + ) + .into()); + } + } + .map_err(|e| ConversionsError::SecretKeyFailure(e).into()) +} + +fn keypair(codec: Codec, depth: u8) -> Result<(Vec, Zeroizing>), Error> { + match codec { + Codec::LamportMerkleSha3256Priv => merkle_wrapper::keypair_256(depth), + Codec::LamportMerkleSha3384Priv => merkle_wrapper::keypair_384(depth), + Codec::LamportMerkleSha3512Priv => merkle_wrapper::keypair_512(depth), + Codec::LamportMerkleSha2256Priv => merkle_wrapper::keypair_sha2_256(depth), + Codec::LamportMerkleSha2384Priv => merkle_wrapper::keypair_sha2_384(depth), + Codec::LamportMerkleSha2512Priv => merkle_wrapper::keypair_sha2_512(depth), + Codec::LamportMerkleBlake2B512Priv => merkle_wrapper::keypair_blake2b_512(depth), + Codec::LamportMerkleBlake2S256Priv => merkle_wrapper::keypair_blake2s_256(depth), + Codec::LamportMerkleBlake3256Priv => merkle_wrapper::keypair_blake3_256(depth), + Codec::LamportMerkleShake128Priv => merkle_wrapper::keypair_shake_128(depth), + Codec::LamportMerkleShake256Priv => merkle_wrapper::keypair_shake_256(depth), + _ => { + return Err(ConversionsError::SecretKeyFailure( + "not a merkle-Lamport private key".into(), + ) + .into()); + } + } + .map_err(|e| ConversionsError::SecretKeyFailure(e).into()) +} + +fn sign_bytes( + codec: Codec, + secret: &[u8], + msg: &[u8], +) -> Result<(Vec, Zeroizing>), Error> { + match codec { + Codec::LamportMerkleSha3256Priv => merkle_wrapper::sign_256(secret, msg), + Codec::LamportMerkleSha3384Priv => merkle_wrapper::sign_384(secret, msg), + Codec::LamportMerkleSha3512Priv => merkle_wrapper::sign_512(secret, msg), + Codec::LamportMerkleSha2256Priv => merkle_wrapper::sign_sha2_256(secret, msg), + Codec::LamportMerkleSha2384Priv => merkle_wrapper::sign_sha2_384(secret, msg), + Codec::LamportMerkleSha2512Priv => merkle_wrapper::sign_sha2_512(secret, msg), + Codec::LamportMerkleBlake2B512Priv => merkle_wrapper::sign_blake2b_512(secret, msg), + Codec::LamportMerkleBlake2S256Priv => merkle_wrapper::sign_blake2s_256(secret, msg), + Codec::LamportMerkleBlake3256Priv => merkle_wrapper::sign_blake3_256(secret, msg), + Codec::LamportMerkleShake128Priv => merkle_wrapper::sign_shake_128(secret, msg), + Codec::LamportMerkleShake256Priv => merkle_wrapper::sign_shake_256(secret, msg), + _ => return Err(SignError::NotSigningKey.into()), + } + .map_err(|e| SignError::SigningFailed(e).into()) +} + +fn verify_bytes(codec: Codec, pub_bytes: &[u8], sig: &[u8], msg: &[u8]) -> Result<(), Error> { + match codec { + Codec::LamportMerkleSha3256Pub => merkle_wrapper::verify_256(pub_bytes, sig, msg), + Codec::LamportMerkleSha3384Pub => merkle_wrapper::verify_384(pub_bytes, sig, msg), + Codec::LamportMerkleSha3512Pub => merkle_wrapper::verify_512(pub_bytes, sig, msg), + Codec::LamportMerkleSha2256Pub => merkle_wrapper::verify_sha2_256(pub_bytes, sig, msg), + Codec::LamportMerkleSha2384Pub => merkle_wrapper::verify_sha2_384(pub_bytes, sig, msg), + Codec::LamportMerkleSha2512Pub => merkle_wrapper::verify_sha2_512(pub_bytes, sig, msg), + Codec::LamportMerkleBlake2B512Pub => { + merkle_wrapper::verify_blake2b_512(pub_bytes, sig, msg) + } + Codec::LamportMerkleBlake2S256Pub => { + merkle_wrapper::verify_blake2s_256(pub_bytes, sig, msg) + } + Codec::LamportMerkleBlake3256Pub => merkle_wrapper::verify_blake3_256(pub_bytes, sig, msg), + Codec::LamportMerkleShake128Pub => merkle_wrapper::verify_shake_128(pub_bytes, sig, msg), + Codec::LamportMerkleShake256Pub => merkle_wrapper::verify_shake_256(pub_bytes, sig, msg), + _ => { + return Err(VerifyError::BadSignature("not a merkle-Lamport public key".into()).into()); + } + } + .map_err(|e| VerifyError::BadSignature(e).into()) +} + +fn split_bytes( + codec: Codec, + secret: &[u8], + t: usize, + n: usize, +) -> Result>>, Error> { + match codec { + Codec::LamportMerkleSha3256Priv => merkle_wrapper::split_256(secret, t, n), + Codec::LamportMerkleSha3384Priv => merkle_wrapper::split_384(secret, t, n), + Codec::LamportMerkleSha3512Priv => merkle_wrapper::split_512(secret, t, n), + Codec::LamportMerkleSha2256Priv => merkle_wrapper::split_sha2_256(secret, t, n), + Codec::LamportMerkleSha2384Priv => merkle_wrapper::split_sha2_384(secret, t, n), + Codec::LamportMerkleSha2512Priv => merkle_wrapper::split_sha2_512(secret, t, n), + Codec::LamportMerkleBlake2B512Priv => merkle_wrapper::split_blake2b_512(secret, t, n), + Codec::LamportMerkleBlake2S256Priv => merkle_wrapper::split_blake2s_256(secret, t, n), + Codec::LamportMerkleBlake3256Priv => merkle_wrapper::split_blake3_256(secret, t, n), + Codec::LamportMerkleShake128Priv => merkle_wrapper::split_shake_128(secret, t, n), + Codec::LamportMerkleShake256Priv => merkle_wrapper::split_shake_256(secret, t, n), + _ => return Err(ThresholdError::NotASecretKey.into()), + } + .map_err(|e| ThresholdError::ShareCombineFailed(e).into()) +} + +fn share_sign_bytes(codec: Codec, share: &[u8], msg: &[u8]) -> Result, Error> { + match codec { + Codec::LamportMerkleSha3256PrivShare => merkle_wrapper::share_sign_256(share, msg), + Codec::LamportMerkleSha3384PrivShare => merkle_wrapper::share_sign_384(share, msg), + Codec::LamportMerkleSha3512PrivShare => merkle_wrapper::share_sign_512(share, msg), + Codec::LamportMerkleSha2256PrivShare => merkle_wrapper::share_sign_sha2_256(share, msg), + Codec::LamportMerkleSha2384PrivShare => merkle_wrapper::share_sign_sha2_384(share, msg), + Codec::LamportMerkleSha2512PrivShare => merkle_wrapper::share_sign_sha2_512(share, msg), + Codec::LamportMerkleBlake2B512PrivShare => { + merkle_wrapper::share_sign_blake2b_512(share, msg) + } + Codec::LamportMerkleBlake2S256PrivShare => { + merkle_wrapper::share_sign_blake2s_256(share, msg) + } + Codec::LamportMerkleBlake3256PrivShare => merkle_wrapper::share_sign_blake3_256(share, msg), + Codec::LamportMerkleShake128PrivShare => merkle_wrapper::share_sign_shake_128(share, msg), + Codec::LamportMerkleShake256PrivShare => merkle_wrapper::share_sign_shake_256(share, msg), + _ => return Err(SignError::NotSigningKey.into()), + } + .map_err(|e| SignError::SigningFailed(e).into()) +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_merkle_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_merkle_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + is_merkle_priv_share(self.mk.codec) + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let depth = wire_depth_at(secret_bytes.as_slice(), 1)?; + check_depth_attribute(self.mk, depth)?; + let pub_bytes = public_from_private(self.mk.codec, secret_bytes.as_slice())?; + Builder::new(public_codec(self.mk.codec)?) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .with_depth(depth) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Merkle-Lamport not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Merkle-Lamport not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + /// Not supported for merkle keys: signing consumes a one-time leaf and the + /// caller must persist the advanced state. Use [`SignView::sign_advance`]. + fn sign( + &self, + _msg: &[u8], + _combined: bool, + _scheme: Option, + ) -> Result { + Err(Error::UnsupportedAlgorithm( + "merkle-Lamport keys are stateful; use sign_advance to consume a leaf and persist \ + the advanced state" + .into(), + )) + } + + /// Sign at the next leaf, returning the Multisig AND the advanced Multikey. + /// The caller MUST persist the advanced key so the consumed leaf index is + /// never reused. + fn sign_advance( + &self, + msg: &[u8], + combined: bool, + _scheme: Option, + ) -> Result<(ms::Multisig, Multikey), Error> { + let codec = self.mk.codec; + let key_bytes = { + let kd = self.mk.data_view()?; + kd.key_bytes()? + }; + // A private-key SHARE signs to a signature SHARE (stateful, but the + // share state is not persisted here — combine on the multisig instead); + // a full private key signs with its next leaf and MUST be advanced. + if is_merkle_priv_share(codec) { + let sig = share_sign_bytes(codec, key_bytes.as_slice(), msg)?; + let out_codec = share_sig_codec(codec)?; + let mut msb = ms::Builder::new(out_codec).with_signature_bytes(&sig); + if combined { + msb = msb.with_message_bytes(&msg); + } + let depth = state_depth(key_bytes.as_slice())?; + let mut sig_ms = msb.try_build()?; + sig_ms.attributes.insert(MsAttrId::Depth, vec![depth]); + return Ok((sig_ms, self.mk.clone())); + } + if !is_merkle_priv(codec) { + return Err(SignError::NotSigningKey.into()); + } + let depth = state_depth(key_bytes.as_slice())?; + check_depth_attribute(self.mk, depth)?; + let (sig, advanced) = sign_bytes(codec, key_bytes.as_slice(), msg)?; + let out_codec = sig_codec(codec)?; + let mut msb = ms::Builder::new(out_codec).with_signature_bytes(&sig); + if combined { + msb = msb.with_message_bytes(&msg); + } + let mut sig_ms = msb.try_build()?; + sig_ms.attributes.insert(MsAttrId::Depth, vec![depth]); + let advanced_mk = Builder::new(codec) + .with_comment(&self.mk.comment) + .with_key_bytes(advanced.as_slice()) + .with_depth(depth) + .try_build()?; + Ok((sig_ms, advanced_mk)) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &ms::Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + self.mk.conv_view()?.to_public_key()? + } else { + self.mk.clone() + }; + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + // Cross-check the pubkey's depth attribute against the MtVerifyingKey + // wire depth (byte 0) and the signature wire depth (byte 0). + let key_depth = wire_depth_at(key_bytes.as_slice(), 0)?; + check_depth_attribute(&pubmk, key_depth)?; + let sv = multisig.data_view()?; + let sig = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + let sig_depth = wire_depth_at(&sig, 0)?; + if sig_depth != key_depth { + return Err(AttributesError::DepthMismatch { + expected: key_depth, + found: sig_depth, + } + .into()); + } + if let Ok(ms_depth) = multisig_depth(multisig) + && ms_depth != sig_depth + { + return Err(AttributesError::DepthMismatch { + expected: ms_depth, + found: sig_depth, + } + .into()); + } + verify_bytes(pubmk.codec, key_bytes.as_slice(), &sig, msg_bytes) + } +} + +impl<'a> ThresholdView for View<'a> { + /// Split a merkle-Lamport signing key into `threshold`-of-`limit` key + /// shares. Each returned Multikey is a `LamportMerkle*PrivShare` whose key + /// data is a GF(256) share of the whole tree state that can sign + /// independently to a signature share. + fn split(&self, threshold: usize, limit: usize) -> Result, Error> { + if !self.mk.attr_view()?.is_secret_key() { + return Err(ThresholdError::NotASecretKey.into()); + } + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let depth = state_depth(secret_bytes.as_slice())?; + check_depth_attribute(self.mk, depth)?; + let share_codec = priv_share_codec(self.mk.codec)?; + let shares = split_bytes(self.mk.codec, secret_bytes.as_slice(), threshold, limit)?; + shares + .into_iter() + .map(|share| { + Builder::new(share_codec) + .with_comment(&self.mk.comment) + .with_key_bytes(&share) + .with_depth(depth) + .try_build() + }) + .collect() + } + + /// Not supported: merkle-Lamport threshold signing never reconstructs the + /// signing key. Combine signature shares on the multisig instead. + fn add_share(&self, _share: &Multikey) -> Result { + Err(ThresholdError::ShareCombineFailed( + "merkle-Lamport threshold signing does not reconstruct the signing key; combine \ + signature shares instead" + .into(), + ) + .into()) + } + + /// Not supported: see [`add_share`](Self::add_share). + fn combine(&self) -> Result { + Err(ThresholdError::ShareCombineFailed( + "merkle-Lamport threshold signing does not reconstruct the signing key; combine \ + signature shares instead" + .into(), + ) + .into()) + } + + /// Merkle-Lamport does not use encrypted threshold params; delegate to + /// [`split`](Self::split). + fn split_with_disclosure( + &self, + threshold: usize, + limit: usize, + _mode: crate::views::ThresholdDisclosure, + _meta_key: Option<&Multikey>, + ) -> Result, Error> { + self.split(threshold, limit) + } + + /// Merkle-Lamport does not use encrypted threshold params; delegate to + /// [`add_share`](Self::add_share). + fn add_share_with_meta( + &self, + share: &Multikey, + _meta_key: Option<&Multikey>, + ) -> Result { + self.add_share(share) + } + + /// Merkle-Lamport does not use encrypted threshold params; delegate to + /// [`combine`](Self::combine). + fn combine_with_meta(&self, _meta_key: Option<&Multikey>) -> Result { + self.combine() + } +} + +impl<'a> MerkleStateView for View<'a> { + fn depth(&self) -> Result { + let key_bytes = { + let kd = self.mk.data_view()?; + kd.key_bytes()? + }; + let depth = if self.is_secret_key() { + state_depth(key_bytes.as_slice())? + } else { + // MtVerifyingKey wire: [depth, root] + wire_depth_at(key_bytes.as_slice(), 0)? + }; + check_depth_attribute(self.mk, depth)?; + Ok(depth) + } + + fn capacity(&self) -> Result { + Ok(1usize << self.depth()?) + } + + fn next_index(&self) -> Result { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + let key_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + // [version, depth, next_index, …] + Ok(usize::from(wire_depth_at(key_bytes.as_slice(), 2)?)) + } + + fn remaining_signatures(&self) -> Result { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + let key_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let sk = MtSigningKey::::from_bytes(key_bytes.as_slice()); + // The digest type does not affect remaining count; parse generically + // per codec instead. + let _ = sk; + remaining_from_state(key_bytes.as_slice(), self.mk.codec) + } +} + +/// Compute remaining signatures from state bytes without decoding secret +/// material: remaining = capacity - consumed, where the state carries exactly +/// `remaining` leaf key blocks after the leaf-hash section. +fn remaining_from_state(state: &[u8], codec: Codec) -> Result { + // state = [version, depth, next_index] || 2^depth leaf hashes || remaining secret blocks + if state.len() < 3 { + return Err(AttributesError::MissingKey.into()); + } + let depth = state[1]; + if !(1..=3).contains(&depth) { + return Err(AttributesError::InvalidAttributeValue(depth).into()); + } + let capacity = 1usize << depth; + let hash_size = digest_bytes_for(codec)?; + let leaves_len = capacity + .checked_mul(hash_size) + .ok_or(Error::InputTooLarge { + claimed: capacity, + max: MAX_DECODED_SIZE, + })?; + let header = 3 + leaves_len; + if state.len() < header { + return Err(AttributesError::MissingKey.into()); + } + let material_size = signing_key_material_size_for(codec)?; + let rest = state.len() - header; + if !rest.is_multiple_of(material_size) { + return Err(AttributesError::InvalidAttributeValue(depth).into()); + } + let next_index = usize::from(state[2]); + if next_index + rest / material_size != capacity { + return Err(AttributesError::InvalidAttributeValue(depth).into()); + } + Ok(rest / material_size) +} + +/// Digest size in bytes for a merkle-Lamport codec. +fn digest_bytes_for(codec: Codec) -> Result { + const D: usize = 32; + const D48: usize = 48; + const D64: usize = 64; + Ok(match codec { + Codec::LamportMerkleSha3256Pub + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3256PrivShare + | Codec::LamportMerkleSha3256Sig + | Codec::LamportMerkleSha3256SigShare + | Codec::LamportMerkleSha2256Pub + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2256PrivShare + | Codec::LamportMerkleSha2256Sig + | Codec::LamportMerkleSha2256SigShare + | Codec::LamportMerkleBlake2S256Pub + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake2S256PrivShare + | Codec::LamportMerkleBlake2S256Sig + | Codec::LamportMerkleBlake2S256SigShare + | Codec::LamportMerkleBlake3256Pub + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleBlake3256PrivShare + | Codec::LamportMerkleBlake3256Sig + | Codec::LamportMerkleBlake3256SigShare => D, + Codec::LamportMerkleSha3384Pub + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3384PrivShare + | Codec::LamportMerkleSha3384Sig + | Codec::LamportMerkleSha3384SigShare + | Codec::LamportMerkleSha2384Pub + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2384PrivShare + | Codec::LamportMerkleSha2384Sig + | Codec::LamportMerkleSha2384SigShare => D48, + Codec::LamportMerkleSha3512Pub + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3512PrivShare + | Codec::LamportMerkleSha3512Sig + | Codec::LamportMerkleSha3512SigShare + | Codec::LamportMerkleSha2512Pub + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2512PrivShare + | Codec::LamportMerkleSha2512Sig + | Codec::LamportMerkleSha2512SigShare + | Codec::LamportMerkleBlake2B512Pub + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2B512PrivShare + | Codec::LamportMerkleBlake2B512Sig + | Codec::LamportMerkleBlake2B512SigShare + | Codec::LamportMerkleShake128Pub + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake128PrivShare + | Codec::LamportMerkleShake128Sig + | Codec::LamportMerkleShake128SigShare + | Codec::LamportMerkleShake256Pub + | Codec::LamportMerkleShake256Priv + | Codec::LamportMerkleShake256PrivShare + | Codec::LamportMerkleShake256Sig + | Codec::LamportMerkleShake256SigShare => D64, + _ => return Err(AttributesError::UnsupportedCodec(codec).into()), + }) +} + +/// Lamport signing key secret-material size (bits × bytes × 2) for a codec. +fn signing_key_material_size_for(codec: Codec) -> Result { + let digest = digest_bytes_for(codec)?; + let bits = digest * 8; + Ok(2 * bits * digest) +} + +pub(crate) fn generate_private_key(codec: Codec) -> Result>, Error> { + generate_private_key_with_depth(codec, 1) +} + +/// Generate a merkle-Lamport private key of the given depth (1..=3). +pub(crate) fn generate_private_key_with_depth( + codec: Codec, + depth: u8, +) -> Result>, Error> { + if !(1..=3).contains(&depth) { + return Err(ThresholdError::InvalidThresholdLimit(depth as usize, 3).into()); + } + let (_pk, sk) = keypair(codec, depth)?; + Ok(sk) +} + +const MAX_DECODED_SIZE: usize = crate::mk::MAX_DECODED_SIZE; + +#[cfg(test)] +mod tests { + use super::*; + use crate::LAMPORT_MERKLE_KEY_CODECS; + use multi_sig::AttrId as MsAttrId; + + fn build_priv(codec: Codec, depth: u8) -> Multikey { + Builder::new_from_random_bytes_with_depth(codec, depth, &mut rand::rng()) + .unwrap() + .try_build() + .unwrap() + } + + fn merkle_priv_codecs() -> impl Iterator { + LAMPORT_MERKLE_KEY_CODECS.into_iter().filter(|c| { + matches!( + c, + Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake256Priv + ) + }) + } + + #[test] + fn test_merkle_roundtrip_all_digests_depth1() { + for codec in merkle_priv_codecs() { + let sk = build_priv(codec, 1); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + // depth attribute stamped and cross-checked + assert_eq!(pk.attributes.get(&AttrId::Depth).map(|b| b[0]), Some(1)); + + let msg = b"merkle roundtrip"; + let (ms, advanced) = sk + .sign_view() + .unwrap() + .sign_advance(msg, false, None) + .unwrap(); + assert_eq!(ms.attributes.get(&MsAttrId::Depth).map(|b| b[0]), Some(1)); + + pk.verify_view().unwrap().verify(&ms, Some(msg)).unwrap(); + sk.verify_view().unwrap().verify(&ms, Some(msg)).unwrap(); + + // tampered message must fail + assert!( + pk.verify_view() + .unwrap() + .verify(&ms, Some(b"tampered")) + .is_err() + ); + + // advanced key has one fewer remaining signature + let sv = sk.merkle_state_view().unwrap(); + let av = advanced.merkle_state_view().unwrap(); + assert_eq!(av.next_index().unwrap(), sv.next_index().unwrap() + 1); + } + } + + #[test] + fn test_merkle_sign_rejected_sign_advance_required() { + let sk = build_priv(Codec::LamportMerkleSha3256Priv, 1); + assert!(sk.sign_view().unwrap().sign(b"x", false, None).is_err()); + } + + #[test] + fn test_merkle_depth_validation() { + // depth 0 and 4 must fail + assert!( + Builder::new_from_random_bytes_with_depth( + Codec::LamportMerkleSha3256Priv, + 0, + &mut rand::rng() + ) + .is_err() + ); + assert!( + Builder::new_from_random_bytes_with_depth( + Codec::LamportMerkleSha3256Priv, + 4, + &mut rand::rng() + ) + .is_err() + ); + } + + #[test] + fn test_merkle_depth3_sha3_256() { + let sk = build_priv(Codec::LamportMerkleSha3256Priv, 3); + let sv = sk.merkle_state_view().unwrap(); + assert_eq!(sv.depth().unwrap(), 3); + assert_eq!(sv.capacity().unwrap(), 8); + assert_eq!(sv.next_index().unwrap(), 0); + assert_eq!(sv.remaining_signatures().unwrap(), 8); + + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + let msg = b"depth three"; + let (ms, mut advanced) = sk + .sign_view() + .unwrap() + .sign_advance(msg, true, None) + .unwrap(); + assert_eq!(ms.message, msg.to_vec()); + pk.verify_view().unwrap().verify(&ms, Some(msg)).unwrap(); + // exhaust the tree + for i in 1..8 { + let m = format!("m{i}"); + let (m2, adv2) = advanced + .sign_view() + .unwrap() + .sign_advance(m.as_bytes(), false, None) + .unwrap(); + pk.verify_view() + .unwrap() + .verify(&m2, Some(m.as_bytes())) + .unwrap(); + advanced = adv2; + } + let av = advanced.merkle_state_view().unwrap(); + assert_eq!(av.remaining_signatures().unwrap(), 0); + assert!( + advanced + .sign_view() + .unwrap() + .sign_advance(b"m9", false, None) + .is_err() + ); + } + + #[test] + fn test_merkle_depth_mismatch_rejected() { + let sk = build_priv(Codec::LamportMerkleSha3256Priv, 2); + // tamper the depth attribute: wire says 2, attr now says 3 + let mut tampered = sk.clone(); + tampered + .attributes + .insert(AttrId::Depth, Zeroizing::new(vec![3])); + assert!(tampered.conv_view().unwrap().to_public_key().is_err()); + assert!(tampered.merkle_state_view().unwrap().depth().is_err()); + // sign_advance cross-checks too + assert!( + tampered + .sign_view() + .unwrap() + .sign_advance(b"m", false, None) + .is_err() + ); + } + + #[test] + fn test_merkle_threshold_flow() { + let sk = build_priv(Codec::LamportMerkleSha3256Priv, 1); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + let msg = b"threshold merkle"; + + // split 2-of-3 + let shares = sk.threshold_view().unwrap().split(2, 3).unwrap(); + assert_eq!(shares.len(), 3); + for share in &shares { + assert!(share.attr_view().unwrap().is_secret_key_share()); + assert_eq!(share.attributes.get(&AttrId::Depth).map(|b| b[0]), Some(1)); + } + + // two participants sign to signature shares + let s1 = shares[0] + .sign_view() + .unwrap() + .sign_advance(msg, false, None) + .unwrap() + .0; + let s2 = shares[1] + .sign_view() + .unwrap() + .sign_advance(msg, false, None) + .unwrap() + .0; + + // accumulate on the multisig and combine + let mut acc = ms::Builder::new(Codec::LamportMerkleSha3256Sig) + .with_message_bytes(&msg) + .try_build() + .unwrap(); + for share_ms in [&s1, &s2] { + let next = acc.threshold_view().unwrap().add_share(share_ms).unwrap(); + acc = next; + } + let combined = acc.threshold_view().unwrap().combine().unwrap(); + pk.verify_view() + .unwrap() + .verify(&combined, Some(msg)) + .unwrap(); + } + + #[test] + fn test_merkle_introspection_depth1() { + let sk = build_priv(Codec::LamportMerkleSha2256Priv, 1); + let sv = sk.merkle_state_view().unwrap(); + assert_eq!(sv.depth().unwrap(), 1); + assert_eq!(sv.capacity().unwrap(), 2); + assert_eq!(sv.next_index().unwrap(), 0); + assert_eq!(sv.remaining_signatures().unwrap(), 2); + + // public keys expose depth/capacity only + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + let pv = pk.merkle_state_view().unwrap(); + assert_eq!(pv.depth().unwrap(), 1); + assert_eq!(pv.capacity().unwrap(), 2); + assert!(pv.next_index().is_err()); + assert!(pv.remaining_signatures().is_err()); + } + + #[test] + fn test_merkle_sig_index_absent() { + let sk = build_priv(Codec::LamportMerkleBlake3256Priv, 1); + let (ms, _adv) = sk + .sign_view() + .unwrap() + .sign_advance(b"m", false, None) + .unwrap(); + // leaf index travels inside the Mt wire format, not as SigIndex + assert!(ms.sig_index().is_none()); + let _ = MsAttrId::SigData; + } +} diff --git a/src/views/x25519_mceliece348864.rs b/src/views/x25519_mceliece348864.rs index e900734..5478f7d 100644 --- a/src/views/x25519_mceliece348864.rs +++ b/src/views/x25519_mceliece348864.rs @@ -326,7 +326,7 @@ impl<'a> OpenView for View<'a> { } } -#[cfg(test)] +#[cfg(all(test, feature = "slow-tests"))] mod tests { use super::*; use crate::views::Views; diff --git a/src/views/xmss.rs b/src/views/xmss.rs index 562679f..a31609a 100644 --- a/src/views/xmss.rs +++ b/src/views/xmss.rs @@ -9,8 +9,9 @@ //! //! NOTE: this view signs at whatever index the stored secret key currently holds; //! it does NOT persist the advanced secret key. The authoritative advance-and- -//! persist is owned by the keystore (`bs-keystore`), which calls [`sign_advance`] -//! to obtain both the signature and the advanced secret key in one step. +//! persist is owned by the keystore (`bs-keystore`), which calls +//! [`SignView::sign_advance`] to obtain both the signature and the advanced +//! secret key in one step. use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, @@ -228,27 +229,13 @@ fn build_multisig( Ok(ms.try_build()?) } -/// Sign `msg`, returning the [`Multisig`](multi_sig::Multisig) AND the advanced -/// secret key bytes. The caller (keystore) MUST persist the advanced key so the -/// consumed leaf index is never reused. -/// -/// Returns an error if the multikey is not an XMSS secret key. -pub fn sign_advance( - mk: &Multikey, - msg: &[u8], - combined: bool, -) -> Result<(multi_sig::Multisig, Zeroizing>), Error> { - let attr = mk.attr_view()?; - if !attr.is_secret_key() || !is_xmss_priv(mk.codec) { - return Err(SignError::NotSigningKey.into()); - } - let secret_bytes = { - let kd = mk.data_view()?; - kd.secret_bytes()? - }; - let sig = sign_bytes(mk.codec, secret_bytes.as_slice(), msg)?; - let ms = build_multisig(mk.codec, &sig, msg, combined)?; - Ok((ms, sig.advanced_secret_key.clone())) +/// Rebuild `mk` with only the `KeyData` attribute replaced by `new_bytes`. +/// Codec, comment, and all other attributes are copied unchanged. +fn with_key_data(mk: &Multikey, new_bytes: &[u8]) -> Result { + Builder::new(mk.codec) + .with_comment(&mk.comment) + .with_key_bytes(new_bytes) + .try_build() } pub(crate) struct View<'a> { @@ -353,6 +340,31 @@ impl<'a> SignView for View<'a> { let sig = sign_bytes(self.mk.codec, secret_bytes.as_slice(), msg)?; build_multisig(self.mk.codec, &sig, msg, combined) } + + /// Sign `msg` and return the Multisig AND the advanced Multikey. The caller + /// (keystore) MUST persist the advanced key so the consumed leaf index is + /// never reused. + /// + /// Returns an error if the multikey is not an XMSS secret key. + fn sign_advance( + &self, + msg: &[u8], + combined: bool, + _scheme: Option, + ) -> Result<(multi_sig::Multisig, Multikey), Error> { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() || !is_xmss_priv(self.mk.codec) { + return Err(SignError::NotSigningKey.into()); + } + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let sig = sign_bytes(self.mk.codec, secret_bytes.as_slice(), msg)?; + let ms = build_multisig(self.mk.codec, &sig, msg, combined)?; + let advanced = with_key_data(self.mk, &sig.advanced_secret_key)?; + Ok((ms, advanced)) + } } impl<'a> VerifyView for View<'a> { @@ -434,9 +446,14 @@ mod tests { .try_build() .unwrap(); - let (ms0, advanced) = sign_advance(&sk, b"first", false).unwrap(); + let (ms0, advanced) = sk + .sign_view() + .unwrap() + .sign_advance(b"first", false, None) + .unwrap(); assert_eq!(ms0.sig_index(), Some(0)); // advanced secret key now points at index 1 - assert_eq!(xmss_wrapper::current_index(&advanced).unwrap(), 1); + let advanced_bytes = advanced.data_view().unwrap().secret_bytes().unwrap(); + assert_eq!(xmss_wrapper::current_index(&advanced_bytes).unwrap(), 1); } } diff --git a/tests/merkle_lamport_tests.rs b/tests/merkle_lamport_tests.rs new file mode 100644 index 0000000..d037dcf --- /dev/null +++ b/tests/merkle_lamport_tests.rs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +//! End-to-end merkle-tree Lamport flow: keygen, `sign_advance`, verify, +//! threshold split, share-sign, multi-sig accumulate, combine, verify. + +#![cfg(feature = "lamport")] +#![allow(clippy::uninlined_format_args)] + +use multi_codec::Codec; +use multi_key::{Builder, Multikey, Views as _}; +use multi_sig::{Multisig, Views as _}; + +#[test] +fn test_merkle_sign_advance_and_verify() { + let mut rng = rand::rng(); + let sk = + Builder::new_from_random_bytes_with_depth(Codec::LamportMerkleSha2256Priv, 1, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let sv = sk.merkle_state_view().unwrap(); + assert_eq!(sv.depth().unwrap(), 1); + assert_eq!(sv.capacity().unwrap(), 2); + assert_eq!(sv.remaining_signatures().unwrap(), 2); + + let msg = b"integration merkle message"; + let (ms, advanced) = sk + .sign_view() + .unwrap() + .sign_advance(msg, true, None) + .unwrap(); + + // depth attribute travels on the signature + assert_eq!(ms.depth(), Some(1)); + + pk.verify_view().unwrap().verify(&ms, Some(msg)).unwrap(); + + // advanced key must be persisted and reflect the consumed leaf + let av = advanced.merkle_state_view().unwrap(); + assert_eq!(av.next_index().unwrap(), 1); + assert_eq!(av.remaining_signatures().unwrap(), 1); + + // roundtrip the advanced key through wire encoding + let bytes: Vec = advanced.clone().into(); + let restored = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(restored, advanced); + let rv = restored.merkle_state_view().unwrap(); + assert_eq!(rv.next_index().unwrap(), 1); +} + +#[test] +fn test_merkle_threshold_split_sign_combine_verify() { + let mut rng = rand::rng(); + let sk = + Builder::new_from_random_bytes_with_depth(Codec::LamportMerkleSha3256Priv, 1, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + // split 2-of-3 + let shares = sk.threshold_view().unwrap().split(2, 3).unwrap(); + assert_eq!(shares.len(), 3); + + let msg = b"threshold integration message"; + + // two participants sign (detached) to signature shares + let sig_shares: Vec = shares + .iter() + .take(2) + .map(|share| { + let (ms, _adv) = share + .sign_view() + .unwrap() + .sign_advance(msg, false, None) + .unwrap(); + ms + }) + .collect(); + + // accumulate and combine on the multisig + let mut acc = multi_sig::Builder::new(Codec::LamportMerkleSha3256Sig) + .with_message_bytes(&msg) + .try_build() + .unwrap(); + for share in &sig_shares { + let next = acc.threshold_view().unwrap().add_share(share).unwrap(); + acc = next; + } + assert_eq!(acc.depth(), Some(1)); + + let combined = acc.threshold_view().unwrap().combine().unwrap(); + assert_eq!(combined.depth(), Some(1)); + + // verify under the tree-root public key + pk.verify_view() + .unwrap() + .verify(&combined, Some(msg)) + .unwrap(); +} + +#[test] +fn test_merkle_builder_depth_validation() { + let mut rng = rand::rng(); + for bad in [0u8, 4, 255] { + assert!( + Builder::new_from_random_bytes_with_depth( + Codec::LamportMerkleSha2256Priv, + bad, + &mut rng + ) + .is_err(), + "depth {bad} must be rejected" + ); + } +}