diff --git a/CHANGELOG.md b/CHANGELOG.md index ae72b27..cb0c82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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.2] - 2026-09-01 + +### Fixed + +- Merkle-tree Lamport keys generated with `Builder::new_from_random_bytes` now stamp the mandatory `depth` attribute (value 1). Previously such keys were generated with a depth-1 tree but carried no `depth` attribute, so every merkle operation on them (`to_public_key`, `sign_advance`, `merkle_state_view`) failed with `AttributesError::MissingThreshold`. Use `new_from_random_bytes_with_depth` for depths 2 or 3. + ## [1.2.0] - 2026-09-01 ### Added @@ -222,6 +228,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Initial published release on crates.io as `multi-key`. [1.1.1]: https://github.com/cryptidtech/multi-key/compare/v1.1.0...v1.1.1 +[1.2.2]: https://github.com/cryptidtech/multi-key/compare/v1.2.1...v1.2.2 +[1.2.1]: https://github.com/cryptidtech/multi-key/compare/v1.2.0...v1.2.1 +[1.2.0]: https://github.com/cryptidtech/multi-key/compare/v1.1.1...v1.2.0 [1.1.0]: https://github.com/cryptidtech/multi-key/compare/v1.0.9...v1.1.0 [1.0.9]: https://github.com/cryptidtech/multi-key/compare/v1.0.8...v1.0.9 [1.0.8]: https://github.com/cryptidtech/multi-key/compare/v1.0.7...v1.0.8 diff --git a/Cargo.toml b/Cargo.toml index d74efaf..ab14d18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-key" -version = "1.2.1" +version = "1.2.2" edition = "2024" rust-version = "1.96" authors = ["Dave Grantham "] diff --git a/src/mk.rs b/src/mk.rs index 78ddac9..1094281 100644 --- a/src/mk.rs +++ b/src/mk.rs @@ -2207,7 +2207,7 @@ impl Builder { | Codec::LamportMerkleBlake3256Priv | Codec::LamportMerkleShake128Priv | Codec::LamportMerkleShake256Priv => { - lamport_merkle::generate_private_key(codec)?.to_vec() + lamport_merkle::generate_private_key_with_depth(codec, 1)?.to_vec() } #[cfg(feature = "xmss")] Codec::XmssSha210256Priv | Codec::XmssSha216256Priv | Codec::XmssSha220256Priv => { @@ -2217,6 +2217,13 @@ impl Builder { }; let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); + // Merkle-tree Lamport keys embed a mandatory depth byte; a depth-1 + // (capacity 2) tree is the default here, mirroring + // `new_from_random_bytes_with_depth`. + #[cfg(feature = "lamport")] + if Self::lamport_merkle_codec(codec) { + attributes.insert(AttrId::Depth, Zeroizing::new(vec![1])); + } Ok(Builder { codec, attributes: Some(attributes), @@ -2224,6 +2231,25 @@ impl Builder { }) } + /// True when `codec` is a merkle-tree Lamport private key codec. + #[cfg(feature = "lamport")] + fn lamport_merkle_codec(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 + ) + } + /// 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 diff --git a/src/views/lamport_merkle.rs b/src/views/lamport_merkle.rs index 0eb05b9..a162154 100644 --- a/src/views/lamport_merkle.rs +++ b/src/views/lamport_merkle.rs @@ -989,6 +989,10 @@ fn signing_key_material_size_for(codec: Codec) -> Result { Ok(2 * bits * digest) } +/// Generate a depth-1 merkle-Lamport private key (unused since 1.2.2: the +/// plain `Builder::new_from_random_bytes` path stamps depth inline; kept for +/// symmetry with the other view helpers). +#[allow(dead_code)] pub(crate) fn generate_private_key(codec: Codec) -> Result>, Error> { generate_private_key_with_depth(codec, 1) } @@ -1101,6 +1105,31 @@ mod tests { ); } + #[test] + fn test_merkle_plain_ctor_stamps_depth() { + // The plain constructor generates a depth-1 tree and must stamp the + // mandatory depth attribute (fixed in 1.2.2). + let sk = + Builder::new_from_random_bytes(Codec::LamportMerkleBlake3256Priv, &mut rand::rng()) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(sk.attributes.get(&AttrId::Depth).map(|b| b[0]), Some(1)); + + // the key must be fully usable: public derivation and signing + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + let msg = b"plain ctor roundtrip"; + let (ms, advanced) = sk + .sign_view() + .unwrap() + .sign_advance(msg, false, None) + .unwrap(); + pk.verify_view().unwrap().verify(&ms, Some(msg)).unwrap(); + let av = advanced.merkle_state_view().unwrap(); + assert_eq!(av.next_index().unwrap(), 1); + assert_eq!(av.remaining_signatures().unwrap(), 1); + } + #[test] fn test_merkle_depth3_sha3_256() { let sk = build_priv(Codec::LamportMerkleSha3256Priv, 3);