Skip to main content

rhdl_dsig_scalable_core/ubft/
probe.rs

1//! A uBFT stand-in for measuring [`super::UbftEngine`] on one card.
2//!
3//! # The problem
4//!
5//! The service's latency only means something when it is measured on the
6//! card, at the card's clock, through the same RTL a uBFT replica would use.
7//! But a replica needs peers, a network and a host stack; one card has none
8//! of that. The probe plays the replica's part of the conversation:
9//!
10//! ```text
11//!   probe ──sign request i──▶ UbftEngine ──frame i──▶ probe ──▶ HBM (frame i)
12//!         ──verify request (header + frame i, message patched in)──▶
13//!         ◀──verdict i── check: valid unless i was tampered, tag == i
14//! ```
15//!
16//! Requests go one at a time with `gap` idle cycles between them, so every
17//! request is isolated and the engine's own latency counters (see
18//! [`super`]) time it. The frame goes to HBM so the host checks every
19//! signature against the software reference and Ed25519.
20//!
21//! # Messages
22//!
23//! Request `i` signs the 16 bytes `i (LE64) ‖ !i (LE64)` with tag `i`. When
24//! `tamper_every` is `t > 0`, every request with `i mod t == t − 1` is
25//! verified with message byte 0 flipped, and must be rejected.
26//!
27//! In DSig profile 2 (`DSIG_MESSAGE_BYTES=64`) request `i` signs a 48-byte
28//! context zero-extended to 64 bytes: `i ‖ !i`, then those 16 bytes XOR
29//! `0xa5`, then XOR `0x3c` ([`probe_message_kernel`]). The sign request is
30//! in layout v2 (tag 0..8, context 8..56) and the verify record is 27
31//! beats: the frame's last beat carries the message's first 22 bytes, and
32//! an appended beat the other 42 (as `crate::verify::record`).
33//!
34//! # Feeding the verify request at full rate
35//!
36//! The frame is kept in a 32-row block RAM while it arrives. During the
37//! verify request the RAM is read one row ahead: the read address is always
38//! the row that will be presented next cycle, so a taken beat is replaced by
39//! the next one without a bubble.
40
41use super::{Axis64, Axis512};
42use crate::slice_signer::FrameBeat;
43use rhdl::prelude::*;
44use rhdl_fpga::core::{
45    dff::DFF,
46    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
47};
48
49/// Probe arguments.
50#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
51pub struct ProbeArgs {
52    /// Requests to make.
53    pub count: b32,
54    /// Idle cycles between one verdict and the next sign request.
55    pub gap: b32,
56    /// Key slot the verify requests name.
57    pub slot: b4,
58    /// Tamper with every `tamper_every`-th verification (0: never).
59    pub tamper_every: b32,
60    /// Start (once the engine runs).
61    pub go: bool,
62}
63
64/// Probe inputs.
65#[derive(Clone, Copy, Debug, PartialEq, Digital)]
66pub struct ProbeIn {
67    /// Arguments.
68    pub args: ProbeArgs,
69    /// The engine's `s_axis_sign` `tready`.
70    pub sign_ready: bool,
71    /// The engine's `m_axis_frame`.
72    pub frame: Axis512,
73    /// The engine's `s_axis_verify` `tready`.
74    pub verify_ready: bool,
75    /// The engine's `m_axis_verdict`.
76    pub verdict: Axis64,
77    /// The HBM writer takes the presented beat.
78    pub writer_ready: bool,
79}
80
81/// Probe outputs.
82#[derive(Clone, Copy, Debug, PartialEq, Digital)]
83pub struct ProbeOut {
84    /// Sign request to the engine.
85    pub sign: Axis512,
86    /// Frame `tready` to the engine.
87    pub frame_ready: bool,
88    /// Verify request to the engine.
89    pub verify: Axis512,
90    /// Verdict `tready` to the engine.
91    pub verdict_ready: bool,
92    /// Frame beat to the HBM writer.
93    pub beat: FrameBeat,
94    /// Requests whose verdict came back.
95    pub done: b32,
96    /// Verdicts that matched the expectation (valid, or rejected when
97    /// tampered) and carried the right tag.
98    pub matched: b32,
99    /// Verdicts that did not.
100    pub mismatched: b32,
101    /// The first mismatching verdict word (0 if none).
102    pub first_bad: b64,
103}
104
105/// Phases of one request.
106const IDLE: b3 = b3(0);
107/// Present the sign request.
108const SIGN: b3 = b3(1);
109/// Receive the frame (and pass it to HBM).
110const FRAME: b3 = b3(2);
111/// Present the verify header.
112const HEADER: b3 = b3(3);
113/// Present the frame beats.
114const BODY: b3 = b3(4);
115/// Wait for the verdict.
116const VERDICT: b3 = b3(5);
117/// The frame's last beat number.
118const LAST_BEAT: b5 = b5(25);
119/// Byte of beat 25 where the record's message starts (1642 − 25·64).
120const MESSAGE_AT: usize = 42;
121/// Profile 2: send the record's 27th beat.
122const TAIL: b3 = b3(6);
123/// DSig profile 2 (64-byte messages; see the module notes).
124const P2: bool = crate::MESSAGE_BYTES == 64;
125/// Bytes of the signed message.
126const MSG: usize = crate::MESSAGE_BYTES;
127// Loop bounds that are zero in the other profile, so no index reaches past
128// a 16-byte message in profile 1.
129/// Profile 1's message bytes in beat 25 and in the sign request.
130const P1_MSG: usize = if P2 { 0 } else { 16 };
131/// Profile 2's 48-byte context in the sign request.
132const P2_CTX: usize = if P2 { 48 } else { 0 };
133/// Profile 2's message bytes in beat 25 (1642..1664).
134const P2_HEAD: usize = if P2 { 22 } else { 0 };
135/// Profile 2's message bytes in the appended beat (1664..1706).
136const P2_TAIL: usize = if P2 { 42 } else { 0 };
137/// Profile 2's context bytes 16..48 (two derived 16-byte runs).
138const P2_RUN: usize = if P2 { 16 } else { 0 };
139/// Byte of the sign request where the tag starts.
140const SIGN_TAG_AT: usize = if P2 { 0 } else { 16 };
141/// Beat 25's first byte after the message: zeros from here.
142const ZERO_FROM: usize = MESSAGE_AT + P1_MSG + P2_HEAD;
143
144/// Resettable probe state.
145#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
146#[doc(hidden)]
147pub struct ProbeState {
148    phase: b3,
149    /// Current request number.
150    i: b32,
151    /// Beat number within the frame.
152    beat: b5,
153    /// Idle cycles left before the next request.
154    wait: b32,
155    /// The current request is tampered.
156    tampered: bool,
157    /// Requests since the last tampered one.
158    since: b32,
159    done: b32,
160    matched: b32,
161    mismatched: b32,
162    first_bad: b64,
163}
164
165/// The probe.
166#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
167pub struct UbftProbe {
168    /// Phases, counters.
169    state: DFF<ProbeState>,
170    /// The current frame, one beat per row.
171    frame: SyncBRAM<[b8; 64], 5>,
172}
173
174impl Default for UbftProbe {
175    /// Idle.
176    fn default() -> Self {
177        Self {
178            state: DFF::new(ProbeState::default()),
179            frame: SyncBRAM::default(),
180        }
181    }
182}
183
184impl SynchronousIO for UbftProbe {
185    type I = ProbeIn;
186    type O = ProbeOut;
187    type Kernel = ubft_probe_kernel;
188}
189
190/// The message of request `i`: `i ‖ !i`, little endian (16 bytes); in
191/// profile 2 followed by those bytes XOR `0xa5` and XOR `0x3c`, then 16 zero
192/// bytes (a 48-byte context zero-extended to 64).
193#[allow(clippy::needless_range_loop)]
194#[kernel]
195pub fn probe_message_kernel(i: b32) -> [b8; MSG] {
196    let x: b64 = i.resize();
197    let y: b64 = !x;
198    let mut m = [b8(0); MSG];
199    for k in 0..8 {
200        m[k] = (x >> ((8 * k) as u128)).resize();
201        m[8 + k] = (y >> ((8 * k) as u128)).resize();
202    }
203    for k in 0..P2_RUN {
204        m[16 + k] = m[k] ^ b8(0xa5);
205        m[32 + k] = m[k] ^ b8(0x3c);
206    }
207    m
208}
209
210/// The probe's logic.
211#[allow(clippy::needless_range_loop, clippy::too_many_lines)]
212#[kernel]
213pub fn ubft_probe_kernel(_clock_reset: ClockReset, input: ProbeIn, q: UbftProbeQ) -> (ProbeOut, UbftProbeD) {
214    let mut s = q.state;
215    let args = input.args;
216    let message = probe_message_kernel(q.state.i);
217    let mut sign = Axis512 {
218        valid: false,
219        data: [b8(0); 64],
220        last: true,
221    };
222    let mut verify = Axis512 {
223        valid: false,
224        data: [b8(0); 64],
225        last: false,
226    };
227    let mut frame_ready = false;
228    let mut beat = FrameBeat {
229        valid: false,
230        data: input.frame.data,
231        last: input.frame.last,
232    };
233    let mut write = RamWrite::<[b8; 64], 5> {
234        addr: q.state.beat,
235        value: input.frame.data,
236        enable: false,
237    };
238    // The row presented next cycle (see the module notes).
239    let mut read_addr = q.state.beat;
240
241    if q.state.phase == IDLE {
242        if args.go && q.state.i < args.count {
243            if q.state.wait == b32(0) {
244                s.phase = SIGN;
245                // `since` counts requests since the last tampered one:
246                // request i is tampered when i mod t == t - 1.
247                let next = q.state.since + b32(1);
248                let tamper = args.tamper_every != b32(0) && next == args.tamper_every;
249                s.tampered = tamper;
250                s.since = if tamper { b32(0) } else { next };
251            } else {
252                s.wait = q.state.wait - b32(1);
253            }
254        }
255    } else if q.state.phase == SIGN {
256        // Profile 1: message 0..16, tag 16..24. Profile 2 (layout v2): tag
257        // 0..8, the 48-byte context 8..56.
258        for k in 0..P1_MSG {
259            sign.data[k] = message[k];
260        }
261        for k in 0..P2_CTX {
262            sign.data[8 + k] = message[k];
263        }
264        for k in 0..4 {
265            sign.data[SIGN_TAG_AT + k] = (q.state.i >> ((8 * k) as u128)).resize();
266        }
267        sign.valid = true;
268        if input.sign_ready {
269            s.phase = FRAME;
270            s.beat = b5(0);
271        }
272    } else if q.state.phase == FRAME {
273        frame_ready = input.writer_ready;
274        beat.valid = input.frame.valid && input.writer_ready;
275        if input.frame.valid && input.writer_ready {
276            write.enable = true;
277            if q.state.beat == LAST_BEAT {
278                s.phase = HEADER;
279                s.beat = b5(0);
280                read_addr = b5(0);
281            } else {
282                s.beat = q.state.beat + b5(1);
283            }
284        }
285    } else if q.state.phase == HEADER {
286        verify.valid = true;
287        verify.data[0] = args.slot.resize();
288        for k in 0..4 {
289            verify.data[4 + k] = (q.state.i >> ((8 * k) as u128)).resize();
290        }
291        read_addr = b5(0);
292        if input.verify_ready {
293            s.phase = BODY;
294        }
295    } else if q.state.phase == BODY {
296        verify.valid = true;
297        verify.data = q.frame;
298        let is_last = q.state.beat == LAST_BEAT;
299        if is_last {
300            // The record has the message where the frame has the tag
301            // (profile 2: its first 22 bytes; the rest follow in TAIL).
302            for k in 0..P1_MSG {
303                verify.data[MESSAGE_AT + k] = message[k];
304            }
305            for k in 0..P2_HEAD {
306                verify.data[MESSAGE_AT + k] = message[k];
307            }
308            for k in ZERO_FROM..64 {
309                verify.data[k] = b8(0);
310            }
311            if q.state.tampered {
312                verify.data[MESSAGE_AT] = message[0] ^ b8(1);
313            }
314        }
315        verify.last = is_last && !P2;
316        if input.verify_ready {
317            if is_last {
318                s.phase = if P2 { TAIL } else { VERDICT };
319            } else {
320                s.beat = q.state.beat + b5(1);
321                read_addr = q.state.beat + b5(1);
322            }
323        }
324    } else if q.state.phase == TAIL {
325        // Profile 2's 27th record beat: message bytes 22..64, then zeros.
326        verify.valid = true;
327        for k in 0..P2_TAIL {
328            verify.data[k] = message[P2_HEAD + k];
329        }
330        verify.last = true;
331        if input.verify_ready {
332            s.phase = VERDICT;
333        }
334    } else {
335        // VERDICT
336        if input.verdict.valid {
337            let word = input.verdict.data;
338            let valid = (word & b64(0xff)) == b64(0x80);
339            let tag_ok = (word >> 32) == q.state.i.resize::<64>();
340            let ok = tag_ok && (valid != q.state.tampered);
341            if ok {
342                s.matched = q.state.matched + b32(1);
343            } else {
344                s.mismatched = q.state.mismatched + b32(1);
345                if q.state.mismatched == b32(0) {
346                    s.first_bad = word;
347                }
348            }
349            s.done = q.state.done + b32(1);
350            s.i = q.state.i + b32(1);
351            s.wait = args.gap;
352            s.phase = IDLE;
353        }
354    }
355
356    let d = UbftProbeD {
357        state: s,
358        frame: RamIn::<[b8; 64], 5> { read_addr, write },
359    };
360    (
361        ProbeOut {
362            sign,
363            frame_ready,
364            verify,
365            verdict_ready: q.state.phase == VERDICT,
366            beat,
367            done: q.state.done,
368            matched: q.state.matched,
369            mismatched: q.state.mismatched,
370            first_bad: q.state.first_bad,
371        },
372        d,
373    )
374}