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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <dwg@linuxprogrammer.org>"]
Expand Down
28 changes: 27 additions & 1 deletion src/mk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -2217,13 +2217,39 @@ 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),
..Default::default()
})
}

/// 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
Expand Down
29 changes: 29 additions & 0 deletions src/views/lamport_merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,10 @@ fn signing_key_material_size_for(codec: Codec) -> Result<usize, Error> {
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<Zeroizing<Vec<u8>>, Error> {
generate_private_key_with_depth(codec, 1)
}
Expand Down Expand Up @@ -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);
Expand Down
Loading