Skip to main content

wots_rhdl/verify/
framing.rs

1//! Canonical 35-beat input framing for profile-separated WOTS verification.
2//!
3//! The byte stream is `signature || public_seed || public_key_hash || message`.
4//! At 2,240 bytes it occupies exactly thirty-five 512-bit transfers. Every byte
5//! lane is valid and `last` is asserted only on beat 34. Serialized values do
6//! not carry a profile tag; callers must select the profile through an
7//! authenticated, compile-time-separated top.
8
9use core::{fmt, marker::PhantomData};
10
11use hashsigs_types::{
12    CHAIN_COUNT, HASH_BYTES, MESSAGE_BYTES, MessageDigest, Profile, PublicKey, SIGNATURE_BYTES,
13    Signature,
14};
15use rhdl::prelude::*;
16
17use crate::blocks::HashBytes;
18
19/// Bytes transferred by one verifier input beat.
20pub const VERIFY_BEAT_BYTES: usize = 64;
21/// Exact verifier input-frame size in bytes.
22pub const VERIFY_INPUT_BYTES: usize = SIGNATURE_BYTES + 2 * HASH_BYTES + MESSAGE_BYTES;
23/// Exact verifier input-frame length in 512-bit beats.
24pub const VERIFY_INPUT_BEATS: usize = VERIFY_INPUT_BYTES / VERIFY_BEAT_BYTES;
25/// Canonical keep mask for every verifier input beat.
26pub const VERIFY_FULL_KEEP: u64 = u64::MAX;
27
28const _: () = {
29    assert!(SIGNATURE_BYTES == 2_144);
30    assert!(VERIFY_INPUT_BYTES == 2_240);
31    assert!(VERIFY_INPUT_BYTES.is_multiple_of(VERIFY_BEAT_BYTES));
32    assert!(VERIFY_INPUT_BEATS == 35);
33};
34
35/// One host-side verifier input beat.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct VerifyFrameBeat {
38    /// Sixty-four bytes in ascending stream order.
39    pub data: [u8; VERIFY_BEAT_BYTES],
40    /// One validity bit per byte lane; canonical input always uses all ones.
41    pub keep: u64,
42    /// End-of-frame marker; canonical input asserts this only on beat 34.
43    pub last: bool,
44    /// Opaque external identifier, identical on every beat of one frame.
45    pub job_id: u32,
46}
47
48impl Default for VerifyFrameBeat {
49    fn default() -> Self {
50        Self {
51            data: [0; VERIFY_BEAT_BYTES],
52            keep: 0,
53            last: false,
54            job_id: 0,
55        }
56    }
57}
58
59/// Raw verifier-frame storage carrying a caller-selected profile type `P`.
60///
61/// The stored beats are not guaranteed to be canonical or to contain a
62/// signature and public key generated under `P`. No profile tag appears on the
63/// wire; the type parameter only records the caller's selection. Treat this
64/// value as untrusted until [`unpack_verify_input`] succeeds. The caller must
65/// authenticate the profile selection outside this frame, and successful
66/// unpacking validates framing rather than the signature itself.
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct VerifyFrame<P: Profile> {
69    beats: [VerifyFrameBeat; VERIFY_INPUT_BEATS],
70    profile: PhantomData<fn() -> P>,
71}
72
73impl<P: Profile> VerifyFrame<P> {
74    /// Stores raw beats under an explicitly selected profile without validation.
75    ///
76    /// This does not prove that the payload uses `P`; it only attaches the
77    /// caller-selected type marker. [`unpack_verify_input`] validates framing
78    /// before returning protocol values carrying that selected type; signature
79    /// verification remains a separate operation.
80    #[must_use]
81    pub const fn from_beats(beats: [VerifyFrameBeat; VERIFY_INPUT_BEATS]) -> Self {
82        Self {
83            beats,
84            profile: PhantomData,
85        }
86    }
87
88    /// Borrows all thirty-five stored beats, which may still be noncanonical.
89    #[must_use]
90    pub const fn beats(&self) -> &[VerifyFrameBeat; VERIFY_INPUT_BEATS] {
91        &self.beats
92    }
93
94    /// Releases all thirty-five unvalidated, caller-profile-marked beats.
95    #[must_use]
96    pub const fn into_beats(self) -> [VerifyFrameBeat; VERIFY_INPUT_BEATS] {
97        self.beats
98    }
99}
100
101/// Canonical verifier-frame validation failures.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum VerifyFrameError {
104    /// A beat did not mark all sixty-four lanes valid.
105    Keep {
106        /// Zero-based beat index.
107        beat: usize,
108        /// Observed keep mask.
109        actual: u64,
110    },
111    /// `last` was asserted early or omitted from beat 34.
112    Last {
113        /// Zero-based beat index.
114        beat: usize,
115        /// Observed marker.
116        actual: bool,
117    },
118    /// A beat changed the job identifier captured on beat zero.
119    JobId {
120        /// Zero-based beat index.
121        beat: usize,
122        /// Identifier captured on beat zero.
123        expected: u32,
124        /// Identifier carried by this beat.
125        actual: u32,
126    },
127}
128
129impl fmt::Display for VerifyFrameError {
130    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match *self {
132            Self::Keep { beat, actual } => {
133                write!(
134                    formatter,
135                    "beat {beat} has noncanonical keep {actual:#018x}"
136                )
137            }
138            Self::Last { beat, actual } => {
139                write!(formatter, "beat {beat} has noncanonical last={actual}")
140            }
141            Self::JobId {
142                beat,
143                expected,
144                actual,
145            } => write!(
146                formatter,
147                "beat {beat} changed job ID from {expected:#010x} to {actual:#010x}"
148            ),
149        }
150    }
151}
152
153impl std::error::Error for VerifyFrameError {}
154
155/// Pack one exact signature, public key, and already-hashed message.
156#[must_use]
157pub fn pack_verify_input<P: Profile>(
158    signature: &Signature<P>,
159    public_key: &PublicKey<P>,
160    message: &MessageDigest,
161    job_id: u32,
162) -> VerifyFrame<P> {
163    let signature = signature.to_bytes();
164    let public_key = public_key.to_bytes();
165    let message = message.as_bytes();
166    let mut stream = [0_u8; VERIFY_INPUT_BYTES];
167    stream[..SIGNATURE_BYTES].copy_from_slice(&signature);
168    stream[SIGNATURE_BYTES..SIGNATURE_BYTES + 2 * HASH_BYTES].copy_from_slice(&public_key);
169    stream[SIGNATURE_BYTES + 2 * HASH_BYTES..].copy_from_slice(message);
170
171    let beats = core::array::from_fn(|beat| {
172        let offset = beat * VERIFY_BEAT_BYTES;
173        let mut data = [0_u8; VERIFY_BEAT_BYTES];
174        data.copy_from_slice(&stream[offset..offset + VERIFY_BEAT_BYTES]);
175        VerifyFrameBeat {
176            data,
177            keep: VERIFY_FULL_KEEP,
178            last: beat + 1 == VERIFY_INPUT_BEATS,
179            job_id,
180        }
181    });
182    VerifyFrame::from_beats(beats)
183}
184
185/// Validate and unpack a verifier input frame.
186///
187/// # Errors
188///
189/// Returns the first noncanonical keep, last, or job-ID field.
190///
191/// # Panics
192///
193/// The fixed-size conversions are statically guaranteed by the framing
194/// constants. A panic therefore indicates an internal invariant violation.
195pub fn unpack_verify_input<P: Profile>(
196    frame: &VerifyFrame<P>,
197) -> Result<(Signature<P>, PublicKey<P>, MessageDigest, u32), VerifyFrameError> {
198    let beats = frame.beats();
199    let job_id = beats[0].job_id;
200    let mut stream = [0_u8; VERIFY_INPUT_BYTES];
201    for (beat_index, beat) in beats.iter().enumerate() {
202        if beat.keep != VERIFY_FULL_KEEP {
203            return Err(VerifyFrameError::Keep {
204                beat: beat_index,
205                actual: beat.keep,
206            });
207        }
208        let expected_last = beat_index + 1 == VERIFY_INPUT_BEATS;
209        if beat.last != expected_last {
210            return Err(VerifyFrameError::Last {
211                beat: beat_index,
212                actual: beat.last,
213            });
214        }
215        if beat.job_id != job_id {
216            return Err(VerifyFrameError::JobId {
217                beat: beat_index,
218                expected: job_id,
219                actual: beat.job_id,
220            });
221        }
222        let offset = beat_index * VERIFY_BEAT_BYTES;
223        stream[offset..offset + VERIFY_BEAT_BYTES].copy_from_slice(&beat.data);
224    }
225
226    let signature = Signature::from_slice(&stream[..SIGNATURE_BYTES])
227        .expect("the fixed verifier frame contains exactly 2,144 signature bytes");
228    let public_key = PublicKey::from_slice(&stream[SIGNATURE_BYTES..SIGNATURE_BYTES + 64])
229        .expect("the fixed verifier frame contains exactly 64 public-key bytes");
230    let message = MessageDigest::from_slice(&stream[SIGNATURE_BYTES + 64..])
231        .expect("the fixed verifier frame contains exactly 32 message bytes");
232    Ok((signature, public_key, message, job_id))
233}
234
235/// One RHDL-visible 512-bit verifier transfer.
236#[derive(Clone, Copy, Debug, Digital, Eq, PartialEq)]
237pub struct VerifyStreamBeat {
238    /// Sixty-four ascending byte lanes.
239    pub data: [b8; VERIFY_BEAT_BYTES],
240    /// One validity bit per byte lane.
241    pub keep: b64,
242    /// End-of-frame marker.
243    pub last: bool,
244    /// Opaque external job identifier.
245    pub job_id: b32,
246}
247
248impl Default for VerifyStreamBeat {
249    fn default() -> Self {
250        Self {
251            data: [b8(0); VERIFY_BEAT_BYTES],
252            keep: b64(0),
253            last: false,
254            job_id: b32(0),
255        }
256    }
257}
258
259/// Two canonical 32-byte hash values carried by one 512-bit beat.
260#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
261pub struct VerifyBeatHalves {
262    /// Low-addressed bytes `0..31`.
263    pub first: HashBytes,
264    /// High-addressed bytes `32..63`.
265    pub second: HashBytes,
266}
267
268/// Split a verifier beat without changing byte order.
269#[kernel]
270pub fn verify_beat_halves_kernel(data: [b8; VERIFY_BEAT_BYTES]) -> VerifyBeatHalves {
271    let mut first = [b8(0); HASH_BYTES];
272    let mut second = [b8(0); HASH_BYTES];
273    for index in 0..HASH_BYTES {
274        first[index] = data[index];
275        second[index] = data[index + HASH_BYTES];
276    }
277    VerifyBeatHalves { first, second }
278}
279
280/// Compare two 256-bit values without data-dependent early exit.
281#[allow(clippy::assign_op_pattern)]
282#[kernel]
283pub fn verify_hash_equal_kernel(left: HashBytes, right: HashBytes) -> bool {
284    let mut difference = b8(0);
285    for index in 0..HASH_BYTES {
286        difference = difference | (left[index] ^ right[index]);
287    }
288    difference == b8(0)
289}
290
291const _: () = {
292    assert!(CHAIN_COUNT == 67);
293};