hsm_usb_protocol/hid_descriptor.rs
1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! HID report descriptor for the mini-HSM.
17//!
18//! The descriptor declares a vendor-defined usage page (`0xFF00`) with one
19//! 128-byte IN report and one 128-byte OUT report. This is the same shape
20//! as a FIDO/U2F device (just with a larger report size), which keeps
21//! host-side support universal across Linux, macOS, and Windows: no
22//! kernel driver, no admin privileges, just `hidapi` or equivalent.
23//!
24//! Byte breakdown:
25//!
26//! ```text
27//! 06 00 FF Usage Page (Vendor Defined 0xFF00)
28//! 09 01 Usage (Vendor Usage 1)
29//! A1 01 Collection (Application)
30//! 09 20 Usage (Vendor Usage 0x20) [for IN reports]
31//! 15 00 Logical Minimum (0)
32//! 26 FF 00 Logical Maximum (255)
33//! 75 08 Report Size (8 bits)
34//! 95 80 Report Count (128)
35//! 81 02 Input (Data, Variable, Absolute)
36//! 09 21 Usage (Vendor Usage 0x21) [for OUT reports]
37//! 15 00 Logical Minimum (0)
38//! 26 FF 00 Logical Maximum (255)
39//! 75 08 Report Size (8 bits)
40//! 95 80 Report Count (128)
41//! 91 02 Output (Data, Variable, Absolute)
42//! C0 End Collection
43//! ```
44//!
45//! Reference: the FIDO U2F HID descriptor in
46//! `https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-hid-protocol-v1.2-ps-20170411.html`,
47//! section "HID Report Descriptor and Device Identification". The U2F spec
48//! uses 64-byte reports; we bump that to 128 so a single ECDSA P-256
49//! signature or public key fits in one report.
50
51/// HID report descriptor, exactly as sent to the host during USB enumeration.
52pub const HID_REPORT_DESCRIPTOR: &[u8] = &[
53 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
54 0x09, 0x01, // Usage (Vendor Usage 1)
55 0xA1, 0x01, // Collection (Application)
56
57 // Input report: token -> host
58 0x09, 0x20, // Usage (Vendor Usage 0x20)
59 0x15, 0x00, // Logical Minimum (0)
60 0x26, 0xFF, 0x00, // Logical Maximum (255)
61 0x75, 0x08, // Report Size (8 bits)
62 0x95, 0x80, // Report Count (128)
63 0x81, 0x02, // Input (Data, Variable, Absolute)
64
65 // Output report: host -> token
66 0x09, 0x21, // Usage (Vendor Usage 0x21)
67 0x15, 0x00, // Logical Minimum (0)
68 0x26, 0xFF, 0x00, // Logical Maximum (255)
69 0x75, 0x08, // Report Size (8 bits)
70 0x95, 0x80, // Report Count (128)
71 0x91, 0x02, // Output (Data, Variable, Absolute)
72
73 0xC0, // End Collection
74];
75
76#[cfg(test)]
77mod tests
78{
79 use super::*;
80
81 #[test]
82 fn descriptor_length_is_34()
83 {
84 // 3 (usage page) + 2 (usage) + 2 (collection)
85 // + 2 * (2 + 2 + 3 + 2 + 2 + 2) per direction (in + out)
86 // + 1 (end collection)
87 // = 7 + 26 + 1 = 34.
88 assert_eq!(HID_REPORT_DESCRIPTOR.len(), 34);
89 }
90
91 #[test]
92 fn descriptor_starts_with_vendor_usage_page_ff00()
93 {
94 // 06 00 FF = Usage Page (Vendor 0xFF00)
95 assert_eq!(&HID_REPORT_DESCRIPTOR[..3], &[0x06, 0x00, 0xFF]);
96 }
97
98 #[test]
99 fn descriptor_ends_with_end_collection()
100 {
101 assert_eq!(*HID_REPORT_DESCRIPTOR.last().unwrap(), 0xC0);
102 }
103
104 #[test]
105 fn descriptor_declares_128_byte_reports()
106 {
107 // Report Count is 0x95 0x80 (= 128). It appears twice (IN and OUT).
108 let mut occurrences = 0;
109 let mut i = 0;
110 while i + 1 < HID_REPORT_DESCRIPTOR.len()
111 {
112 if HID_REPORT_DESCRIPTOR[i] == 0x95 && HID_REPORT_DESCRIPTOR[i + 1] == 0x80
113 {
114 occurrences += 1;
115 }
116 i += 1;
117 }
118 assert_eq!(occurrences, 2);
119 }
120
121 #[test]
122 fn descriptor_declares_8_bit_report_size()
123 {
124 // Report Size is 0x75 0x08 (= 8 bits). It appears twice.
125 let mut occurrences = 0;
126 let mut i = 0;
127 while i + 1 < HID_REPORT_DESCRIPTOR.len()
128 {
129 if HID_REPORT_DESCRIPTOR[i] == 0x75 && HID_REPORT_DESCRIPTOR[i + 1] == 0x08
130 {
131 occurrences += 1;
132 }
133 i += 1;
134 }
135 assert_eq!(occurrences, 2);
136 }
137}