Skip to main content

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}