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}