Skip to main content

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}