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
6 changes: 3 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ jobs:
run: cargo test --verbose --all-features

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
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ 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).

## [0.2.0] - 2026-09-01

### Added

- `Builder::try_build_advance` and `Builder::try_build_advance_encoded` for stateful signature schemes. They sign the WASM first-lock script with `SignView::sign_advance` and return the Vlad (or encoded Vlad) AND the advanced `Multikey`. The caller must persist the advanced key so the consumed one-time slot is never reused. The advanced key verifies the Vlad exactly like the original key (the merkle root does not change on advance). Plain `try_build`/`try_build_encoded` are unchanged and keep working for stateless keys (Ed25519, XMSS).
- `examples/lamport_merkle.rs` — build and verify a Vlad with a merkle-tree Lamport `lamport-merkle-blake3-256` ephemeral key at depth 1 (two one-time leaves: one for the Vlad, one for the first provenance-log entry), using `try_build_advance`.
- Three tests covering the merkle flow: sign/verify with both keys plus state introspection, tree exhaustion after two signatures (and the `UnsupportedAlgorithm` error from the stateless path), and rejection of a tampered depth attribute.

### Changed

- Updated dependencies: `multi-codec` 1.2 → 1.3, `multi-key` 1.1 → 1.2, `multi-sig` 1.2 → 1.3.
- Raised `rust-version` from 1.95 to 1.96 (required by `multi-key` 1.2 / `lamport_signature_plus` 0.5.0) and updated the CI MSRV job to 1.96.
- `examples/xmss.rs` doc comment updated: one-time Lamport cannot sign a Vlad, but merkle-tree Lamport can.
- README updated: `lamport-merkle-blake3-256` at depth 1 documented as the recommended ephemeral key type.

### Notes

- Merkle-tree Lamport keys (`LamportMerkle*Priv`) reject `Builder::try_build` by design: `SignView::sign` errors with `UnsupportedAlgorithm` and directs the caller to `try_build_advance`, which returns the advanced key state for persistence.

## [0.1.2] - 2026-08-18

### Changed
Expand Down Expand Up @@ -65,6 +84,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- The `multi-base`, `multi-codec`, `multi-key`, `multi-sig`, `multi-trait`, and `multi-util` dependencies use the published crates.io versions. `multi-vlad` does not declare `multi-hash` as a dependency; it appears transitively via `multi-key`.

[0.2.0]: https://github.com/cryptidtech/multi-vlad/compare/v0.1.2...v0.2.0
[0.1.2]: https://github.com/cryptidtech/multi-vlad/releases/tag/v0.1.2
[0.1.1]: https://github.com/cryptidtech/multi-vlad/releases/tag/v0.1.1
[0.1.0]: https://github.com/cryptidtech/multi-vlad/releases/tag/v0.1.0
11 changes: 6 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "multi-vlad"
version = "0.1.2"
version = "0.2.0"
edition = "2021"
rust-version = "1.95"
rust-version = "1.96"
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]
description = "Verifiable Long-Lived Address (VLAD) implementation"
repository = "https://github.com/cryptidtech/multi-vlad.git"
Expand All @@ -19,9 +19,9 @@ xmss = ["multi-key/xmss"]
[dependencies]
multi-base = { version = "1.0", default-features = false }
multi-cbor = { version = "0.1", optional = true }
multi-codec = "1.2"
multi-key = "1.1"
multi-sig = "1.2"
multi-codec = "1.3"
multi-key = "1.2"
multi-sig = "1.3"
multi-trait = { version = "1.0", default-features = false }
multi-util = "1.1"
thiserror = "2.0"
Expand All @@ -36,6 +36,7 @@ multi-cbor = "0.1"
proptest = "1.11"
rand = "0.10"
serde_json = "1.0"
zeroize = "1"

[[bench]]
name = "multivlad_bench"
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@ assert_eq!(vlad, decoded);
cargo run --example ed25519
```

- `examples/xmss.rs` — Build and verify a Vlad with an XMSS-SHA2_10_256 post-quantum ephemeral key pair. XMSS is a stateful hash-based scheme: a single key can sign a bounded number of messages (2^h for height h), which is necessary for a Vlad because the same ephemeral key must sign both the Vlad and the first provenance-log entry. Runnable:
- `examples/lamport_merkle.rs` — Build and verify a Vlad with a merkle-tree Lamport (`lamport-merkle-blake3-256`) post-quantum ephemeral key pair. This is the recommended ephemeral key type. The tree depth is 1, so the key holds exactly two one-time signatures: one for the Vlad and one for the first provenance-log entry. Merkle keys are stateful, so the example uses `Builder::try_build_advance` and shows the advanced key that the caller must persist. Runnable:
```bash
cargo run --example lamport_merkle
```

- `examples/xmss.rs` — Build and verify a Vlad with an XMSS-SHA2_10_256 post-quantum ephemeral key pair. XMSS is a stateful hash-based scheme: a single key can sign a bounded number of messages (2^h for height h), which is necessary for a Vlad because the same ephemeral key must sign both the Vlad and the first provenance-log entry. Use this scheme when the same ephemeral key must sign more than two messages. Runnable:
```bash
cargo run --example xmss
```
Expand Down
75 changes: 75 additions & 0 deletions examples/lamport_merkle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
//! Build a `Vlad` using a merkle-tree Lamport post-quantum ephemeral key pair.
//!
//! A Vlad is created by signing the WASM first-lock script with an ephemeral
//! key pair. In a real deployment the same ephemeral key is later used to sign
//! the first provenance-log entry, so the key must be usable for at least two
//! signatures. Merkle-tree Lamport (`lamport-merkle-blake3-256`) is a stateful
//! hash-based scheme: a tree of depth `d` holds `2^d` one-time Lamport leaves.
//! At depth 1 that is exactly two signatures — one for the Vlad and one for
//! the first provenance-log entry.
//!
//! Merkle keys are stateful: [`Builder::try_build_advance`] returns both the
//! Vlad and the advanced key state. The caller MUST persist the advanced key
//! so the consumed leaf is never reused.
//!
//! See the `xmss` example for a larger-capacity stateful alternative and the
//! `ed25519` example for an equivalent with Ed25519.

use multi_codec::Codec;
use multi_key::{Builder as MkBuilder, Multikey, Views as _};
use multi_util::CodecInfo as _;
use multi_vlad::{Builder, Vlad};

fn main() {
// 1. Generate a random depth-1 merkle-Lamport signing key (the ephemeral
// key pair). Depth 1 yields 2^1 = 2 one-time signatures — exactly what
// a Vlad plus its first provenance-log entry needs.
let mut rng = rand::rng();
let mk: Multikey =
MkBuilder::new_from_random_bytes_with_depth(Codec::LamportMerkleBlake3256Priv, 1, &mut rng)
.unwrap()
.try_build()
.unwrap();

// 2. Create a minimal WASM first-lock script (magic + version 1).
// A real script would embed public keys and validation logic.
let wasm = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];

// 3. Build the Vlad: sign the WASM with the ephemeral merkle key
// (combined=true) and capture the advanced key state.
let (vlad, advanced): (Vlad, Multikey) = Builder::default()
.with_signing_key(&mk)
.with_message(&wasm)
.try_build_advance()
.unwrap();

// 4. Validate the Vlad structure: combined signature + WASM magic.
vlad.validate().unwrap();

// 5. Extract the WASM first-lock script.
assert_eq!(vlad.wasm(), &wasm[..]);

// 6. Verify the signature over the WASM. Both the original key and the
// advanced key work (the merkle root does not change on advance).
vlad.verify(&mk).unwrap();
vlad.verify(&advanced).unwrap();

// 7. Round-trip through bytes and re-verify with the advanced key.
let bytes: Vec<u8> = vlad.clone().into();
let decoded = Vlad::try_from(&bytes[..]).unwrap();
assert_eq!(vlad, decoded);
decoded.verify(&advanced).unwrap();

// 8. In a real deployment the `advanced` key MUST be persisted now: leaf 0
// is consumed and the next signature (the first plog entry) uses leaf 1.
let mv = advanced.merkle_state_view().unwrap();
assert_eq!(mv.next_index().unwrap(), 1);
assert_eq!(mv.remaining_signatures().unwrap(), 1);

println!("merkle-Lamport (blake3-256, depth 1) Vlad built and verified successfully");
println!(" codec: {:?}", vlad.codec());
println!(" wasm bytes: {}", vlad.wasm().len());
println!(" total bytes: {}", bytes.len());
println!(" leaves left: {}", mv.remaining_signatures().unwrap());
}
6 changes: 4 additions & 2 deletions examples/xmss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
//! (h=10) can sign up to 1024 messages per key, which is sufficient for the
//! Vlad plus its first plog entry.
//!
//! Lamport keys are one-time (one signature per key) and cannot be used for a
//! Vlad, which requires at least two signatures from the same ephemeral key.
//! One-time Lamport keys cannot be used for a Vlad, which requires at least
//! two signatures from the same ephemeral key. Merkle-tree Lamport
//! (`lamport-merkle-blake3-256`) can: its depth-1 tree holds exactly two
//! one-time leaves. See the `lamport_merkle` example.
//!
//! See the `ed25519` example for an equivalent with Ed25519.

Expand Down
143 changes: 143 additions & 0 deletions src/vlad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,44 @@ impl Builder {
let ms = sv.sign(msg, true, None)?;
Ok(Vlad(ms))
}

/// build the vlad with a stateful signing key, returning the vlad AND the
/// advanced key state
///
/// Stateful signature schemes (merkle-tree Lamport, XMSS) consume a
/// one-time slot per signature. The caller MUST persist the returned
/// advanced key so the consumed slot is never reused. The advanced key
/// verifies the vlad just like the original key (the tree root does not
/// change when the state advances).
///
/// Stateless keys return an error: use [`try_build`](Self::try_build).
pub fn try_build_advance(&self) -> Result<(Vlad, Multikey), Error> {
let mk = self.mk.as_ref().ok_or(VladError::MissingSigningKey)?;
let msg = self.message.as_ref().ok_or(VladError::MissingMessage)?;
// validate message is WASM binary before signing
if msg.len() < 4 || msg[..4] != WASM_MAGIC {
return Err(VladError::InvalidWasm.into());
}
let sv = mk.sign_view()?;
// combined=true: message is stored inside the Multisig
let (ms, advanced) = sv.sign_advance(msg, true, None)?;
Ok((Vlad(ms), advanced))
}

/// build a base encoded vlad with a stateful signing key, returning the
/// encoded vlad AND the advanced key state
///
/// See [`try_build_advance`](Self::try_build_advance).
pub fn try_build_advance_encoded(&self) -> Result<(EncodedVlad, Multikey), Error> {
let (vlad, advanced) = self.try_build_advance()?;
Ok((
EncodedVlad::new(
self.base_encoding.unwrap_or_else(Vlad::preferred_encoding),
vlad,
),
advanced,
))
}
}

#[cfg(test)]
Expand Down Expand Up @@ -565,4 +603,109 @@ mod tests {
// 6. the message() accessor returns the same bytes as wasm()
assert_eq!(vlad.message(), vlad.wasm());
}

#[test]
fn test_merkle_vlad_sign_and_verify() {
let mut rng = rand::rng();
let mk = multi_key::Builder::new_from_random_bytes_with_depth(
Codec::LamportMerkleBlake3256Priv,
1,
&mut rng,
)
.unwrap()
.try_build()
.unwrap();
let msg = test_wasm_message();

let (vlad, advanced) = Builder::default()
.with_signing_key(&mk)
.with_message(&msg)
.try_build_advance()
.unwrap();

// structure checks
vlad.validate().unwrap();
assert_eq!(vlad.wasm(), msg.as_slice());

// the signature carries the depth attribute
let ms = vlad.multisig();
assert_eq!(ms.depth(), Some(1));

// both the original and the advanced key verify (same tree root)
vlad.verify(&mk).unwrap();
vlad.verify(&advanced).unwrap();

// round-trip through bytes and re-verify with the advanced key
let bytes: Vec<u8> = vlad.clone().into();
let decoded = Vlad::try_from(bytes.as_ref()).unwrap();
assert_eq!(vlad, decoded);
decoded.validate().unwrap();
decoded.verify(&advanced).unwrap();

// the advanced key consumed leaf 0 of the 2-leaf tree
let mv = advanced.merkle_state_view().unwrap();
assert_eq!(mv.depth().unwrap(), 1);
assert_eq!(mv.capacity().unwrap(), 2);
assert_eq!(mv.next_index().unwrap(), 1);
assert_eq!(mv.remaining_signatures().unwrap(), 1);
}

#[test]
fn test_merkle_vlad_exhausts_after_two_signs() {
let mut rng = rand::rng();
let mk = multi_key::Builder::new_from_random_bytes_with_depth(
Codec::LamportMerkleBlake3256Priv,
1,
&mut rng,
)
.unwrap()
.try_build()
.unwrap();
let msg = test_wasm_message();

// leaf 0 signs the vlad
let (_vlad, advanced) = Builder::default()
.with_signing_key(&mk)
.with_message(&msg)
.try_build_advance()
.unwrap();

// leaf 1 signs a second message via sign_advance on the advanced key
let sv = advanced.sign_view().unwrap();
let (_ms2, exhausted) = sv.sign_advance(b"second message", false, None).unwrap();

// the tree is empty; a third signature fails
let sv3 = exhausted.sign_view().unwrap();
assert!(sv3.sign_advance(b"third message", false, None).is_err());

// the stateless sign path never worked for merkle keys
assert!(Builder::default()
.with_signing_key(&mk)
.with_message(&msg)
.try_build()
.is_err());
}

#[test]
fn test_merkle_vlad_rejects_wrong_depth_attr() {
let mut rng = rand::rng();
let mut mk = multi_key::Builder::new_from_random_bytes_with_depth(
Codec::LamportMerkleBlake3256Priv,
1,
&mut rng,
)
.unwrap()
.try_build()
.unwrap();

// tamper the depth attribute: wire says 1, attr now says 2
mk.attributes
.insert(multi_key::AttrId::Depth, zeroize::Zeroizing::new(vec![2]));

assert!(Builder::default()
.with_signing_key(&mk)
.with_message(&test_wasm_message())
.try_build_advance()
.is_err());
}
}
Loading