passkey_types/u2f/
commands.rs

1use super::{ResponseStatusWords, authenticate::AuthenticationRequest, register::RegisterRequest};
2
3/// U2F command, determined at the INS position,
4///
5/// Anything between the values `0x40-0xbf` are vendor specific and therefore unsupported.
6///
7/// This Enum does not `#[repr(u8)]` with discriminant values since we cannot currently have tuple
8/// variants in those enums until <https://github.com/rust-lang/rust/issues/60553> stabilizes.
9#[derive(Debug)]
10pub enum Command {
11    /// Value of `0x01` with parameters of `P1 = 0x00`, `P2 = 0x00`
12    Register,
13    /// Value of `0x02` with parameters of `P1 = 0x03|0x07|0x08`, `P2 = 0x00`
14    Authenticate,
15    /// Value of `0x03` with parameters of `P1 = 0x00`, `P2 = 0x00`
16    Version,
17    /// Unsupported command value
18    Unsuported(u8),
19}
20
21impl From<Command> for u8 {
22    fn from(src: Command) -> Self {
23        match src {
24            Command::Register => 0x01,
25            Command::Authenticate => 0x02,
26            Command::Version => 0x03,
27            Command::Unsuported(cmd) => cmd,
28        }
29    }
30}
31
32impl From<u8> for Command {
33    fn from(src: u8) -> Self {
34        match src {
35            0x01 => Command::Register,
36            0x02 => Command::Authenticate,
37            0x03 => Command::Version,
38            cmd => Command::Unsuported(cmd),
39        }
40    }
41}
42
43/// Data payload of a U2F Request
44#[derive(Debug)]
45pub enum RequestPayload {
46    /// Register command payload
47    Register(RegisterRequest),
48    /// Authentication command payload
49    Authenticate(AuthenticationRequest),
50    /// Version command payload
51    Version,
52}
53
54/// U2F request frame
55#[derive(Debug)]
56pub struct Request {
57    /// Must be of value 0
58    pub cla: u8,
59    /// Command byte
60    pub ins: Command,
61    /// Parameter byte, only used during authentication
62    pub p1: u8,
63    /// Length of the data payload
64    pub data_len: usize,
65    /// Data payload
66    pub data: RequestPayload,
67}
68
69const REQUEST_HEADER_LEN: usize = 6;
70
71impl TryFrom<&[u8]> for Request {
72    type Error = ResponseStatusWords;
73
74    #[expect(clippy::as_conversions)]
75    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
76        if value.len() < REQUEST_HEADER_LEN {
77            return Err(ResponseStatusWords::WrongLength);
78        }
79
80        let cla = value[0];
81        if cla != 0 {
82            return Err(ResponseStatusWords::WrongData);
83        }
84        let ins = Command::from(value[1]);
85        let p1 = value[2];
86        let data_start = REQUEST_HEADER_LEN + 1;
87        // SAFETY: This unwrap is safe since 3..7 gives 4 bytes which is a safe conversion to an
88        // array of len 4. Technically the first of these bytes is `p2` the second parameter,
89        // but in the base U2F spec this will always be 0. So this length is safe.
90        let data_len = u32::from_be_bytes(value[3..data_start].try_into().unwrap()) as usize;
91        let data_end = data_start + data_len;
92        let payload = &value[data_start..data_end];
93
94        let data = match ins {
95            Command::Register => RequestPayload::Register(
96                payload
97                    .try_into()
98                    // Wrong length because it must be two SHA256's which are 32 bytes each
99                    .map_err(|_| ResponseStatusWords::WrongLength)?,
100            ),
101            Command::Authenticate => RequestPayload::Authenticate(
102                AuthenticationRequest::try_from(payload, p1)
103                    .map_err(|_| ResponseStatusWords::WrongLength)?,
104            ),
105            Command::Version => RequestPayload::Version,
106            Command::Unsuported(_) => return Err(ResponseStatusWords::InsNotSupported),
107        };
108
109        Ok(Request {
110            cla,
111            ins,
112            p1,
113            data_len,
114            data,
115        })
116    }
117}