Skip to main content

rhdl_dsig_scalable_core/
chain_lane.rs

1//! The chain hash: Haraka-512, BLAKE3 or SHA-512, chosen at compile time.
2//!
3//! # Why there is a choice
4//!
5//! The chain hash decides most of the signer's area (see the table below),
6//! so the crate can be built with any of three DSig-compatible chain hashes
7//! and each build measured on its own. Haraka-512 is made of AES rounds,
8//! which map well to FPGA tables. This module is the single switch: it names the
9//! lane type ([`ChainLane`]), its latency ([`LANE_LATENCY`]), the software
10//! reference ([`software_chain`]) and the profile name ([`PROFILE`]) for the
11//! hash picked by the `DSIG_CHAIN` build variable (`build.rs`). Nothing else in the crate needs to know
12//! which hash was chosen.
13//!
14//! A W-OTS+ chain step hashes one 64-byte *chain block*
15//! ([`dsig_chain_block`](crate::haraka::dsig_chain_block): a salted 16-byte
16//! nonce, the 18-byte node, zeros) and keeps 18 bytes. That is 204 of the ~251
17//! hash calls in a signature, so the chain hash decides most of the area. The
18//! three DSig-compatible choices:
19//!
20//! | Build variable | Profile | Chain step | Lane hardware | Latency |
21//! |---|---|---|---|---|
22//! | `DSIG_CHAIN=haraka` (default) | `DSIG_FPGA_W4_HARAKA_BLAKE3_V1` | `Haraka-512(block)` truncated, mid bytes | [`TableChainStepLane`](crate::chain_step::TableChainStepLane), 494 S-boxes | 20 |
23//! | `DSIG_CHAIN=blake3` | `DSIG_FPGA_W4_BLAKE3CHAIN_BLAKE3_V1` | `BLAKE3(block)[..18]` (one compression) | [`Blake3ChainLane`] | 28 |
24//! | `DSIG_CHAIN=sha512` | `DSIG_FPGA_W4_SHA512CHAIN_BLAKE3_V1` | `SHA-512(block)[..18]` (one padded block) | [`Sha512ChainLane`] | 82 |
25//!
26//! Everything else (seeds, XOF, commitment, Merkle, message digest) stays
27//! BLAKE3 in every profile. Each profile is a different signature scheme:
28//! signatures from one never verify under another, and [`PROFILE`] names the
29//! one compiled in.
30//!
31//! Every lane has the same ports as the Haraka lane
32//! ([`ChainStepInput`] → [`ChainStepOutput`]) and publishes its latency as
33//! [`LANE_LATENCY`], which the chain engine and the factory's timetable use.
34//!
35//! # How the choice spreads through the slice
36//!
37//! ```text
38//!   chain-* feature ─▶ ChainLane, LANE_LATENCY
39//!        │                 │
40//!        │                 ▼
41//!        │     slice_chain: SLICE_CHAIN_LATENCY = 3 × LANE_LATENCY
42//!        │                 │
43//!        │                 ▼
44//!        │     slice_factory: END_DELAY = SLICE_CHAIN_LATENCY + 2
45//!        │                 ├─▶ SCHEDULE (where commitment ops can go)
46//!        │                 ├─▶ PACK_HISTORY (older pk_nonces kept)
47//!        └───────────────▶ └─▶ CTX_BITS (5 with DSIG_CHAIN=sha512, else 4)
48//! ```
49//!
50//! With SHA-512 the chain ends of a key emerge about three key periods after
51//! its chains start. By then newer keys are feeding the engine, which is why
52//! the factory keeps older `pk_nonce`s and needs 32 key contexts instead of
53//! 16 (see [`crate::slice_factory::PACK_HISTORY`] and
54//! [`crate::slice_factory::CTX_BITS`]).
55
56use crate::blake3_core::{CompressionInput, InlineCompressionPipeline};
57use crate::cert_unit::FLAGS_SINGLE_BLOCK;
58use crate::chain_step::{ChainStepInput, ChainStepOutput, STEP_OUTPUT_BYTES, chain_step_block_kernel};
59use rhdl::prelude::*;
60use rhdl_fpga::core::dff::DFF;
61use rhdl_sha512_pipelined::{LATENCY as SHA_LATENCY, Sha512Pipeline, ShaIn};
62
63// Exactly one chain hash is compiled in: `build.rs` reads `DSIG_CHAIN`
64// (haraka, blake3 or sha512; default haraka), rejects anything else, and sets
65// `cfg(dsig_chain = "…")` to that one name.
66
67/// The compiled-in DSig profile.
68///
69/// A profile name identifies the whole signature scheme (w = 4, the chain
70/// hash, BLAKE3 for everything else). Signatures made under one profile
71/// never verify under another.
72/// Haraka is the default and the experimental profile this project
73/// preserves.
74#[cfg(dsig_chain = "haraka")]
75pub const PROFILE: &str = "DSIG_FPGA_W4_HARAKA_BLAKE3_V1";
76/// The compiled-in DSig profile.
77#[cfg(dsig_chain = "blake3")]
78pub const PROFILE: &str = "DSIG_FPGA_W4_BLAKE3CHAIN_BLAKE3_V1";
79/// The compiled-in DSig profile.
80#[cfg(dsig_chain = "sha512")]
81pub const PROFILE: &str = "DSIG_FPGA_W4_SHA512CHAIN_BLAKE3_V1";
82
83/// The lane type the chain engine instantiates three times per slice.
84///
85/// `MASK` is an area knob for the Haraka T-table lane (which AES rounds use
86/// table ROMs); the BLAKE3 and SHA-512 lanes accept it and ignore it, so the
87/// rest of the crate has one signature for all three.
88#[cfg(dsig_chain = "haraka")]
89pub type ChainLane<const MASK: usize> = crate::chain_step::TableChainStepLane<MASK>;
90/// The lane type the chain engine instantiates three times per slice.
91#[cfg(dsig_chain = "blake3")]
92pub type ChainLane<const MASK: usize> = Blake3ChainLane<MASK>;
93/// The lane type the chain engine instantiates three times per slice.
94#[cfg(dsig_chain = "sha512")]
95pub type ChainLane<const MASK: usize> = Sha512ChainLane<MASK>;
96
97/// Cycles from a lane's input to its output.
98///
99/// Haraka: 20 (two pipeline stages per AES round, 10 rounds).
100#[cfg(dsig_chain = "haraka")]
101pub const LANE_LATENCY: usize = crate::chain_step::CHAIN_STEP_LATENCY;
102/// Cycles from a lane's input to its output.
103///
104/// BLAKE3: 28, the compression pipeline's depth.
105#[cfg(dsig_chain = "blake3")]
106pub const LANE_LATENCY: usize = crate::blake3_core::PIPELINE_STAGES;
107/// Cycles from a lane's input to its output.
108///
109/// SHA-512: 82, the depth of the pipelined core from the
110/// `rhdl_sha512_pipelined` crate.
111#[cfg(dsig_chain = "sha512")]
112pub const LANE_LATENCY: usize = SHA_LATENCY;
113
114/// Software chain step for the compiled-in profile (the reference).
115///
116/// Input: the 64-byte chain block. Output: the next 18-byte chain node.
117/// Tests compare every hardware lane against this function bit for bit.
118#[must_use]
119pub fn software_chain(block: &[u8; 64]) -> [u8; STEP_OUTPUT_BYTES] {
120    // Haraka: the 18 middle bytes of Haraka-512 (see `haraka`).
121    #[cfg(dsig_chain = "haraka")]
122    {
123        crate::haraka::mid_haraka512(block)
124    }
125    // BLAKE3: the first 18 bytes of the standard hash.
126    #[cfg(dsig_chain = "blake3")]
127    {
128        blake3::hash(block).as_bytes()[..STEP_OUTPUT_BYTES].try_into().unwrap()
129    }
130    // SHA-512: the first 18 bytes of the digest.
131    #[cfg(dsig_chain = "sha512")]
132    {
133        use sha2::Digest;
134        sha2::Sha512::digest(block)[..STEP_OUTPUT_BYTES].try_into().unwrap()
135    }
136}
137
138// ---------------------------------------------------------------- BLAKE3 lane
139
140/// Little-endian words of a 64-byte block.
141///
142/// A thin wrapper around
143/// [`words_from_bytes_kernel`](crate::slice_factory::words_from_bytes_kernel),
144/// because BLAKE3 reads its message block as little-endian 32-bit words.
145#[kernel]
146pub fn le_words_kernel(block: [b8; 64]) -> [b32; 16] {
147    crate::slice_factory::words_from_bytes_kernel(block)
148}
149
150/// A chain step as one BLAKE3 compression: the standard single-block hash of
151/// the 64-byte chain block (`CHUNK_START | CHUNK_END | ROOT`, counter 0),
152/// truncated to 18 bytes. One step per clock, [`PIPELINE_STAGES`](crate::blake3_core::PIPELINE_STAGES) cycles.
153#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
154pub struct Blake3ChainLane<const MASK: usize> {
155    /// The same 28-stage BLAKE3 compression pipeline the factory uses. Its
156    /// 32-bit tag already fits the chain tag, so no extra delay line is needed.
157    core: InlineCompressionPipeline,
158}
159
160/// A BLAKE3 lane with an empty pipeline.
161impl<const MASK: usize> Default for Blake3ChainLane<MASK> {
162    /// Builds the pipeline.
163    fn default() -> Self {
164        Self {
165            core: InlineCompressionPipeline,
166        }
167    }
168}
169
170/// Gives the BLAKE3 lane the same ports as the Haraka lane.
171impl<const MASK: usize> SynchronousIO for Blake3ChainLane<MASK> {
172    /// Salted nonce, node, tag, valid.
173    type I = ChainStepInput;
174    /// Next node, tag, valid, ready.
175    type O = ChainStepOutput;
176    /// The wiring, [`blake3_chain_lane_kernel`].
177    type Kernel = blake3_chain_lane_kernel<MASK>;
178}
179
180/// Build the block, compress, keep the first 18 output bytes.
181///
182/// The input enters the pipeline this cycle; the matching output appears
183/// [`PIPELINE_STAGES`](crate::blake3_core::PIPELINE_STAGES) (28) cycles later. `ready` is always true: there are
184/// no tables to load.
185#[allow(clippy::needless_range_loop)]
186#[kernel]
187pub fn blake3_chain_lane_kernel<const MASK: usize>(
188    _cr: ClockReset,
189    input: ChainStepInput,
190    q: Blake3ChainLaneQ<MASK>,
191) -> (ChainStepOutput, Blake3ChainLaneD<MASK>) {
192    // The 64-byte chain block: salted nonce, node, zeros.
193    let block = chain_step_block_kernel(input.salted_nonce, input.secret);
194    // One-block root hash from the IV, counter 0, full 64-byte block.
195    let d = Blake3ChainLaneD::<MASK> {
196        core: CompressionInput {
197            cv: crate::slice_factory::IV_BITS,
198            block: le_words_kernel(block),
199            counter_low: b32(0),
200            counter_high: b32(0),
201            block_len: b32(64),
202            flags: FLAGS_SINGLE_BLOCK,
203            tag: input.tag,
204            valid: input.valid,
205        },
206    };
207    // Output side: the pipeline's result for the input of 28 cycles ago.
208    let bytes = crate::slice_factory::bytes_from_words_kernel(q.core.words);
209    // The next node is the first 18 bytes of the hash.
210    let mut node = [b8(0); STEP_OUTPUT_BYTES];
211    for b in 0..STEP_OUTPUT_BYTES {
212        node[b] = bytes[b];
213    }
214    (
215        ChainStepOutput {
216            node,
217            tag: q.core.tag,
218            valid: q.core.valid,
219            ready: true,
220        },
221        d,
222    )
223}
224
225// ---------------------------------------------------------------- SHA-512 lane
226
227/// The single padded SHA-512 block of a 64-byte message, big-endian words:
228/// the message, `0x80`, zeros, and the 128-bit length 512.
229///
230/// SHA-512 works on 128-byte blocks. A 64-byte message plus its padding
231/// fits in exactly one block, so one pass of the core is one chain step.
232/// Combinational.
233#[kernel]
234pub fn sha512_block_kernel(message: [b8; 64]) -> [b64; 16] {
235    let mut words = [b64(0); 16];
236    // Words 0..8: the message, eight bytes per word, first byte highest.
237    for w in 0..8 {
238        words[w] = (message[8 * w].resize::<64>() << 56)
239            | (message[8 * w + 1].resize::<64>() << 48)
240            | (message[8 * w + 2].resize::<64>() << 40)
241            | (message[8 * w + 3].resize::<64>() << 32)
242            | (message[8 * w + 4].resize::<64>() << 24)
243            | (message[8 * w + 5].resize::<64>() << 16)
244            | (message[8 * w + 6].resize::<64>() << 8)
245            | message[8 * w + 7].resize::<64>();
246    }
247    // Padding: a single 1 bit right after the message, zeros, then the
248    // message length in bits (64 × 8 = 512) in the last word.
249    words[8] = b64(0x8000_0000_0000_0000);
250    words[15] = b64(512);
251    words
252}
253
254/// A chain step as one SHA-512 block on the shared pipelined core
255/// ([`rhdl_sha512_pipelined`]), truncated to 18 bytes. One step per clock,
256/// [`SHA_LATENCY`] cycles. The 32-bit chain tag rides a parallel delay line
257/// because the core's tag is 16 bits.
258#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
259pub struct Sha512ChainLane<const MASK: usize> {
260    /// The pipelined SHA-512 core from `rhdl_sha512_pipelined`.
261    core: Sha512Pipeline,
262    /// Delay line for the 32-bit chain tag, [`SHA_LATENCY`] stages, so the tag
263    /// leaves together with its digest.
264    tag: [DFF<b32>; SHA_LATENCY],
265}
266
267/// A SHA-512 lane with an empty pipeline and zeroed tag line.
268impl<const MASK: usize> Default for Sha512ChainLane<MASK> {
269    /// Builds the core and the tag delay line.
270    fn default() -> Self {
271        Self {
272            core: Sha512Pipeline::default(),
273            tag: core::array::from_fn(|_| DFF::new(b32(0))),
274        }
275    }
276}
277
278/// Gives the SHA-512 lane the same ports as the Haraka lane.
279impl<const MASK: usize> SynchronousIO for Sha512ChainLane<MASK> {
280    /// Salted nonce, node, tag, valid.
281    type I = ChainStepInput;
282    /// Next node, tag, valid, ready.
283    type O = ChainStepOutput;
284    /// The wiring, [`sha512_chain_lane_kernel`].
285    type Kernel = sha512_chain_lane_kernel<MASK>;
286}
287
288/// Build and pad the block, hash, keep the first 18 digest bytes.
289///
290/// The input enters the core this cycle; its digest and tag appear
291/// [`SHA_LATENCY`] (82) cycles later. `ready` is always true.
292#[allow(clippy::needless_range_loop)]
293#[kernel]
294pub fn sha512_chain_lane_kernel<const MASK: usize>(
295    _cr: ClockReset,
296    input: ChainStepInput,
297    q: Sha512ChainLaneQ<MASK>,
298) -> (ChainStepOutput, Sha512ChainLaneD<MASK>) {
299    // The 64-byte chain block: salted nonce, node, zeros.
300    let block = chain_step_block_kernel(input.salted_nonce, input.secret);
301    // Shift the tag delay line by one stage.
302    let mut tag = q.tag;
303    tag[0] = input.tag;
304    for k in 1..SHA_LATENCY {
305        tag[k] = q.tag[k - 1];
306    }
307    let d = Sha512ChainLaneD::<MASK> {
308        // Start from the standard SHA-512 IV; one block, no chaining. The
309        // core's own 16-bit tag is unused (the delay line carries ours).
310        core: ShaIn {
311            valid: input.valid,
312            tag: b16(0),
313            block: sha512_block_kernel(block),
314            use_iv: true,
315            chain: [b64(0); 8],
316        },
317        tag,
318    };
319    // The finished hash state (eight 64-bit words) for the input 82 cycles ago.
320    let state = q.core.done.state;
321    let mut node = [b8(0); STEP_OUTPUT_BYTES];
322    // Digest bytes are big-endian within each word: bytes 0..16 from words
323    // 0 and 1, bytes 16 and 17 from the top of word 2.
324    for w in 0..2 {
325        for k in 0..8 {
326            node[8 * w + k] = (state[w] >> (56 - 8 * k as u128)).resize();
327        }
328    }
329    node[16] = (state[2] >> 56).resize();
330    node[17] = (state[2] >> 48).resize();
331    (
332        ChainStepOutput {
333            node,
334            tag: q.tag[SHA_LATENCY - 1],
335            valid: q.core.done.valid,
336            ready: true,
337        },
338        d,
339    )
340}