hsm_usb_protocol/responses.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//! Status bytes returned by the token in every response report.
17//!
18//! Wire format mirrors [`crate::commands`]: a single 128-byte HID report
19//! whose opcode is one of these [`ResponseStatus`] values and whose
20//! payload depends on the originating command.
21
22/// First byte of every HID response.
23#[repr(u8)]
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[cfg_attr(feature = "defmt", derive(defmt::Format))]
26pub enum ResponseStatus
27{
28 /// `0x00` - Operation succeeded. Payload depends on the originating
29 /// command (e.g. 64-byte pubkey for `GetPubkey`, 64-byte signature for
30 /// `Sign`, empty for `VerifyPin`).
31 Ok = 0x00,
32 /// `0x01` - Command opcode unknown.
33 InvalidCommand = 0x01,
34 /// `0x02` - Payload size or shape is invalid.
35 InvalidPayload = 0x02,
36 /// `0x03` - Slot index out of range.
37 InvalidSlot = 0x03,
38 /// `0x04` - I2C / wake / framing error talking to the ATECC.
39 /// `payload[0]` is a one-byte sub-code identifying the precise
40 /// failure category (HAL nack, wake-failed, CRC mismatch, timeout,
41 /// malformed response, buffer-too-small, self-test failure). The
42 /// canonical encoding matches `atecc608b::AteccErrorKind::as_sub_code`.
43 AteccCommunicationError = 0x04,
44 /// `0x05` - Chip returned an error status. The chip's raw status byte
45 /// is in `payload[0]`. Decode via
46 /// `atecc608b::ChipError::from_status_byte` to get the symbolic
47 /// variant (`ParseError`, `ExecutionError`, etc.).
48 AteccChipError = 0x05,
49 /// `0x06` - The user did not press the button within the 30 s window.
50 TouchTimeout = 0x06,
51 /// `0x07` - Token has not been provisioned yet.
52 NotProvisioned = 0x07,
53 /// `0x08` - Magic word for a `Lock*` command did not match.
54 LockMagicMismatch = 0x08,
55 /// `0x09` - CRC of the expected config does not match what's on chip.
56 LockCrcMismatch = 0x09,
57 /// `0x0A` - Another operation is in progress.
58 Busy = 0x0A,
59 /// `0x0B` - PIN was wrong. Tries remaining in `payload[0]`.
60 WrongPin = 0x0B,
61 /// `0x0C` - A PIN session is required before signing.
62 PinRequired = 0x0C,
63 /// `0x0D` - PIN slot is blocked. Only PUK unblock can recover.
64 PinBlocked = 0x0D,
65 /// `0x0E` - PUK was wrong. Tries remaining in `payload[0]`.
66 WrongPuk = 0x0E,
67 /// `0x0F` - PUK retries exhausted. Chip is bricked.
68 Bricked = 0x0F,
69 /// `0x10` - `EmergencyReset` was requested but the user still has
70 /// PIN or PUK attempts remaining. Payload is 2 bytes:
71 /// `[pin_tries_remaining, puk_tries_remaining]`.
72 EmergencyResetNotPermitted = 0x10,
73}
74
75impl ResponseStatus
76{
77 /// Map a raw byte to a [`ResponseStatus`], if recognized.
78 #[must_use]
79 pub const fn from_byte(byte: u8) -> Option<Self>
80 {
81 match byte
82 {
83 0x00 => Some(Self::Ok),
84 0x01 => Some(Self::InvalidCommand),
85 0x02 => Some(Self::InvalidPayload),
86 0x03 => Some(Self::InvalidSlot),
87 0x04 => Some(Self::AteccCommunicationError),
88 0x05 => Some(Self::AteccChipError),
89 0x06 => Some(Self::TouchTimeout),
90 0x07 => Some(Self::NotProvisioned),
91 0x08 => Some(Self::LockMagicMismatch),
92 0x09 => Some(Self::LockCrcMismatch),
93 0x0A => Some(Self::Busy),
94 0x0B => Some(Self::WrongPin),
95 0x0C => Some(Self::PinRequired),
96 0x0D => Some(Self::PinBlocked),
97 0x0E => Some(Self::WrongPuk),
98 0x0F => Some(Self::Bricked),
99 0x10 => Some(Self::EmergencyResetNotPermitted),
100 _ => None,
101 }
102 }
103
104 /// The raw status byte that goes on the wire.
105 #[must_use]
106 pub const fn as_u8(self) -> u8
107 {
108 self as u8
109 }
110}
111
112impl TryFrom<u8> for ResponseStatus
113{
114 type Error = UnknownStatus;
115
116 fn try_from(byte: u8) -> Result<Self, Self::Error>
117 {
118 Self::from_byte(byte).ok_or(UnknownStatus { byte })
119 }
120}
121
122/// Returned when a byte cannot be mapped to a known [`ResponseStatus`].
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124#[cfg_attr(feature = "defmt", derive(defmt::Format))]
125pub struct UnknownStatus
126{
127 /// The raw byte that was not recognized.
128 pub(crate) byte: u8,
129}
130
131#[cfg(test)]
132mod tests
133{
134 use super::*;
135
136 #[test]
137 fn from_byte_round_trips()
138 {
139 for status in [
140 ResponseStatus::Ok,
141 ResponseStatus::InvalidCommand,
142 ResponseStatus::InvalidPayload,
143 ResponseStatus::InvalidSlot,
144 ResponseStatus::AteccCommunicationError,
145 ResponseStatus::AteccChipError,
146 ResponseStatus::TouchTimeout,
147 ResponseStatus::NotProvisioned,
148 ResponseStatus::LockMagicMismatch,
149 ResponseStatus::LockCrcMismatch,
150 ResponseStatus::Busy,
151 ResponseStatus::WrongPin,
152 ResponseStatus::PinRequired,
153 ResponseStatus::PinBlocked,
154 ResponseStatus::WrongPuk,
155 ResponseStatus::Bricked,
156 ResponseStatus::EmergencyResetNotPermitted,
157 ]
158 {
159 assert_eq!(ResponseStatus::from_byte(status.as_u8()), Some(status));
160 }
161 }
162
163 #[test]
164 fn try_from_returns_error_with_byte()
165 {
166 let err = ResponseStatus::try_from(0x99u8).unwrap_err();
167 assert_eq!(err.byte, 0x99);
168 }
169
170 #[test]
171 fn from_byte_returns_none_for_unknown()
172 {
173 assert!(ResponseStatus::from_byte(0x11).is_none());
174 assert!(ResponseStatus::from_byte(0x42).is_none());
175 assert!(ResponseStatus::from_byte(0x99).is_none());
176 assert!(ResponseStatus::from_byte(0xFF).is_none());
177 }
178}