Skip to main content

rhdl_dsig_scalable_core/
reference.rs

1//! Complete byte-oriented reference for the explicitly versioned DSig profile.
2//!
3//! This is a software oracle and a trusted preload path, not synthesizable
4//! hardware or sustained FPGA key-generation evidence. A [`PreparedKey`](crate::reference::PreparedKey) is
5//! consumed by signing, is deliberately not `Clone`, and wipes its cached secret
6//! nodes on drop. The caller must still persist epoch/batch allocation before
7//! deriving seeds: reconstructing the same seed after reset defeats one-time
8//! security. Do not expose precomputed chain caches outside a trusted boundary.
9//!
10//! # Why this module exists
11//!
12//! Hardware is hard to debug. When the engine writes a 1,642-byte signature to
13//! HBM, how do you know that every byte is right? You need an answer key. This
14//! module is that answer key: a plain, slow, easy-to-read Rust implementation
15//! of the whole DSig profile, from seed to transferable signature. Every
16//! hardware block is checked against it bit for bit (see
17//! [`crate::slice_model`] and the tests in `tests/`), and the host tool
18//! `dsig_engine_host` uses it to verify what the card produced.
19//!
20//! It is written for clarity, not speed. It uses byte arrays instead of RHDL
21//! `Bits`, and the `blake3` and `ed25519-dalek` crates instead of hardware
22//! lanes. Nothing here is synthesized.
23//!
24//! # The story of one signature, in software
25//!
26//! ```text
27//!  master seed ─┐
28//!  signer key  ─┼─ one_time_seed() ──▶ 32-byte seed per key (×128 per batch)
29//!  epoch,batch ─┤        (BLAKE3 derive_key, SEED_DERIVATION_CONTEXT)
30//!  key index   ─┘
31//!                              │
32//!                              ▼
33//!                  PreparedKey::from_seed()
34//!                    pk_nonce, nonce   (BLAKE3 XOF, prefixes 0x5eed / 0xc0ca)
35//!                    nodes[0][0..68]   (BLAKE3 XOF of the seed: chain secrets)
36//!                    nodes[1..=3]      (3 chain steps per chain: dsig_chain)
37//!                    pk_hash           (endpoint_commitment of nodes[3])
38//!                              │
39//!            128 pk_hash ──▶ BatchTree::new() ──▶ root
40//!                              │
41//!                              ▼
42//!            BatchCommitment { epoch, batch_id, root } ── Ed25519 ──▶ BatchCertificate
43//!                              │
44//!  message ──▶ message_digits() ──▶ 68 base-4 digits (64 message + 4 checksum)
45//!                              │
46//!                              ▼
47//!            PreparedKey::sign(): reveal nodes[digit][chain] for each chain
48//!                              │
49//!                              ▼
50//!            DsigSignature = WOTS part + key index + 7 siblings + certificate
51//!                              │
52//!                              ▼
53//!            to_bytes(): 1,642 bytes on the wire
54//! ```
55//!
56//! # Words you will meet
57//!
58//! * **W-OTS+** (Winternitz one-time signature): each of the 68 chains starts
59//!   at a secret node and is hashed forward. Signing a base-4 digit `d`
60//!   reveals the node at depth `d`; a verifier hashes it the remaining
61//!   `3 − d` steps and must land on the public endpoint.
62//! * **Checksum digits**: the last 4 digits encode the sum of `3 − digit`
63//!   over the 64 message digits. An attacker can only hash *forward*, which
64//!   raises digits; that lowers the checksum, which they cannot do. So no
65//!   forgery comes for free from a seen signature.
66//! * **Nonce** (salt): a 16-byte random value mixed into hashes so that two
67//!   keys never share chain inputs.
68//! * **Merkle tree**: a binary tree of hashes. Its 128 leaves are the keys'
69//!   `pk_hash` values; its root stands for all 128 at once, so one Ed25519
70//!   signature can vouch for all of them.
71//!
72//! # Wire layout of a signature
73//!
74//! | Bytes | Field |
75//! |---|---|
76//! | 0..64 | [`BatchCommitment`] (see [`crate::certificate`]) |
77//! | 64..128 | Ed25519 signature `R ‖ S` |
78//! | 128..130 | key index, little endian |
79//! | 130..162 | `pk_hash` |
80//! | 162..178 | `pk_nonce` |
81//! | 178..194 | message `nonce` |
82//! | 194..418 | 7 Merkle siblings × 32 bytes, bottom up |
83//! | 418..1642 | 68 chain nodes × 18 bytes, in chain order |
84//!
85//! # Security rules this module enforces (and the ones it cannot)
86//!
87//! * A one-time key signs at most once. [`PreparedKey::sign`] takes `self` by
88//!   value, so Rust's ownership rules stop a second call. [`PreparedBatch`]
89//!   stores keys as `Option` and `take`s them, so a repeated index returns
90//!   `None`.
91//! * Secrets are wiped. Secret nodes, nonces, seeds and the master seed are
92//!   zeroized as soon as they are no longer needed; the master seed is never
93//!   stored in [`PreparedBatch`].
94//! * What it cannot enforce: that `(epoch, batch_id)` was never used before.
95//!   Rebuilding a batch from the same master seed and namespace recreates the
96//!   same keys, which would let one key sign two messages. The host must
97//!   durably allocate namespaces first; [`crate::lease`] does that.
98//!
99//! # Neighbours
100//!
101//! * [`crate::certificate`] defines the signed 64-byte batch envelope.
102//! * [`crate::haraka`] provides [`dsig_chain`], one chain step in the
103//!   compiled-in chain hash (Haraka-512 unless a `chain-*` feature says
104//!   otherwise).
105//! * [`crate::slice_model`] reuses [`message_digits`] and [`PreparedBatch`]
106//!   to predict the hardware cycle by cycle.
107//! * [`crate::lease`] wraps [`PreparedBatch`] with never-reused batch ranges.
108
109use ed25519_dalek::{SigningKey, VerifyingKey};
110use zeroize::Zeroize;
111
112use crate::{
113    BATCH_KEYS, CHAINS, Digest, ELEMENT_BYTES, Element, MERKLE_HEIGHT, MESSAGE_DIGITS, Nonce,
114    POSITIONS,
115    certificate::{BatchCertificate, BatchCommitment},
116    haraka::dsig_chain,
117};
118
119/// Four stored positions for every chain; the last row contains public nodes.
120///
121/// Indexed `[depth][chain]`: `depth` 0 is the secret start of a chain, 1 and
122/// 2 are intermediate nodes, 3 is the public endpoint. Each entry is an
123/// 18-byte [`Element`]. 4 × 68 × 18 = 4,896 bytes per key. Storing every
124/// depth (instead of only the secret) is what lets signing be a table lookup
125/// with no hashing; the hardware key cache ([`crate::cache_bank`]) stores the
126/// same 4 × 68 nodes per key for the same reason.
127pub type ChainCache = [[Element; CHAINS]; POSITIONS];
128/// Self-standing signature wire length, with no transport padding.
129///
130/// 64 (commitment) + 64 (Ed25519) + 2 (index) + 32 + 16 + 16 (public key
131/// hash, public salt, message salt) + 7 × 32 (Merkle path) + 68 × 18 (chain
132/// nodes) = 1,642 bytes. The hardware pads each one to a 2 KiB frame slot in
133/// HBM; this constant is the unpadded length.
134pub const SIGNATURE_WIRE_BYTES: usize = 1642;
135
136/// Public metadata for one cached one-time key.
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub struct KeyPublic {
139    /// Salt used in all of this key's Haraka chains (16 bytes).
140    ///
141    /// Public: a verifier needs it to recompute the chains.
142    pub pk_nonce: Nonce,
143    /// BLAKE3 commitment to public salt and 68 chain endpoints (32 bytes).
144    ///
145    /// This is the key's public identity and the Merkle leaf for its batch.
146    pub pk_hash: Digest,
147}
148
149/// Single-use secret cache. Signing consumes this object before returning data.
150///
151/// It is not `Clone` on purpose: a copy would be a second chance to sign
152/// with the same one-time key. Dropping it zeroizes the secret parts.
153pub struct PreparedKey {
154    /// Public salt and endpoint commitment; safe to share.
155    public: KeyPublic,
156    /// Secret 16-byte message salt, disclosed only inside the signature.
157    nonce: Nonce,
158    /// All four depths of all 68 chains. Rows 0 to 2 are secret.
159    nodes: ChainCache,
160}
161
162impl Drop for PreparedKey {
163    /// Wipe the secret chain nodes and the message salt so they do not
164    /// linger in freed memory.
165    fn drop(&mut self) {
166        self.nodes.zeroize();
167        self.nonce.zeroize();
168    }
169}
170
171impl PreparedKey {
172    /// Prepare a fresh key from an exclusively allocated random 256-bit seed.
173    ///
174    /// The seed must never be reused, including across resets or processes.
175    /// The four-byte salt prefixes retain the upstream x86 little-endian
176    /// convention. The signature checksum and certificate format do not.
177    ///
178    /// Steps: derive two 16-byte salts from the seed, expand the seed into 68
179    /// secret chain starts, hash each chain three steps forward, then commit
180    /// to the public salt and the 68 endpoints.
181    #[must_use]
182    pub fn from_seed(mut seed: [u8; 32]) -> Self {
183        // Helper: BLAKE3(prefix ‖ seed) read as a 16-byte XOF output. The
184        // prefix separates the two salts so they are independent.
185        let nonce_for = |prefix: u32| {
186            let mut hasher = blake3::Hasher::new();
187            hasher.update(&prefix.to_le_bytes());
188            hasher.update(&seed);
189            let mut nonce = [0; 16];
190            let mut reader = hasher.finalize_xof();
191            reader.fill(&mut nonce);
192            reader.zeroize();
193            hasher.zeroize();
194            nonce
195        };
196        // Public chain salt (prefix 0x5eed) and secret message salt (prefix 0xc0ca).
197        let pk_nonce = nonce_for(0x5eed);
198        let nonce = nonce_for(0xc0ca);
199        // nodes[depth][chain], filled row by row below.
200        let mut nodes = [[[0; ELEMENT_BYTES]; CHAINS]; POSITIONS];
201        // Row 0: the 68 secret chain starts, read in order from one BLAKE3
202        // XOF stream of the seed (68 × 18 = 1,224 bytes).
203        let mut expansion = blake3::Hasher::new();
204        expansion.update(&seed);
205        let mut reader = expansion.finalize_xof();
206        for element in &mut nodes[0] {
207            reader.fill(element);
208        }
209        // Wipe the XOF state: it could regenerate the secrets.
210        reader.zeroize();
211        expansion.zeroize();
212        // Rows 1 to 3: each node is one chain step of the node above it. The
213        // step is salted with pk_nonce and tweaked by (chain, depth), so no
214        // two steps anywhere hash the same input.
215        for depth in 0..POSITIONS - 1 {
216            for chain in 0..CHAINS {
217                nodes[depth + 1][chain] = dsig_chain(&nodes[depth][chain], &pk_nonce, chain, depth);
218            }
219        }
220        // The seed is no longer needed; wipe this copy.
221        seed.zeroize();
222        // Row 3 holds the public endpoints; commit to them.
223        let pk_hash = endpoint_commitment(&pk_nonce, &nodes[POSITIONS - 1]);
224        Self {
225            public: KeyPublic { pk_nonce, pk_hash },
226            nonce,
227            nodes,
228        }
229    }
230
231    /// Public metadata to send through the authenticated key-admission plane.
232    #[must_use]
233    pub const fn public(&self) -> KeyPublic {
234        self.public
235    }
236
237    /// Signer-private message nonce for the trusted hardware loader only.
238    ///
239    /// Do not distribute this with pre-certified public keys. It must remain
240    /// unpredictable until the one-time key is irreversibly bound to a message;
241    /// early disclosure weakens the 128-bit salted-digest chosen-message model.
242    #[must_use]
243    pub const fn signing_nonce_for_device(&self) -> Nonce {
244        self.nonce
245    }
246
247    /// Borrow secret cache bytes for trusted device loading. Never log them.
248    #[must_use]
249    pub const fn cache_for_device(&self) -> &ChainCache {
250        &self.nodes
251    }
252
253    /// Consume this key and reveal only message-selected nodes.
254    ///
255    /// Takes `self` by value, so this key cannot sign again. For chain `i`
256    /// with digit `d`, the revealed node is `nodes[d][i]`.
257    #[must_use]
258    pub fn sign(self, message: &[u8]) -> WotsSignature {
259        // 68 base-4 digits, each 0..=3, one per chain.
260        let digits = message_digits(&self.public.pk_hash, &self.nonce, message);
261        WotsSignature {
262            public: self.public,
263            nonce: self.nonce,
264            elements: core::array::from_fn(|i| self.nodes[usize::from(digits[i])][i]),
265        }
266    }
267}
268
269/// Public salt plus endpoint commitment, exactly ordered as upstream DSig.
270///
271/// `BLAKE3(pk_nonce ‖ endpoint[0] ‖ … ‖ endpoint[67])`: 16 + 68 × 18 = 1,240
272/// input bytes, which is why the summary in [`crate`] counts 21 BLAKE3
273/// compressions for the endpoint commitment. Signer and verifier both call
274/// this; a match proves the revealed nodes lead to this key's endpoints.
275#[must_use]
276pub fn endpoint_commitment(pk_nonce: &Nonce, endpoints: &[Element; CHAINS]) -> Digest {
277    let mut hash = blake3::Hasher::new();
278    hash.update(pk_nonce);
279    for endpoint in endpoints {
280        hash.update(endpoint);
281    }
282    *hash.finalize().as_bytes()
283}
284
285/// Extract low two bits from each XOF byte and append the complement checksum.
286///
287/// Input: 64 XOF bytes. Output: 68 digits in 0..=3. Digits 0..64 are the low
288/// two bits of each byte. The checksum is `Σ (3 − digit)` (at most 64 × 3 =
289/// 192, which fits in four base-4 digits) and is written little endian into
290/// digits 64..68. The hardware signer ([`crate::slice_signer`]) does the same
291/// split.
292#[must_use]
293pub fn digits_from_xof(xof: &[u8; 64]) -> [u8; CHAINS] {
294    let mut digits = [0; CHAINS];
295    // Sum of "steps left to the endpoint" over the 64 message digits.
296    let mut checksum = 0_u16;
297    for i in 0..MESSAGE_DIGITS {
298        digits[i] = xof[i] & 3;
299        checksum += u16::from(3 - digits[i]);
300    }
301    // Four base-4 checksum digits, least significant first.
302    for i in 0..4 {
303        digits[MESSAGE_DIGITS + i] = ((checksum >> (2 * i)) & 3) as u8;
304    }
305    digits
306}
307
308/// Salted BLAKE3-XOF message mapping; 64 low-bit digits encode 128 digest bits.
309///
310/// `BLAKE3-XOF(pk_hash ‖ nonce ‖ message)`, 64 bytes, then
311/// [`digits_from_xof`]. Binding `pk_hash` ties the digest to one key; the
312/// secret `nonce` keeps the digits unpredictable until the signature is out.
313#[must_use]
314pub fn message_digits(pk_hash: &Digest, nonce: &Nonce, message: &[u8]) -> [u8; CHAINS] {
315    let mut hash = blake3::Hasher::new();
316    hash.update(pk_hash);
317    hash.update(nonce);
318    hash.update(message);
319    // Only the low 2 bits of each of these 64 bytes are used.
320    let mut xof = [0; 64];
321    hash.finalize_xof().fill(&mut xof);
322    digits_from_xof(&xof)
323}
324
325/// WOTS component only; its public-key hash is not itself a trusted identity.
326///
327/// Anyone can make a key and a matching WOTS signature. Trust comes from the
328/// Merkle path and the Ed25519 certificate in [`DsigSignature`].
329#[derive(Clone, Debug, Eq, PartialEq)]
330pub struct WotsSignature {
331    /// Salts and claimed public commitment.
332    pub public: KeyPublic,
333    /// Message salt disclosed only after the key is bound to this message.
334    pub nonce: Nonce,
335    /// The 68 selected 18-byte nodes, in chain order.
336    pub elements: [Element; CHAINS],
337}
338
339impl WotsSignature {
340    /// Verify the hash-chain component, without authenticating the claimed key.
341    ///
342    /// Recompute the digits, hash each revealed node forward to depth 3, and
343    /// compare the commitment with the claimed `pk_hash`.
344    #[must_use]
345    pub fn verify_hash_component(&self, message: &[u8]) -> bool {
346        let digits = message_digits(&self.public.pk_hash, &self.nonce, message);
347        // Start from the revealed nodes; after the loop these are endpoints.
348        let mut endpoints = self.elements;
349        for chain in 0..CHAINS {
350            for depth in usize::from(digits[chain])..POSITIONS - 1 {
351                endpoints[chain] =
352                    dsig_chain(&endpoints[chain], &self.public.pk_nonce, chain, depth);
353            }
354        }
355        endpoint_commitment(&self.public.pk_nonce, &endpoints) == self.public.pk_hash
356    }
357}
358
359/// Fixed 128-leaf BLAKE3 tree authenticating public-key commitments.
360///
361/// Stored as a flat array in heap order: node 0 is the root, node `i` has
362/// children `2i + 1` and `2i + 2`, and the 128 leaves sit at 127..255.
363///
364/// ```text
365///               0  (root)
366///            /     \
367///           1       2
368///          / \     / \
369///        ...  ...  ...  ...        7 levels
370///   127 128 ...        254         leaves = pk_hash of keys 0..127
371/// ```
372#[derive(Clone)]
373pub struct BatchTree {
374    /// 255 digests: 127 inner nodes followed by 128 leaves.
375    nodes: [Digest; BATCH_KEYS * 2 - 1],
376}
377
378/// One Merkle parent: `BLAKE3(left ‖ right)`, 64 input bytes, one BLAKE3
379/// compression. The order matters: swapping children gives a different root.
380fn parent_hash(left: &Digest, right: &Digest) -> Digest {
381    let mut hash = blake3::Hasher::new();
382    hash.update(left);
383    hash.update(right);
384    *hash.finalize().as_bytes()
385}
386
387impl BatchTree {
388    /// Construct a tree of exactly one complete certificate batch.
389    #[must_use]
390    pub fn new(leaves: &[Digest; BATCH_KEYS]) -> Self {
391        let mut nodes = [[0; 32]; BATCH_KEYS * 2 - 1];
392        // Leaves go at the end of the heap array.
393        nodes[BATCH_KEYS - 1..].copy_from_slice(leaves);
394        // Walk inner nodes from the last to the root, so both children of
395        // each node are already computed.
396        for i in (0..BATCH_KEYS - 1).rev() {
397            nodes[i] = parent_hash(&nodes[i * 2 + 1], &nodes[i * 2 + 2]);
398        }
399        Self { nodes }
400    }
401
402    /// Batch commitment presented to the Ed25519 background service.
403    #[must_use]
404    pub const fn root(&self) -> Digest {
405        self.nodes[0]
406    }
407
408    /// Bottom-up sibling path; invalid indices are rejected instead of masked.
409    ///
410    /// Returns the 7 sibling digests a verifier needs to rebuild the root from
411    /// leaf `index` (0..128).
412    #[must_use]
413    pub fn proof(&self, index: usize) -> Option<[Digest; MERKLE_HEIGHT]> {
414        if index >= BATCH_KEYS {
415            return None;
416        }
417        // Heap position of the leaf.
418        let mut node = BATCH_KEYS - 1 + index;
419        let mut proof = [[0; 32]; MERKLE_HEIGHT];
420        for sibling in &mut proof {
421            // In heap order a left child is odd and a right child is even;
422            // the sibling is the neighbour on the other side.
423            *sibling = self.nodes[if node % 2 == 0 { node - 1 } else { node + 1 }];
424            // Move up to the parent.
425            node = (node - 1) / 2;
426        }
427        Some(proof)
428    }
429}
430
431/// Recover a root from a public-key commitment and checked leaf index.
432///
433/// Bit `level` of `index` says whether the running hash is a left (0) or
434/// right (1) child at that level. Returns `None` for an index of 128 or more.
435#[must_use]
436pub fn root_from_proof(
437    leaf: Digest,
438    index: usize,
439    proof: &[Digest; MERKLE_HEIGHT],
440) -> Option<Digest> {
441    if index >= BATCH_KEYS {
442        return None;
443    }
444    // The running hash, from the leaf up to the root.
445    let mut root = leaf;
446    for (level, sibling) in proof.iter().enumerate() {
447        root = if (index >> level) & 1 == 0 {
448            parent_hash(&root, sibling)
449        } else {
450            parent_hash(sibling, &root)
451        };
452    }
453    Some(root)
454}
455
456/// Transferable signature: hash-based component, inclusion proof, certificate.
457#[derive(Clone, Debug, Eq, PartialEq)]
458pub struct DsigSignature {
459    /// One-time hash-based message signature.
460    pub wots: WotsSignature,
461    /// Leaf position within the fixed-size authenticated batch (0..128).
462    pub key_index: u16,
463    /// Seven bottom-up siblings linking this one-time key to the batch root.
464    pub proof: [Digest; MERKLE_HEIGHT],
465    /// Background Ed25519 certificate, retained for uncached verifiers.
466    pub certificate: BatchCertificate,
467}
468
469impl DsigSignature {
470    /// Full cold-path verification, bound to a caller's trusted Ed25519 identity.
471    ///
472    /// "Cold" means nothing is cached: this does one Ed25519 verification
473    /// plus the hash work. [`VerifiedBatch::verify`] is the warm path.
474    #[must_use]
475    pub fn verify(&self, message: &[u8], trusted_signer: &VerifyingKey) -> bool {
476        self.certificate.verify_strict(trusted_signer)
477            && self.verify_authenticated_hash_component(message)
478    }
479
480    /// The hash-only part of verification: the Merkle path must lead from
481    /// `pk_hash` to the certified root, and the WOTS part must check out.
482    /// Private because it is only safe after the certificate is trusted.
483    fn verify_authenticated_hash_component(&self, message: &[u8]) -> bool {
484        root_from_proof(
485            self.wots.public.pk_hash,
486            usize::from(self.key_index),
487            &self.proof,
488        ) == Some(self.certificate.commitment.root)
489            && self.wots.verify_hash_component(message)
490    }
491
492    /// Stable wire layout, independent of Rust/C++ padding or native word size.
493    #[must_use]
494    ///
495    /// The byte offsets are listed in the module docs' wire layout table.
496    pub fn to_bytes(&self) -> [u8; SIGNATURE_WIRE_BYTES] {
497        let mut bytes = [0; SIGNATURE_WIRE_BYTES];
498        // Write cursor into `bytes`.
499        let mut position = 0;
500        // Append one field and advance the cursor.
501        let mut put = |part: &[u8]| {
502            bytes[position..position + part.len()].copy_from_slice(part);
503            position += part.len();
504        };
505        put(&self.certificate.commitment.to_bytes());
506        put(&self.certificate.signature);
507        put(&self.key_index.to_le_bytes());
508        put(&self.wots.public.pk_hash);
509        put(&self.wots.public.pk_nonce);
510        put(&self.wots.nonce);
511        for sibling in &self.proof {
512            put(sibling);
513        }
514        for element in &self.wots.elements {
515            put(element);
516        }
517        debug_assert_eq!(position, SIGNATURE_WIRE_BYTES);
518        bytes
519    }
520
521    /// Parse exactly one corrected-profile signature. Parsing never authenticates.
522    ///
523    /// Returns `None` for a wrong length, a foreign domain/profile/batch size
524    /// or a key index of 128 or more. Call [`DsigSignature::verify`] after.
525    #[must_use]
526    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
527        if bytes.len() != SIGNATURE_WIRE_BYTES {
528            return None;
529        }
530        // Bytes 0..64: rejects any other domain, profile or batch size.
531        let commitment = BatchCommitment::from_bytes(bytes[..64].try_into().ok()?)?;
532        let certificate = BatchCertificate {
533            commitment,
534            signature: bytes[64..128].try_into().ok()?,
535        };
536        // Bytes 128..130: must name one of the 128 leaves.
537        let key_index = u16::from_le_bytes(bytes[128..130].try_into().ok()?);
538        if usize::from(key_index) >= BATCH_KEYS {
539            return None;
540        }
541        let public = KeyPublic {
542            pk_hash: bytes[130..162].try_into().ok()?,
543            pk_nonce: bytes[162..178].try_into().ok()?,
544        };
545        // Bytes 178..194: the message salt.
546        let nonce = bytes[178..194].try_into().ok()?;
547        // Bytes 194..418: seven 32-byte siblings.
548        let mut proof = [[0; 32]; MERKLE_HEIGHT];
549        for (i, sibling) in proof.iter_mut().enumerate() {
550            sibling.copy_from_slice(&bytes[194 + i * 32..194 + (i + 1) * 32]);
551        }
552        // Bytes 418..1642: sixty-eight 18-byte chain nodes.
553        let mut elements = [[0; ELEMENT_BYTES]; CHAINS];
554        for (i, element) in elements.iter_mut().enumerate() {
555            element.copy_from_slice(&bytes[418 + i * ELEMENT_BYTES..418 + (i + 1) * ELEMENT_BYTES]);
556        }
557        Some(Self {
558            wots: WotsSignature {
559                public,
560                nonce,
561                elements,
562            },
563            key_index,
564            proof,
565            certificate,
566        })
567    }
568}
569
570/// Authenticated background cache entry; no unchecked public constructor.
571///
572/// Why: Ed25519 verification is the expensive part of checking a signature.
573/// All 128 signatures of a batch share one certificate, so a verifier checks
574/// it once, keeps a `VerifiedBatch`, and then checks each signature with hash
575/// work only. The only way to build one is [`VerifiedBatch::authenticate`].
576pub struct VerifiedBatch {
577    /// The trusted Ed25519 public key (32 bytes) the certificate verified under.
578    signer: [u8; 32],
579    /// The exact certificate that verified.
580    certificate: BatchCertificate,
581}
582
583impl VerifiedBatch {
584    /// Cache a certificate only after strict Ed25519 verification succeeds.
585    #[must_use]
586    pub fn authenticate(
587        certificate: &BatchCertificate,
588        trusted_signer: &VerifyingKey,
589    ) -> Option<Self> {
590        certificate.verify_strict(trusted_signer).then(|| Self {
591            signer: trusted_signer.to_bytes(),
592            certificate: certificate.clone(),
593        })
594    }
595
596    /// Fast-path eligibility binds the full certificate and trusted identity.
597    #[must_use]
598    pub fn can_verify_fast(
599        &self,
600        signature: &DsigSignature,
601        trusted_signer: &VerifyingKey,
602    ) -> bool {
603        self.signer == trusted_signer.to_bytes() && self.certificate == signature.certificate
604    }
605
606    /// Verify without a repeated Ed25519 operation; cache misses fail closed.
607    #[must_use]
608    pub fn verify(
609        &self,
610        signature: &DsigSignature,
611        message: &[u8],
612        trusted_signer: &VerifyingKey,
613    ) -> bool {
614        self.can_verify_fast(signature, trusted_signer)
615            && signature.verify_authenticated_hash_component(message)
616    }
617}
618
619/// Prepare and certify a reference batch; repeated namespace allocation is unsafe.
620///
621/// One batch = 128 one-time keys, their Merkle tree and one Ed25519
622/// certificate. It holds no master seed. Use it through [`crate::lease`],
623/// which makes sure each `(epoch, batch_id)` is used once.
624pub struct PreparedBatch {
625    /// The 128 keys; `None` once a key has signed or gone to a device.
626    keys: [Option<PreparedKey>; BATCH_KEYS],
627    /// Merkle tree over the keys' `pk_hash` values.
628    tree: BatchTree,
629    /// Ed25519 certificate over the tree root and namespace.
630    certificate: BatchCertificate,
631}
632
633/// Derive the 32-byte seed of one one-time key.
634///
635/// `BLAKE3 derive_key(SEED_DERIVATION_CONTEXT, master ‖ signer ‖ epoch ‖
636/// batch_id ‖ index)` over 32 + 32 + 8 + 8 + 4 = 84 bytes, little endian.
637/// Every input that names the key goes in, so two signers, two namespaces or
638/// two indices never share a seed. The derivation context string,
639/// [`crate::SEED_DERIVATION_CONTEXT`], is frozen and shared with the hardware
640/// builders.
641fn one_time_seed(
642    master: &[u8; 32],
643    signer: &[u8; 32],
644    epoch: u64,
645    batch_id: u64,
646    index: u32,
647) -> [u8; 32] {
648    // The 84-byte input, wiped right after hashing because it holds the master seed.
649    let mut material = [0; 84];
650    material[..32].copy_from_slice(master);
651    material[32..64].copy_from_slice(signer);
652    material[64..72].copy_from_slice(&epoch.to_le_bytes());
653    material[72..80].copy_from_slice(&batch_id.to_le_bytes());
654    material[80..].copy_from_slice(&index.to_le_bytes());
655    let mut hasher = blake3::Hasher::new_derive_key(crate::SEED_DERIVATION_CONTEXT);
656    hasher.update(&material);
657    material.zeroize();
658    let mut seed = [0; 32];
659    let mut reader = hasher.finalize_xof();
660    reader.fill(&mut seed);
661    reader.zeroize();
662    hasher.zeroize();
663    seed
664}
665
666impl PreparedBatch {
667    /// Software bootstrap/oracle. Persist a fresh `(epoch, batch_id)` first.
668    ///
669    /// Derives 128 seeds, prepares 128 keys, builds the tree and signs the
670    /// commitment with `signer`. The master seed is zeroized before return.
671    #[must_use]
672    pub fn new(mut master_seed: [u8; 32], epoch: u64, batch_id: u64, signer: &SigningKey) -> Self {
673        // The Ed25519 public key is mixed into every seed.
674        let signer_identity = signer.verifying_key().to_bytes();
675        let keys: [Option<PreparedKey>; BATCH_KEYS] = core::array::from_fn(|index| {
676            let mut seed = one_time_seed(
677                &master_seed,
678                &signer_identity,
679                epoch,
680                batch_id,
681                index as u32,
682            );
683            let prepared = PreparedKey::from_seed(seed);
684            seed.zeroize();
685            Some(prepared)
686        });
687        master_seed.zeroize();
688        // Leaf i is key i's public commitment.
689        let leaves = core::array::from_fn(|i| keys[i].as_ref().expect("new key").public.pk_hash);
690        let tree = BatchTree::new(&leaves);
691        let certificate = BatchCertificate::sign_reference(
692            BatchCommitment {
693                epoch,
694                batch_id,
695                root: tree.root(),
696            },
697            signer,
698        );
699        Self {
700            keys,
701            tree,
702            certificate,
703        }
704    }
705
706    /// Certificate to pre-verify at expected recipients before online traffic.
707    #[must_use]
708    pub const fn certificate(&self) -> &BatchCertificate {
709        &self.certificate
710    }
711
712    /// Burn ownership before returning a key to a hardware loader.
713    ///
714    /// The slot becomes `None`, so this batch can never sign with that key
715    /// again, even if the device later fails.
716    pub fn take_key_for_device(&mut self, index: usize) -> Option<PreparedKey> {
717        self.keys.get_mut(index)?.take()
718    }
719
720    /// Finish a hardware-produced WOTS signature without recreating a secret key.
721    ///
722    /// The full signature must still be independently verified before use.
723    #[must_use]
724    ///
725    /// Returns `None` if `index` is out of range or `wots` is not the key at
726    /// that leaf.
727    pub fn attach_certificate(&self, index: usize, wots: WotsSignature) -> Option<DsigSignature> {
728        let proof = self.tree.proof(index)?;
729        // Refuse a WOTS part whose key is not leaf `index` of this tree.
730        if root_from_proof(wots.public.pk_hash, index, &proof) != Some(self.tree.root()) {
731            return None;
732        }
733        Some(DsigSignature {
734            wots,
735            key_index: index as u16,
736            proof,
737            certificate: self.certificate.clone(),
738        })
739    }
740
741    /// Consume exactly one unused key; a repeated index returns `None`.
742    pub fn sign(&mut self, index: usize, message: &[u8]) -> Option<DsigSignature> {
743        let key = self.keys.get_mut(index)?.take()?;
744        self.attach_certificate(index, key.sign(message))
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    /// Same master seed, different signer, epoch, batch or index: different seeds; and the input layout is exactly the documented 84 bytes.
753    #[test]
754    fn identical_master_material_is_separated_by_signer_and_namespace() {
755        let master = [0x37; 32];
756        let a = SigningKey::from_bytes(&[0x31; 32])
757            .verifying_key()
758            .to_bytes();
759        let b = SigningKey::from_bytes(&[0x32; 32])
760            .verifying_key()
761            .to_bytes();
762        let seed = one_time_seed(&master, &a, 11, 29, 17);
763        assert_ne!(seed, one_time_seed(&master, &b, 11, 29, 17));
764        assert_ne!(seed, one_time_seed(&master, &a, 12, 29, 17));
765        assert_ne!(seed, one_time_seed(&master, &a, 11, 30, 17));
766        assert_ne!(seed, one_time_seed(&master, &a, 11, 29, 18));
767        let mut material = [0; 84];
768        material[..32].copy_from_slice(&master);
769        material[32..64].copy_from_slice(&a);
770        material[64..72].copy_from_slice(&11u64.to_le_bytes());
771        material[72..80].copy_from_slice(&29u64.to_le_bytes());
772        material[80..].copy_from_slice(&17u32.to_le_bytes());
773        assert_eq!(
774            seed,
775            blake3::derive_key(crate::SEED_DERIVATION_CONTEXT, &material)
776        );
777    }
778
779    /// The complement checksum falls when a digit rises, so forward-only hashing cannot forge; a raw-sum checksum would not.
780    #[test]
781    fn complement_checksum_blocks_the_upstream_monotone_counterexample() {
782        let a = [1; 64];
783        let mut b = a;
784        b[0] = 2;
785        let da = digits_from_xof(&a);
786        let db = digits_from_xof(&b);
787        assert_eq!(&da[64..], &[0, 0, 0, 2]);
788        assert_eq!(&db[64..], &[3, 3, 3, 1]);
789        assert!(da.iter().zip(db).any(|(before, after)| after < *before));
790        // Structural fixture only: raw-sum upstream digits would permit this
791        // forward-only transformation. No message preimage attack is claimed.
792        let upstream_a = [0_u8, 0, 0, 1];
793        let upstream_b = [1_u8, 0, 0, 1];
794        assert!(upstream_a.iter().zip(upstream_b).all(|(x, y)| y >= *x));
795        assert_eq!(&digits_from_xof(&[0; 64])[64..], &[0, 0, 0, 3]);
796        assert_eq!(&digits_from_xof(&[255; 64])[64..], &[0, 0, 0, 0]);
797    }
798
799    /// A WOTS signature verifies for its message and fails for another message or a flipped node bit.
800    #[test]
801    fn one_time_chain_signature_round_trip_and_tamper_rejection() {
802        let signature = PreparedKey::from_seed([0x45; 32]).sign(b"sixteen byte msg");
803        assert!(signature.verify_hash_component(b"sixteen byte msg"));
804        assert!(!signature.verify_hash_component(b"different message"));
805        for index in [0, 32, 64, 67] {
806            let mut bad = signature.clone();
807            bad.elements[index][0] ^= 1;
808            assert!(!bad.verify_hash_component(b"sixteen byte msg"));
809        }
810    }
811
812    /// All 128 keys sign once, verify cold and warm, round-trip the wire format, refuse reuse and reject tampering.
813    #[test]
814    fn complete_batch_is_transferable_cached_and_single_use() {
815        let signer = SigningKey::from_bytes(&[0x29; 32]);
816        let trusted = signer.verifying_key();
817        let wrong = SigningKey::from_bytes(&[0x28; 32]).verifying_key();
818        let mut batch = PreparedBatch::new([0x61; 32], 17, 40, &signer);
819        let cached =
820            VerifiedBatch::authenticate(batch.certificate(), &trusted).expect("certificate");
821        assert!(VerifiedBatch::authenticate(batch.certificate(), &wrong).is_none());
822        for index in 0..BATCH_KEYS {
823            let message = (index as u128).to_le_bytes();
824            let signature = batch.sign(index, &message).expect("unused key");
825            assert!(signature.verify(&message, &trusted));
826            assert!(cached.verify(&signature, &message, &trusted));
827            assert!(!cached.can_verify_fast(&signature, &wrong));
828            assert!(batch.sign(index, &message).is_none());
829            assert_eq!(
830                DsigSignature::from_bytes(&signature.to_bytes()),
831                Some(signature.clone())
832            );
833            let mut bad = signature.clone();
834            bad.proof[0][0] ^= 1;
835            assert!(!bad.verify(&message, &trusted));
836            let mut bad = signature.clone();
837            bad.certificate.commitment.batch_id += 1;
838            assert!(!cached.can_verify_fast(&bad, &trusted));
839            assert!(!bad.verify(&message, &trusted));
840            let mut bad = signature.clone();
841            bad.key_index = 128;
842            assert!(!bad.verify(&message, &trusted));
843            assert!(DsigSignature::from_bytes(&bad.to_bytes()).is_none());
844        }
845        assert!(batch.sign(128, b"bad index").is_none());
846    }
847
848    /// Every leaf's proof rebuilds the root; a wrong index or index 128 is rejected.
849    #[test]
850    fn merkle_all_positions_and_invalid_indices() {
851        let leaves = core::array::from_fn(|i| *blake3::hash(&(i as u64).to_le_bytes()).as_bytes());
852        let tree = BatchTree::new(&leaves);
853        for (i, leaf) in leaves.iter().enumerate() {
854            let proof = tree.proof(i).expect("in range");
855            assert_eq!(root_from_proof(*leaf, i, &proof), Some(tree.root()));
856            assert_ne!(root_from_proof(*leaf, i ^ 1, &proof), Some(tree.root()));
857        }
858        assert!(tree.proof(128).is_none());
859        assert!(root_from_proof(leaves[0], 128, &[[0; 32]; 7]).is_none());
860    }
861}