Skip to content

Commit 8a8ed11

Browse files
authored
Merge pull request #16 from cryptidtech/fix/depth-footgun
fix lamport merkle depth footgun
2 parents 948648f + 6ba9793 commit 8a8ed11

4 files changed

Lines changed: 66 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.2] - 2026-09-01
9+
10+
### Fixed
11+
12+
- 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.
13+
814
## [1.2.0] - 2026-09-01
915

1016
### Added
@@ -222,6 +228,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
222228
- Initial published release on crates.io as `multi-key`.
223229

224230
[1.1.1]: https://github.com/cryptidtech/multi-key/compare/v1.1.0...v1.1.1
231+
[1.2.2]: https://github.com/cryptidtech/multi-key/compare/v1.2.1...v1.2.2
232+
[1.2.1]: https://github.com/cryptidtech/multi-key/compare/v1.2.0...v1.2.1
233+
[1.2.0]: https://github.com/cryptidtech/multi-key/compare/v1.1.1...v1.2.0
225234
[1.1.0]: https://github.com/cryptidtech/multi-key/compare/v1.0.9...v1.1.0
226235
[1.0.9]: https://github.com/cryptidtech/multi-key/compare/v1.0.8...v1.0.9
227236
[1.0.8]: https://github.com/cryptidtech/multi-key/compare/v1.0.7...v1.0.8

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "multi-key"
3-
version = "1.2.1"
3+
version = "1.2.2"
44
edition = "2024"
55
rust-version = "1.96"
66
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]

‎src/mk.rs‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2207,7 +2207,7 @@ impl Builder {
22072207
| Codec::LamportMerkleBlake3256Priv
22082208
| Codec::LamportMerkleShake128Priv
22092209
| Codec::LamportMerkleShake256Priv => {
2210-
lamport_merkle::generate_private_key(codec)?.to_vec()
2210+
lamport_merkle::generate_private_key_with_depth(codec, 1)?.to_vec()
22112211
}
22122212
#[cfg(feature = "xmss")]
22132213
Codec::XmssSha210256Priv | Codec::XmssSha216256Priv | Codec::XmssSha220256Priv => {
@@ -2217,13 +2217,39 @@ impl Builder {
22172217
};
22182218
let mut attributes = Attributes::new();
22192219
attributes.insert(AttrId::KeyData, key_bytes.into());
2220+
// Merkle-tree Lamport keys embed a mandatory depth byte; a depth-1
2221+
// (capacity 2) tree is the default here, mirroring
2222+
// `new_from_random_bytes_with_depth`.
2223+
#[cfg(feature = "lamport")]
2224+
if Self::lamport_merkle_codec(codec) {
2225+
attributes.insert(AttrId::Depth, Zeroizing::new(vec![1]));
2226+
}
22202227
Ok(Builder {
22212228
codec,
22222229
attributes: Some(attributes),
22232230
..Default::default()
22242231
})
22252232
}
22262233

2234+
/// True when `codec` is a merkle-tree Lamport private key codec.
2235+
#[cfg(feature = "lamport")]
2236+
fn lamport_merkle_codec(codec: Codec) -> bool {
2237+
matches!(
2238+
codec,
2239+
Codec::LamportMerkleSha3256Priv
2240+
| Codec::LamportMerkleSha3384Priv
2241+
| Codec::LamportMerkleSha3512Priv
2242+
| Codec::LamportMerkleSha2256Priv
2243+
| Codec::LamportMerkleSha2384Priv
2244+
| Codec::LamportMerkleSha2512Priv
2245+
| Codec::LamportMerkleBlake2B512Priv
2246+
| Codec::LamportMerkleBlake2S256Priv
2247+
| Codec::LamportMerkleBlake3256Priv
2248+
| Codec::LamportMerkleShake128Priv
2249+
| Codec::LamportMerkleShake256Priv
2250+
)
2251+
}
2252+
22272253
/// new builder for a merkle-tree Lamport key of the given depth (1..=3)
22282254
///
22292255
/// The depth is validated against the scheme's limit and stamped as the

‎src/views/lamport_merkle.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,10 @@ fn signing_key_material_size_for(codec: Codec) -> Result<usize, Error> {
989989
Ok(2 * bits * digest)
990990
}
991991

992+
/// Generate a depth-1 merkle-Lamport private key (unused since 1.2.2: the
993+
/// plain `Builder::new_from_random_bytes` path stamps depth inline; kept for
994+
/// symmetry with the other view helpers).
995+
#[allow(dead_code)]
992996
pub(crate) fn generate_private_key(codec: Codec) -> Result<Zeroizing<Vec<u8>>, Error> {
993997
generate_private_key_with_depth(codec, 1)
994998
}
@@ -1101,6 +1105,31 @@ mod tests {
11011105
);
11021106
}
11031107

1108+
#[test]
1109+
fn test_merkle_plain_ctor_stamps_depth() {
1110+
// The plain constructor generates a depth-1 tree and must stamp the
1111+
// mandatory depth attribute (fixed in 1.2.2).
1112+
let sk =
1113+
Builder::new_from_random_bytes(Codec::LamportMerkleBlake3256Priv, &mut rand::rng())
1114+
.unwrap()
1115+
.try_build()
1116+
.unwrap();
1117+
assert_eq!(sk.attributes.get(&AttrId::Depth).map(|b| b[0]), Some(1));
1118+
1119+
// the key must be fully usable: public derivation and signing
1120+
let pk = sk.conv_view().unwrap().to_public_key().unwrap();
1121+
let msg = b"plain ctor roundtrip";
1122+
let (ms, advanced) = sk
1123+
.sign_view()
1124+
.unwrap()
1125+
.sign_advance(msg, false, None)
1126+
.unwrap();
1127+
pk.verify_view().unwrap().verify(&ms, Some(msg)).unwrap();
1128+
let av = advanced.merkle_state_view().unwrap();
1129+
assert_eq!(av.next_index().unwrap(), 1);
1130+
assert_eq!(av.remaining_signatures().unwrap(), 1);
1131+
}
1132+
11041133
#[test]
11051134
fn test_merkle_depth3_sha3_256() {
11061135
let sk = build_priv(Codec::LamportMerkleSha3256Priv, 3);

0 commit comments

Comments
 (0)