Skip to main content

rhdl_dsig_scalable_core/verify/
mod.rs

1//! The DSig verifier: checks signatures at the rate the signer makes them.
2//!
3#![doc = include_str!("../../docs/verifier.md")]
4//!
5//! # Record layout
6//!
7//! One record = [`RECORD_BYTES`] = 1,664 bytes = 26 beats of 64 bytes.
8//!
9//! | Bytes | Field | Beat |
10//! |---|---|---|
11//! | 0..64 | batch commitment ([`crate::certificate`]) | 0 |
12//! | 64..128 | Ed25519 signature `R ‖ S` | 1 |
13//! | 128..130 | key index, little endian | 2 |
14//! | 130..162 | `pk_hash` | 2 |
15//! | 162..178 | `pk_nonce` | 2 |
16//! | 178..194 | message `nonce` | 2–3 |
17//! | 194..418 | 7 Merkle siblings × 32 bytes, bottom up | 3–6 |
18//! | 418..1642 | 68 chain nodes × 18 bytes | 6–25 |
19//! | 1642..1658 | the 16-byte message | 25 |
20//! | 1658..1664 | zero | 25 |
21//!
22//! The first 1,642 bytes are exactly [`DsigSignature::to_bytes`](crate::reference::DsigSignature::to_bytes).
23//!
24//! # Verdicts
25//!
26//! The verifier writes one byte per record, in record order. Bit 7
27//! ([`VERDICT_WRITTEN`]) is always set, so a zero byte means "never
28//! written". A record is valid exactly when its byte is [`VERDICT_VALID`].
29//!
30//! | Bit | Name | Set when |
31//! |---|---|---|
32//! | 0 | [`VERDICT_FORMAT`] | commitment header bytes 0..16 are not `DSIGFP01`, profile 1, batch 128, or the key index is 128 or more |
33//! | 1 | [`VERDICT_MERKLE`] | the Merkle climb from `pk_hash` (with the index's low 7 bits) misses the commitment's root |
34//! | 2 | [`VERDICT_CHAINS`] | the commitment of the 68 chain ends is not `pk_hash` |
35//! | 3 | [`VERDICT_CERT`] | strict Ed25519 verification of the certificate fails |
36//! | 7 | [`VERDICT_WRITTEN`] | always |
37//!
38//! [`expected_verdict`] computes the same byte in software. The card run
39//! checks every byte against it, and [`expected_verdict`] itself is tested
40//! against [`DsigSignature::verify`](crate::reference::DsigSignature::verify): valid ⇔ the reference
41//! accepts.
42//!
43//! # Neighbours
44//!
45//! * [`crate::reference`] is the scheme; [`expected_verdict`] is built on it.
46//! * [`crate::chain_lane`] and [`crate::blake3_core`] are the hash lanes the
47//!   unit reuses.
48//! * The planner's `DsigVerify` component (feature `planner`) prices this
49//!   block for the chip planner and TUI.
50
51pub mod certs;
52pub mod chains;
53pub mod datapath;
54pub mod engine;
55pub mod hasher;
56pub mod unit;
57
58use ed25519_dalek::VerifyingKey;
59
60use crate::certificate::{DOMAIN, PROFILE_ID};
61use crate::reference::{
62    DsigSignature, SIGNATURE_WIRE_BYTES, WotsSignature, root_from_proof,
63};
64use crate::{BATCH_KEYS, MERKLE_HEIGHT};
65
66/// Bytes of one verifier input record: the wire signature, the message and
67/// zero bytes up to a whole beat (profile 1: 16-byte message, 1,664 bytes,
68/// 26 beats; profile 2: 64-byte message, 1,728 bytes, 27 beats).
69pub const RECORD_BYTES: usize = (SIGNATURE_WIRE_BYTES + MESSAGE_BYTES).div_ceil(64) * 64;
70/// HBM beats per record (64 bytes each).
71pub const RECORD_BEATS: usize = RECORD_BYTES / 64;
72/// Offset of the message inside a record (right after the signature).
73pub const MESSAGE_OFFSET: usize = SIGNATURE_WIRE_BYTES;
74/// Bytes of the signed message (`DSIG_MESSAGE_BYTES`; see [`PROFILE_ID`]).
75pub const MESSAGE_BYTES: usize = crate::MESSAGE_BYTES;
76
77const _: () = assert!(RECORD_BEATS * 64 == RECORD_BYTES);
78const _: () = assert!(MESSAGE_OFFSET + MESSAGE_BYTES <= RECORD_BYTES);
79
80/// Verdict bit 0: malformed commitment header or key index ≥ 128.
81pub const VERDICT_FORMAT: u8 = 1;
82/// Verdict bit 1: the Merkle path does not lead to the commitment's root.
83pub const VERDICT_MERKLE: u8 = 2;
84/// Verdict bit 2: the chain ends do not commit to `pk_hash`.
85pub const VERDICT_CHAINS: u8 = 4;
86/// Verdict bit 3: the Ed25519 certificate does not verify.
87pub const VERDICT_CERT: u8 = 8;
88/// Verdict bit 7: set on every written verdict.
89pub const VERDICT_WRITTEN: u8 = 0x80;
90/// The byte of a valid signature.
91pub const VERDICT_VALID: u8 = VERDICT_WRITTEN;
92
93/// The 16 header bytes every well-formed commitment starts with:
94/// `DSIGFP01`, profile 1 and batch size 128, little endian.
95#[must_use]
96pub fn commitment_header() -> [u8; 16] {
97    let mut header = [0; 16];
98    header[..8].copy_from_slice(&DOMAIN);
99    header[8..12].copy_from_slice(&PROFILE_ID.to_le_bytes());
100    header[12..16].copy_from_slice(&(BATCH_KEYS as u32).to_le_bytes());
101    header
102}
103
104/// Builds one input record from a wire signature and its message.
105#[must_use]
106pub fn record(signature: &[u8; SIGNATURE_WIRE_BYTES], message: &[u8; MESSAGE_BYTES]) -> [u8; RECORD_BYTES] {
107    let mut bytes = [0; RECORD_BYTES];
108    bytes[..SIGNATURE_WIRE_BYTES].copy_from_slice(signature);
109    bytes[MESSAGE_OFFSET..MESSAGE_OFFSET + MESSAGE_BYTES].copy_from_slice(message);
110    bytes
111}
112
113/// The verdict byte the hardware must write for `record`, given the trusted
114/// Ed25519 key.
115///
116/// Each check runs on the raw bytes, independently of the others, exactly
117/// as the hardware does: a bad key index still gets its Merkle and chain
118/// checks (with the index's low 7 bits), and a bad header still gets its
119/// certificate checked. Only the combination matters for validity, but the
120/// byte pins down every bit, so the card run is checked bit for bit.
121#[must_use]
122pub fn expected_verdict(record: &[u8; RECORD_BYTES], trusted: &VerifyingKey) -> u8 {
123    let mut verdict = VERDICT_WRITTEN;
124    // Format: the header and the key index.
125    let index = u16::from_le_bytes([record[128], record[129]]);
126    if record[..16] != commitment_header() || usize::from(index) >= BATCH_KEYS {
127        verdict |= VERDICT_FORMAT;
128    }
129    // Merkle: climb from pk_hash with the index's low 7 bits.
130    let pk_hash: [u8; 32] = record[130..162].try_into().expect("32 bytes");
131    let mut proof = [[0; 32]; MERKLE_HEIGHT];
132    for (i, sibling) in proof.iter_mut().enumerate() {
133        sibling.copy_from_slice(&record[194 + 32 * i..226 + 32 * i]);
134    }
135    let root = root_from_proof(pk_hash, usize::from(index) & (BATCH_KEYS - 1), &proof).expect("index < 128");
136    if root[..] != record[32..64] {
137        verdict |= VERDICT_MERKLE;
138    }
139    // Chains: walk every node to its end and commit.
140    let mut elements = [[0; crate::ELEMENT_BYTES]; crate::CHAINS];
141    for (i, element) in elements.iter_mut().enumerate() {
142        element.copy_from_slice(&record[418 + 18 * i..436 + 18 * i]);
143    }
144    let wots = WotsSignature {
145        public: crate::reference::KeyPublic {
146            pk_hash,
147            pk_nonce: record[162..178].try_into().expect("16 bytes"),
148        },
149        nonce: record[178..194].try_into().expect("16 bytes"),
150        elements,
151    };
152    if !wots.verify_hash_component(&record[MESSAGE_OFFSET..MESSAGE_OFFSET + MESSAGE_BYTES]) {
153        verdict |= VERDICT_CHAINS;
154    }
155    // Certificate: strict Ed25519 over the 64 commitment bytes as they are.
156    let signature = ed25519_dalek::Signature::from_bytes(record[64..128].try_into().expect("64 bytes"));
157    if trusted.verify_strict(&record[..64], &signature).is_err() {
158        verdict |= VERDICT_CERT;
159    }
160    verdict
161}
162
163/// Whether the reference verifier accepts the record's signature for its
164/// message. Used to check that [`expected_verdict`] means the same thing.
165#[must_use]
166pub fn reference_accepts(record: &[u8; RECORD_BYTES], trusted: &VerifyingKey) -> bool {
167    DsigSignature::from_bytes(&record[..SIGNATURE_WIRE_BYTES])
168        .is_some_and(|signature| signature.verify(&record[MESSAGE_OFFSET..MESSAGE_OFFSET + MESSAGE_BYTES], trusted))
169}
170
171/// Checks the oracle against the reference on valid and tampered records.
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::reference::PreparedBatch;
176    use ed25519_dalek::SigningKey;
177
178    /// One batch of real signatures, each tampered in every field class.
179    #[test]
180    fn oracle_matches_reference_on_valid_and_tampered_records() {
181        let signer = SigningKey::from_bytes(&[0x11; 32]);
182        let trusted = signer.verifying_key();
183        let mut batch = PreparedBatch::new([0x42; 32], 7, 9, &signer);
184        let mut checked = 0;
185        for index in 0..8_usize {
186            let message = [index as u8; MESSAGE_BYTES];
187            let signature = batch.sign(index, &message).expect("fresh key");
188            let good = record(&signature.to_bytes(), &message);
189            assert_eq!(expected_verdict(&good, &trusted), VERDICT_VALID);
190            assert!(reference_accepts(&good, &trusted));
191            // Flip one bit in each region: header, root, Ed25519, index,
192            // pk_hash, pk_nonce, nonce, a sibling, a chain node, the message.
193            for (offset, bit) in [
194                (3, VERDICT_FORMAT | VERDICT_CERT),
195                (40, VERDICT_MERKLE | VERDICT_CERT),
196                (100, VERDICT_CERT),
197                (129, VERDICT_FORMAT),
198                (140, 0),
199                (170, VERDICT_CHAINS),
200                (180, VERDICT_CHAINS),
201                (300, VERDICT_MERKLE),
202                (1000, VERDICT_CHAINS),
203                (1650, VERDICT_CHAINS),
204            ] {
205                let mut bad = good;
206                bad[offset] ^= 0x40;
207                let verdict = expected_verdict(&bad, &trusted);
208                assert_ne!(verdict, VERDICT_VALID, "offset {offset}");
209                assert!(!reference_accepts(&bad, &trusted), "offset {offset}");
210                if bit != 0 {
211                    assert_eq!(verdict & bit, bit, "offset {offset}: {verdict:#04x}");
212                }
213                checked += 1;
214            }
215        }
216        assert_eq!(checked, 80);
217    }
218}