Skip to main content

wots_rhdl/
legacy.rs

1//! Byte-compatible `LEGACY_KECCAK` fused private-seed signing engine.
2//!
3//! This module is intentionally separate from the SHA-256 performance
4//! datapath.  It implements the pinned `HashSigsRS` construction with legacy
5//! Keccak-256 `0x01` padding; it is not SHA3-256, XMSS, or SLH-DSA.
6//!
7//! [`LegacyFusedPrivateSeedEngine`] owns one area-oriented
8//! [`keccak_rhdl::IterativeKeccak`] and executes the complete fixed-work fused
9//! operation serially.  It accepts an exact 32-byte private seed and an exact
10//! 32-byte already-hashed message, captures all 67 32-byte signature segments,
11//! derives the 64-byte public key, and emits the canonical 35-beat 512-bit
12//! result stream.  Every accepted operation executes exactly 1,173
13//! `Keccak-f[1600]` permutations.
14//!
15//! # Secret ownership
16//!
17//! The seed and derived private-key material live only in resetless internal
18//! datapath registers and are never connected to an output port.  Resettable
19//! control bits make those registers invalid after reset.  The input seed is
20//! overwritten as soon as its hash request is issued; the derived private key
21//! and transient chain secrets are overwritten after the final segment no
22//! longer needs them.  This is an architectural non-disclosure boundary, not a
23//! physical zeroization or side-channel claim.  The caller must still ensure
24//! that a seed is never submitted twice: WOTS private-key reuse is catastrophic.
25//!
26//! # Evidence boundary
27//!
28//! Unit tests exercise the fixed block builders and the composed RHDL machine.
29//! They do not establish synthesis, timing, utilization, route, side-channel,
30//! or physical-zeroization evidence.
31
32use keccak_rhdl::{IterativeInput, IterativeKeccak, KeccakState};
33use rhdl::prelude::*;
34use rhdl_fpga::core::dff::DFF;
35use rhdl_primitives::NoResetDff;
36
37use crate::digits::{CHAIN_COUNT, MessageBytes, MessageDigitArray, message_digits_kernel};
38
39/// Bytes in a legacy hash value, private seed, or already-hashed message.
40pub const LEGACY_HASH_BYTES: usize = 32;
41
42/// Observable randomization masks used by pinned `HashSigsRS`.
43pub const LEGACY_MASK_COUNT: usize = 16;
44
45/// Hash-chain transitions executed for every signature segment.
46pub const LEGACY_CHAIN_STEPS: usize = 15;
47
48/// `Keccak-f[1600]` permutations in one fused private-seed operation.
49pub const LEGACY_FUSED_PERMUTATIONS: usize = 1_173;
50
51/// Permutations when the explicit private-key wrapper bypasses seed hashing.
52pub const LEGACY_FUSED_PRIVATE_KEY_PERMUTATIONS: usize = 1_172;
53
54/// Number of 136-byte rate blocks needed for the 2,144-byte endpoint string.
55pub const LEGACY_ENDPOINT_BLOCKS: usize = 16;
56
57/// Number of 512-bit result beats.
58pub const LEGACY_FUSED_OUTPUT_BEATS: usize = 35;
59
60/// One 32-byte Keccak digest represented as four little-endian 64-bit lanes.
61pub type LegacyHashLanes = [b64; 4];
62
63/// All 67 signature or endpoint values in lane order.
64pub type LegacySegmentStore = [LegacyHashLanes; CHAIN_COUNT];
65
66/// All 16 observable WOTS randomization masks.
67pub type LegacyMaskStore = [LegacyHashLanes; LEGACY_MASK_COUNT];
68
69/// Exact 32-byte private-seed input in ascending byte order.
70pub type LegacyPrivateSeedBytes = [b8; LEGACY_HASH_BYTES];
71
72/// One 512-bit output transfer in ascending byte-lane order.
73pub type LegacyBeatBytes = [b8; 64];
74
75const PHASE_PRIVATE_SEED: u128 = 0;
76const PHASE_PUBLIC_SEED: u128 = 1;
77const PHASE_MASK: u128 = 2;
78const PHASE_SEGMENT_PRF: u128 = 3;
79const PHASE_SECRET_HASH: u128 = 4;
80const PHASE_CHAIN: u128 = 5;
81const PHASE_PUBLIC_KEY: u128 = 6;
82
83const FINAL_OUTPUT_BEAT: u128 = 34;
84const FULL_KEEP: u128 = u64::MAX as u128;
85const FINAL_KEEP: u128 = 0xffff_ffff;
86
87const _: () = {
88    assert!(CHAIN_COUNT == 67);
89    assert!(LEGACY_HASH_BYTES == 32);
90    assert!(LEGACY_MASK_COUNT == 16);
91    assert!(LEGACY_CHAIN_STEPS == 15);
92    assert!(LEGACY_FUSED_PERMUTATIONS == 1_173);
93    assert!(LEGACY_FUSED_PRIVATE_KEY_PERMUTATIONS == 1_172);
94    assert!(LEGACY_ENDPOINT_BLOCKS == 16);
95    assert!(LEGACY_FUSED_OUTPUT_BEATS == 35);
96};
97
98/// Input accepted by [`LegacyFusedPrivateSeedEngine`].
99#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
100pub struct LegacyFusedInput {
101    /// Presents one new one-time signing operation.
102    pub start_valid: bool,
103    /// Caller-supplied private seed.  It is captured only with a start transfer.
104    pub private_seed: LegacyPrivateSeedBytes,
105    /// Already-hashed 32-byte WOTS message.
106    pub message: MessageBytes,
107    /// Whole-result buffer credit available for reservation.
108    pub output_credit_available: bool,
109    /// Downstream accepts the currently presented output beat.
110    pub output_ready: bool,
111}
112
113/// Input accepted by [`LegacyFusedPrivateKeyEngine`].
114///
115/// This compatibility-only ingress exists so the five pinned upstream vectors,
116/// which provide a private key but no private-seed preimage, can exercise the
117/// complete hardware traversal.  It executes 1,172 permutations because it
118/// explicitly bypasses the one seed-to-private-key hash.
119#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
120pub struct LegacyFusedPrivateKeyInput {
121    /// Presents one new one-time signing operation.
122    pub start_valid: bool,
123    /// Exact 32-byte upstream-format private key.
124    pub private_key: LegacyPrivateSeedBytes,
125    /// Already-hashed 32-byte WOTS message.
126    pub message: MessageBytes,
127    /// Whole-result buffer credit available for reservation.
128    pub output_credit_available: bool,
129    /// Downstream accepts the currently presented output beat.
130    pub output_ready: bool,
131}
132
133/// One canonical fused-output stream beat.
134#[derive(Clone, Copy, Debug, Digital, Eq, PartialEq)]
135pub struct LegacyFusedBeat {
136    /// Sixty-four bytes in signature-then-public-key order.
137    pub data: LegacyBeatBytes,
138    /// One validity bit per byte lane.
139    pub keep: b64,
140    /// True only for beat 34.
141    pub last: bool,
142}
143
144impl Default for LegacyFusedBeat {
145    fn default() -> Self {
146        Self {
147            data: [b8(0); 64],
148            keep: b64(0),
149            last: false,
150        }
151    }
152}
153
154/// Status and stream output from either explicit legacy fused engine.
155#[allow(clippy::struct_excessive_bools)] // Each flag is an independent hardware contract.
156#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
157pub struct LegacyFusedOutput {
158    /// A start transfer is accepted when this and `start_valid` are true.
159    pub start_ready: bool,
160    /// The cryptographic traversal is in progress.
161    pub active: bool,
162    /// A whole-result credit is owned by this engine.
163    pub output_credit_reserved: bool,
164    /// The output beat is valid and remains stable under backpressure.
165    pub output_valid: bool,
166    /// Canonical output beat; zero whenever `output_valid` is false.
167    pub output: LegacyFusedBeat,
168    /// One-cycle pulse when the public-key hash completes.
169    pub computed_done: bool,
170    /// One-cycle pulse when the final output transfer returns its credit.
171    pub output_credit_release: bool,
172    /// Completed permutation count for the active or buffered result.
173    pub permutation_count: b11,
174    /// True when completion disagrees with the ingress type's exact work count.
175    pub audit_error: bool,
176}
177
178/// Input to [`legacy_prf_state_kernel`].
179#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
180pub struct LegacyPrfStateInput {
181    /// 32-byte PRF seed in digest lane order.
182    pub seed: LegacyHashLanes,
183    /// Big-endian 16-bit PRF index in the byte preimage.
184    pub index: b16,
185}
186
187/// Input to [`legacy_secret_state_kernel`].
188#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
189pub struct LegacySecretStateInput {
190    /// Function key, equal to mask zero.
191    pub function_key: LegacyHashLanes,
192    /// Domain-separated segment PRF result.
193    pub secret_prf: LegacyHashLanes,
194}
195
196/// Input to [`legacy_chain_state_kernel`].
197#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
198pub struct LegacyChainStateInput {
199    /// Current WOTS chain value.
200    pub state: LegacyHashLanes,
201    /// One-based transition mask.
202    pub mask: LegacyHashLanes,
203}
204
205/// Input to [`legacy_endpoint_block_state_kernel`].
206#[derive(Clone, Copy, Debug, Digital, Eq, PartialEq)]
207pub struct LegacyEndpointBlockInput {
208    /// State returned by the preceding endpoint absorption, or zero for block 0.
209    pub sponge: KeccakState,
210    /// All 67 chain endpoints in canonical segment order.
211    pub endpoints: LegacySegmentStore,
212    /// Fixed endpoint rate-block index in `0..=15`.
213    pub block_index: b5,
214}
215
216impl Default for LegacyEndpointBlockInput {
217    fn default() -> Self {
218        Self {
219            sponge: [b64(0); 25],
220            endpoints: [[b64(0); 4]; CHAIN_COUNT],
221            block_index: b5(0),
222        }
223    }
224}
225
226/// Input to [`legacy_output_beat_kernel`].
227#[derive(Clone, Copy, Debug, Digital, Eq, PartialEq)]
228pub struct LegacyOutputBeatInput {
229    /// Captured signature segments.
230    pub signature: LegacySegmentStore,
231    /// Public seed stored in the public key's first 32 bytes.
232    pub public_seed: LegacyHashLanes,
233    /// Hash of the concatenated endpoints.
234    pub public_key_hash: LegacyHashLanes,
235    /// Canonical beat index in `0..=34`.
236    pub beat_index: b6,
237}
238
239impl Default for LegacyOutputBeatInput {
240    fn default() -> Self {
241        Self {
242            signature: [[b64(0); 4]; CHAIN_COUNT],
243            public_seed: [b64(0); 4],
244            public_key_hash: [b64(0); 4],
245            beat_index: b6(0),
246        }
247    }
248}
249
250/// Convert 32 ascending bytes to four little-endian Keccak lanes.
251#[kernel]
252pub fn legacy_bytes_to_lanes_kernel(bytes: LegacyPrivateSeedBytes) -> LegacyHashLanes {
253    let mut lanes = [b64(0); 4];
254    for lane in 0..4 {
255        let byte_zero: b64 = bytes[lane * 8].resize();
256        let byte_one: b64 = bytes[lane * 8 + 1].resize();
257        let byte_two: b64 = bytes[lane * 8 + 2].resize();
258        let byte_three: b64 = bytes[lane * 8 + 3].resize();
259        let byte_four: b64 = bytes[lane * 8 + 4].resize();
260        let byte_five: b64 = bytes[lane * 8 + 5].resize();
261        let byte_six: b64 = bytes[lane * 8 + 6].resize();
262        let byte_seven: b64 = bytes[lane * 8 + 7].resize();
263        lanes[lane] = byte_zero
264            | (byte_one << 8)
265            | (byte_two << 16)
266            | (byte_three << 24)
267            | (byte_four << 32)
268            | (byte_five << 40)
269            | (byte_six << 48)
270            | (byte_seven << 56);
271    }
272    lanes
273}
274
275/// Build the single padded rate state for a 32-byte legacy Keccak hash.
276#[kernel]
277pub fn legacy_hash32_state_kernel(value: LegacyHashLanes) -> KeccakState {
278    let mut state = [b64(0); 25];
279    state[0] = value[0];
280    state[1] = value[1];
281    state[2] = value[2];
282    state[3] = value[3];
283    state[4] = b64(0x01);
284    state[16] = b64(0x8000_0000_0000_0000);
285    state
286}
287
288/// Build the padded state for `0x03 || seed || u16_be(index)` (35 bytes).
289#[kernel]
290pub fn legacy_prf_state_kernel(input: LegacyPrfStateInput) -> KeccakState {
291    let mut state = [b64(0); 25];
292    let index_high_byte: b8 = (input.index >> 8).resize();
293    let index_low_byte: b8 = input.index.resize();
294    let index_high: b64 = index_high_byte.resize();
295    let index_low: b64 = index_low_byte.resize();
296    state[0] = b64(0x03) | (input.seed[0] << 8);
297    state[1] = (input.seed[0] >> 56) | (input.seed[1] << 8);
298    state[2] = (input.seed[1] >> 56) | (input.seed[2] << 8);
299    state[3] = (input.seed[2] >> 56) | (input.seed[3] << 8);
300    state[4] = (input.seed[3] >> 56) | (index_high << 8) | (index_low << 16) | b64(0x01_00_00_00);
301    state[16] = b64(0x8000_0000_0000_0000);
302    state
303}
304
305/// Build the padded state for `function_key || secret_prf` (64 bytes).
306#[kernel]
307pub fn legacy_secret_state_kernel(input: LegacySecretStateInput) -> KeccakState {
308    let mut state = [b64(0); 25];
309    state[0] = input.function_key[0];
310    state[1] = input.function_key[1];
311    state[2] = input.function_key[2];
312    state[3] = input.function_key[3];
313    state[4] = input.secret_prf[0];
314    state[5] = input.secret_prf[1];
315    state[6] = input.secret_prf[2];
316    state[7] = input.secret_prf[3];
317    state[8] = b64(0x01);
318    state[16] = b64(0x8000_0000_0000_0000);
319    state
320}
321
322/// Build the padded state for one 32-byte randomized chain transition.
323#[kernel]
324pub fn legacy_chain_state_kernel(input: LegacyChainStateInput) -> KeccakState {
325    legacy_hash32_state_kernel([
326        input.state[0] ^ input.mask[0],
327        input.state[1] ^ input.mask[1],
328        input.state[2] ^ input.mask[2],
329        input.state[3] ^ input.mask[3],
330    ])
331}
332
333/// Extract the first 32 rate bytes from a completed permutation.
334#[kernel]
335pub fn legacy_digest_lanes_kernel(state: KeccakState) -> LegacyHashLanes {
336    [state[0], state[1], state[2], state[3]]
337}
338
339/// XOR one fixed 136-byte endpoint block into the preceding sponge state.
340///
341/// Blocks 0 through 14 contain 17 complete endpoint lanes.  Block 15 contains
342/// the final 13 lanes (104 bytes), then legacy domain byte `0x01`, zero fill,
343/// and final rate bit `0x80`.  Together these blocks cover the exact 2,144-byte
344/// endpoint string without a host-side byte buffer.
345#[kernel]
346#[allow(clippy::needless_range_loop)] // Static ranges lower predictably in RHDL.
347pub fn legacy_endpoint_block_state_kernel(input: LegacyEndpointBlockInput) -> KeccakState {
348    let mut state = input.sponge;
349    let block_wide: b9 = input.block_index.resize();
350    let base: b9 = (block_wide << 4) + block_wide;
351
352    if input.block_index < b5(15) {
353        for lane in 0..17 {
354            let absolute = base + b9(lane as u128);
355            let segment: b7 = (absolute >> 2).resize();
356            let word: b2 = absolute.resize();
357            state[lane] ^= input.endpoints[segment][word];
358        }
359    } else {
360        for lane in 0..13 {
361            let absolute = b9(255 + lane as u128);
362            let segment: b7 = (absolute >> 2).resize();
363            let word: b2 = absolute.resize();
364            state[lane] ^= input.endpoints[segment][word];
365        }
366        state[13] ^= b64(0x01);
367        state[16] ^= b64(0x8000_0000_0000_0000);
368    }
369    state
370}
371
372/// Select one canonical signature/public-key output beat.
373#[kernel]
374#[allow(clippy::comparison_chain, clippy::needless_range_loop)] // Explicit branches and static ranges describe the wire framing directly.
375pub fn legacy_output_beat_kernel(input: LegacyOutputBeatInput) -> LegacyFusedBeat {
376    let mut lanes = [b64(0); 8];
377    let mut keep = b64(FULL_KEEP);
378    let mut last = false;
379
380    if input.beat_index < b6(33) {
381        let beat_wide: b9 = input.beat_index.resize();
382        let base = beat_wide << 3;
383        for lane in 0..8 {
384            let absolute = base + b9(lane as u128);
385            let segment: b7 = (absolute >> 2).resize();
386            let word: b2 = absolute.resize();
387            lanes[lane] = input.signature[segment][word];
388        }
389    } else if input.beat_index == b6(33) {
390        lanes[0] = input.signature[66][0];
391        lanes[1] = input.signature[66][1];
392        lanes[2] = input.signature[66][2];
393        lanes[3] = input.signature[66][3];
394        lanes[4] = input.public_seed[0];
395        lanes[5] = input.public_seed[1];
396        lanes[6] = input.public_seed[2];
397        lanes[7] = input.public_seed[3];
398    } else {
399        lanes[0] = input.public_key_hash[0];
400        lanes[1] = input.public_key_hash[1];
401        lanes[2] = input.public_key_hash[2];
402        lanes[3] = input.public_key_hash[3];
403        keep = b64(FINAL_KEEP);
404        last = true;
405    }
406
407    let mut data = [b8(0); 64];
408    for lane in 0..8 {
409        data[lane * 8] = lanes[lane].resize();
410        data[lane * 8 + 1] = (lanes[lane] >> 8).resize();
411        data[lane * 8 + 2] = (lanes[lane] >> 16).resize();
412        data[lane * 8 + 3] = (lanes[lane] >> 24).resize();
413        data[lane * 8 + 4] = (lanes[lane] >> 32).resize();
414        data[lane * 8 + 5] = (lanes[lane] >> 40).resize();
415        data[lane * 8 + 6] = (lanes[lane] >> 48).resize();
416        data[lane * 8 + 7] = (lanes[lane] >> 56).resize();
417    }
418    LegacyFusedBeat { data, keep, last }
419}
420
421#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
422#[doc(hidden)]
423#[allow(clippy::struct_excessive_bools)] // Independent hardware ownership/control wires.
424pub struct LegacyControl {
425    active: bool,
426    waiting: bool,
427    credit_reserved: bool,
428    result_valid: bool,
429    computed_done: bool,
430    credit_release: bool,
431    audit_error: bool,
432    from_private_seed: bool,
433    phase: b3,
434    mask_index: b5,
435    segment: b7,
436    chain_step: b4,
437    public_key_block: b5,
438    output_beat: b6,
439    permutation_count: b11,
440}
441
442#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
443#[doc(hidden)]
444#[allow(clippy::struct_excessive_bools)] // Independent wrapper handshake wires.
445pub struct LegacyCoreInput {
446    start_valid: bool,
447    secret: LegacyPrivateSeedBytes,
448    message: MessageBytes,
449    output_credit_available: bool,
450    output_ready: bool,
451    from_private_seed: bool,
452}
453
454#[derive(Clone, Copy, Debug, Digital, Eq, PartialEq)]
455#[doc(hidden)]
456pub struct LegacyData {
457    private_seed: LegacyHashLanes,
458    message_digits: MessageDigitArray,
459    private_key: LegacyHashLanes,
460    public_seed: LegacyHashLanes,
461    masks: LegacyMaskStore,
462    signature: LegacySegmentStore,
463    endpoints: LegacySegmentStore,
464    secret_prf: LegacyHashLanes,
465    chain_state: LegacyHashLanes,
466    public_key_sponge: KeccakState,
467    public_key_hash: LegacyHashLanes,
468}
469
470/// Shared serial datapath behind the two statically explicit legacy wrappers.
471///
472/// Only one operation/result can be owned at a time.  An accepted start reserves
473/// one whole-result output credit.  Once computation completes, the engine
474/// presents 35 canonical beats and changes the beat index only after
475/// `output_valid && output_ready`.  Consequently every output byte and control
476/// bit remains stable for arbitrarily long downstream stalls.  The reserved
477/// credit is released only with the final beat transfer.
478#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
479#[rhdl(dq_no_prefix)]
480#[doc(hidden)]
481pub struct LegacyFusedCore {
482    keccak: IterativeKeccak,
483    control: DFF<LegacyControl>,
484    data: NoResetDff<LegacyData>,
485}
486
487impl Default for LegacyFusedCore {
488    fn default() -> Self {
489        Self {
490            keccak: IterativeKeccak::default(),
491            control: DFF::new(LegacyControl::default()),
492            data: NoResetDff::new(),
493        }
494    }
495}
496
497impl SynchronousIO for LegacyFusedCore {
498    type I = LegacyCoreInput;
499    type O = LegacyFusedOutput;
500    type Kernel = legacy_fused_core_kernel;
501}
502
503#[kernel]
504#[doc(hidden)]
505#[allow(clippy::used_underscore_binding)] // The RHDL macro consumes this port.
506pub fn legacy_fused_core_kernel(
507    _clock_reset: ClockReset,
508    input: LegacyCoreInput,
509    q: Q,
510) -> (LegacyFusedOutput, D) {
511    let mut d = D::dont_care();
512    let mut control = q.control;
513    let mut data = q.data;
514    d.keccak = IterativeInput::default();
515
516    control.computed_done = false;
517    control.credit_release = false;
518
519    let start_ready = !q.control.active && !q.control.result_valid && input.output_credit_available;
520
521    if start_ready && input.start_valid {
522        control.active = true;
523        control.waiting = false;
524        control.credit_reserved = true;
525        control.result_valid = false;
526        control.audit_error = false;
527        control.from_private_seed = input.from_private_seed;
528        if input.from_private_seed {
529            control.phase = b3(PHASE_PRIVATE_SEED);
530        } else {
531            control.phase = b3(PHASE_PUBLIC_SEED);
532        }
533        control.mask_index = b5(0);
534        control.segment = b7(0);
535        control.chain_step = b4(0);
536        control.public_key_block = b5(0);
537        control.output_beat = b6(0);
538        control.permutation_count = b11(0);
539        if input.from_private_seed {
540            data.private_seed = legacy_bytes_to_lanes_kernel(input.secret);
541            data.private_key = [b64(0); 4];
542        } else {
543            data.private_seed = [b64(0); 4];
544            data.private_key = legacy_bytes_to_lanes_kernel(input.secret);
545        }
546        data.message_digits = message_digits_kernel(input.message);
547        data.public_seed = [b64(0); 4];
548        data.masks = [[b64(0); 4]; LEGACY_MASK_COUNT];
549        data.signature = [[b64(0); 4]; CHAIN_COUNT];
550        data.endpoints = [[b64(0); 4]; CHAIN_COUNT];
551        data.secret_prf = [b64(0); 4];
552        data.chain_state = [b64(0); 4];
553        data.public_key_sponge = [b64(0); 25];
554        data.public_key_hash = [b64(0); 4];
555    } else if q.control.active {
556        if q.control.waiting {
557            if q.keccak.done {
558                let digest = legacy_digest_lanes_kernel(q.keccak.state);
559                control.waiting = false;
560                control.permutation_count = q.control.permutation_count + b11(1);
561
562                match q.control.phase {
563                    Bits::<3>(0) => {
564                        data.private_key = digest;
565                        control.phase = b3(PHASE_PUBLIC_SEED);
566                    }
567                    Bits::<3>(1) => {
568                        data.public_seed = digest;
569                        control.phase = b3(PHASE_MASK);
570                        control.mask_index = b5(0);
571                    }
572                    Bits::<3>(2) => {
573                        data.masks[q.control.mask_index] = digest;
574                        if q.control.mask_index == b5(15) {
575                            control.phase = b3(PHASE_SEGMENT_PRF);
576                            control.segment = b7(0);
577                        } else {
578                            control.mask_index = q.control.mask_index + b5(1);
579                        }
580                    }
581                    Bits::<3>(3) => {
582                        data.secret_prf = digest;
583                        control.phase = b3(PHASE_SECRET_HASH);
584                    }
585                    Bits::<3>(4) => {
586                        data.chain_state = digest;
587                        if q.data.message_digits[q.control.segment] == b4(0) {
588                            data.signature[q.control.segment] = digest;
589                        }
590                        control.phase = b3(PHASE_CHAIN);
591                        control.chain_step = b4(1);
592                    }
593                    Bits::<3>(5) => {
594                        data.chain_state = digest;
595                        if q.data.message_digits[q.control.segment] == q.control.chain_step {
596                            data.signature[q.control.segment] = digest;
597                        }
598                        if q.control.chain_step == b4(15) {
599                            data.endpoints[q.control.segment] = digest;
600                            if q.control.segment == b7(66) {
601                                // These secrets have reached their last use.
602                                data.private_key = [b64(0); 4];
603                                data.secret_prf = [b64(0); 4];
604                                data.chain_state = [b64(0); 4];
605                                control.phase = b3(PHASE_PUBLIC_KEY);
606                                control.public_key_block = b5(0);
607                                data.public_key_sponge = [b64(0); 25];
608                            } else {
609                                control.segment = q.control.segment + b7(1);
610                                control.phase = b3(PHASE_SEGMENT_PRF);
611                            }
612                        } else {
613                            control.chain_step = q.control.chain_step + b4(1);
614                        }
615                    }
616                    _ => {
617                        data.public_key_sponge = q.keccak.state;
618                        if q.control.public_key_block == b5(15) {
619                            data.public_key_hash = digest;
620                            control.active = false;
621                            control.result_valid = true;
622                            control.computed_done = true;
623                            if q.control.from_private_seed {
624                                control.audit_error = q.control.permutation_count != b11(1_172);
625                            } else {
626                                control.audit_error = q.control.permutation_count != b11(1_171);
627                            }
628                            control.output_beat = b6(0);
629                        } else {
630                            control.public_key_block = q.control.public_key_block + b5(1);
631                        }
632                    }
633                }
634            }
635        } else if q.keccak.ready {
636            let mut request_state = legacy_hash32_state_kernel(q.data.private_seed);
637            match q.control.phase {
638                Bits::<3>(0) => {
639                    // The original seed is never needed after this acceptance.
640                    data.private_seed = [b64(0); 4];
641                }
642                Bits::<3>(1) => {
643                    request_state = legacy_prf_state_kernel(LegacyPrfStateInput {
644                        seed: q.data.private_key,
645                        index: b16(0),
646                    });
647                }
648                Bits::<3>(2) => {
649                    request_state = legacy_prf_state_kernel(LegacyPrfStateInput {
650                        seed: q.data.public_seed,
651                        index: q.control.mask_index.resize(),
652                    });
653                }
654                Bits::<3>(3) => {
655                    let segment_index: b16 = q.control.segment.resize();
656                    request_state = legacy_prf_state_kernel(LegacyPrfStateInput {
657                        seed: q.data.private_key,
658                        index: segment_index + b16(1),
659                    });
660                }
661                Bits::<3>(4) => {
662                    request_state = legacy_secret_state_kernel(LegacySecretStateInput {
663                        function_key: q.data.masks[0],
664                        secret_prf: q.data.secret_prf,
665                    });
666                }
667                Bits::<3>(5) => {
668                    request_state = legacy_chain_state_kernel(LegacyChainStateInput {
669                        state: q.data.chain_state,
670                        mask: q.data.masks[q.control.chain_step],
671                    });
672                }
673                _ => {
674                    request_state = legacy_endpoint_block_state_kernel(LegacyEndpointBlockInput {
675                        sponge: q.data.public_key_sponge,
676                        endpoints: q.data.endpoints,
677                        block_index: q.control.public_key_block,
678                    });
679                }
680            }
681            d.keccak = IterativeInput {
682                start: true,
683                state: request_state,
684            };
685            control.waiting = true;
686        }
687    } else if q.control.result_valid && input.output_ready {
688        if q.control.output_beat == b6(FINAL_OUTPUT_BEAT) {
689            control.result_valid = false;
690            control.credit_reserved = false;
691            control.credit_release = true;
692            control.output_beat = b6(0);
693            control.permutation_count = b11(0);
694            // Public output material may also be invalidated after transfer.
695            data.public_seed = [b64(0); 4];
696            data.public_key_hash = [b64(0); 4];
697            data.signature = [[b64(0); 4]; CHAIN_COUNT];
698            data.endpoints = [[b64(0); 4]; CHAIN_COUNT];
699            data.masks = [[b64(0); 4]; LEGACY_MASK_COUNT];
700        } else {
701            control.output_beat = q.control.output_beat + b6(1);
702        }
703    }
704
705    let mut output_beat = LegacyFusedBeat {
706        data: [b8(0); 64],
707        keep: b64(0),
708        last: false,
709    };
710    if q.control.result_valid {
711        output_beat = legacy_output_beat_kernel(LegacyOutputBeatInput {
712            signature: q.data.signature,
713            public_seed: q.data.public_seed,
714            public_key_hash: q.data.public_key_hash,
715            beat_index: q.control.output_beat,
716        });
717    }
718
719    d.control = control;
720    d.data = data;
721    let output = LegacyFusedOutput {
722        start_ready,
723        active: q.control.active,
724        output_credit_reserved: q.control.credit_reserved,
725        output_valid: q.control.result_valid,
726        output: output_beat,
727        computed_done: q.control.computed_done,
728        output_credit_release: q.control.credit_release,
729        permutation_count: q.control.permutation_count,
730        audit_error: q.control.audit_error,
731    };
732    (output, d)
733}
734
735mod seed_wrapper {
736    #[allow(clippy::wildcard_imports)] // RHDL derives require their generated support traits.
737    use super::*;
738
739    /// Fused legacy signing engine with an explicit private-seed ingress.
740    ///
741    /// Each accepted operation executes exactly 1,173 permutations: the shared
742    /// 1,172-permutation private-key traversal plus the seed-to-private-key hash.
743    /// The explicit type prevents a private key from being mistaken for a seed in
744    /// test manifests or host adapters.
745    ///
746    /// The authored machine contains 43,460 logical storage bits: 41,804 bits in
747    /// the outer resetless datapath store, 1,600 bits in the resetless permutation
748    /// state, and 56 resettable control bits.  This is a source-level register-bit
749    /// inventory, not a claim about inferred flip-flops, BRAMs, LUTs, or any mapped
750    /// FPGA resource.
751    #[derive(Clone, Debug, Default, Synchronous, SynchronousDQ)]
752    #[rhdl(dq_no_prefix)]
753    pub struct LegacyFusedPrivateSeedEngine {
754        core: LegacyFusedCore,
755    }
756
757    impl SynchronousIO for LegacyFusedPrivateSeedEngine {
758        type I = LegacyFusedInput;
759        type O = LegacyFusedOutput;
760        type Kernel = legacy_fused_private_seed_kernel;
761    }
762
763    /// Statically bind a private-seed request to the shared legacy datapath.
764    #[kernel]
765    #[allow(clippy::used_underscore_binding)] // The RHDL macro consumes this port.
766    pub fn legacy_fused_private_seed_kernel(
767        _clock_reset: ClockReset,
768        input: LegacyFusedInput,
769        q: Q,
770    ) -> (LegacyFusedOutput, D) {
771        let mut d = D::dont_care();
772        d.core = LegacyCoreInput {
773            start_valid: input.start_valid,
774            secret: input.private_seed,
775            message: input.message,
776            output_credit_available: input.output_credit_available,
777            output_ready: input.output_ready,
778            from_private_seed: true,
779        };
780        (q.core, d)
781    }
782}
783
784pub use seed_wrapper::{LegacyFusedPrivateSeedEngine, legacy_fused_private_seed_kernel};
785
786mod key_wrapper {
787    #[allow(clippy::wildcard_imports)] // RHDL derives require their generated support traits.
788    use super::*;
789
790    /// Fused legacy signing engine with an explicit private-key ingress.
791    ///
792    /// This wrapper is byte-compatible with the private keys in the five pinned
793    /// upstream vectors.  It executes exactly 1,172 permutations and cannot be
794    /// confused with [`LegacyFusedPrivateSeedEngine`] at the Rust/RHDL type
795    /// boundary.  Its storage inventory and stream/credit contract are otherwise
796    /// identical to the private-seed engine.
797    #[derive(Clone, Debug, Default, Synchronous, SynchronousDQ)]
798    #[rhdl(dq_no_prefix)]
799    pub struct LegacyFusedPrivateKeyEngine {
800        core: LegacyFusedCore,
801    }
802
803    impl SynchronousIO for LegacyFusedPrivateKeyEngine {
804        type I = LegacyFusedPrivateKeyInput;
805        type O = LegacyFusedOutput;
806        type Kernel = legacy_fused_private_key_kernel;
807    }
808
809    /// Statically bind a private-key request to the shared legacy datapath.
810    #[kernel]
811    #[allow(clippy::used_underscore_binding)] // The RHDL macro consumes this port.
812    pub fn legacy_fused_private_key_kernel(
813        _clock_reset: ClockReset,
814        input: LegacyFusedPrivateKeyInput,
815        q: Q,
816    ) -> (LegacyFusedOutput, D) {
817        let mut d = D::dont_care();
818        d.core = LegacyCoreInput {
819            start_valid: input.start_valid,
820            secret: input.private_key,
821            message: input.message,
822            output_credit_available: input.output_credit_available,
823            output_ready: input.output_ready,
824            from_private_seed: false,
825        };
826        (q.core, d)
827    }
828}
829
830pub use key_wrapper::{LegacyFusedPrivateKeyEngine, legacy_fused_private_key_kernel};