ctap_types/
ctap1.rs

1//! Types for CTAP1.
2//!
3//! Note that all ctap1::Authenticators automatically implement RPC with [`Request`] and
4//! [`Response`].
5use crate::Bytes;
6
7pub const NO_ERROR: u16 = 0x9000;
8
9/// Re-export of the iso7816::Status.
10pub use iso7816::Status as Error;
11
12pub mod authenticate {
13    use super::{Bytes, ControlByte};
14
15    #[derive(Clone, Debug, Eq, PartialEq)]
16    pub struct Request<'a> {
17        pub control_byte: ControlByte,
18        pub challenge: &'a [u8; 32],
19        pub app_id: &'a [u8; 32],
20        pub key_handle: &'a [u8],
21    }
22
23    #[derive(Clone, Debug, Eq, PartialEq)]
24    pub struct Response {
25        pub user_presence: u8,
26        pub count: u32,
27        pub signature: Bytes<72>,
28    }
29}
30
31pub mod register {
32    use super::Bytes;
33
34    #[derive(Clone, Debug, Eq, PartialEq)]
35    pub struct Request<'a> {
36        pub challenge: &'a [u8; 32],
37        pub app_id: &'a [u8; 32],
38    }
39
40    #[derive(Clone, Debug, Eq, PartialEq)]
41    pub struct Response {
42        pub header_byte: u8,
43        pub public_key: Bytes<65>,
44        pub key_handle: Bytes<255>,
45        pub attestation_certificate: Bytes<1024>,
46        pub signature: Bytes<72>,
47    }
48
49    impl Response {
50        pub fn new(
51            header_byte: u8,
52            public_key: &cosey::EcdhEsHkdf256PublicKey,
53            key_handle: Bytes<255>,
54            signature: Bytes<72>,
55            attestation_certificate: Bytes<1024>,
56        ) -> Self {
57            let mut public_key_bytes = Bytes::new();
58            public_key_bytes.push(0x04).unwrap();
59            public_key_bytes.extend_from_slice(&public_key.x).unwrap();
60            public_key_bytes.extend_from_slice(&public_key.y).unwrap();
61
62            Self {
63                header_byte,
64                public_key: public_key_bytes,
65                key_handle,
66                attestation_certificate,
67                signature,
68            }
69        }
70    }
71}
72
73#[repr(u8)]
74#[derive(Copy, Clone, Debug, Eq, PartialEq)]
75#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
76pub enum ControlByte {
77    // Conor:
78    // I think U2F check-only maps to FIDO2 MakeCredential with the credID in the excludeList,
79    // and pinAuth="" so the request will fail before UP check.
80    // I  think this is what the windows hello API does to silently check if a credential is
81    // on an authenticator
82    CheckOnly = 0x07,
83    EnforceUserPresenceAndSign = 0x03,
84    DontEnforceUserPresenceAndSign = 0x08,
85}
86
87impl TryFrom<u8> for ControlByte {
88    type Error = Error;
89
90    fn try_from(byte: u8) -> Result<ControlByte> {
91        match byte {
92            0x07 => Ok(ControlByte::CheckOnly),
93            0x03 => Ok(ControlByte::EnforceUserPresenceAndSign),
94            0x08 => Ok(ControlByte::DontEnforceUserPresenceAndSign),
95            _ => Err(Error::IncorrectDataParameter),
96        }
97    }
98}
99
100pub type Result<T> = core::result::Result<T, Error>;
101
102/// Type alias for convenience.
103pub type Register<'a> = register::Request<'a>;
104/// Type alias for convenience.
105pub type Authenticate<'a> = authenticate::Request<'a>;
106
107/// Type alias for convenience.
108pub type RegisterResponse = register::Response;
109/// Type alias for convenience.
110pub type AuthenticateResponse = authenticate::Response;
111
112#[derive(Clone, Debug, Eq, PartialEq)]
113#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
114#[allow(clippy::large_enum_variant)]
115/// Enum of all CTAP1 requests.
116pub enum Request<'a> {
117    Register(register::Request<'a>),
118    Authenticate(authenticate::Request<'a>),
119    Version,
120}
121
122#[derive(Clone, Debug, Eq, PartialEq)]
123#[allow(clippy::large_enum_variant)]
124/// Enum of all CTAP1 responses.
125pub enum Response {
126    Register(register::Response),
127    Authenticate(authenticate::Response),
128    Version([u8; 6]),
129}
130
131impl Response {
132    #[allow(clippy::result_unit_err)]
133    #[inline(never)]
134    pub fn serialize<const S: usize>(
135        &self,
136        buf: &mut iso7816::Data<S>,
137    ) -> core::result::Result<(), ()> {
138        match self {
139            Response::Register(reg) => {
140                buf.push(reg.header_byte).map_err(drop)?;
141                buf.extend_from_slice(&reg.public_key)?;
142                buf.push(reg.key_handle.len() as u8).map_err(drop)?;
143                buf.extend_from_slice(&reg.key_handle)?;
144                buf.extend_from_slice(&reg.attestation_certificate)?;
145                buf.extend_from_slice(&reg.signature)
146            }
147            Response::Authenticate(auth) => {
148                buf.push(auth.user_presence).map_err(drop)?;
149                buf.extend_from_slice(&auth.count.to_be_bytes())?;
150                buf.extend_from_slice(&auth.signature)
151            }
152            Response::Version(version) => buf.extend_from_slice(version),
153        }
154    }
155}
156
157impl<'a, const S: usize> TryFrom<&'a iso7816::Command<S>> for Request<'a> {
158    type Error = Error;
159    fn try_from(apdu: &'a iso7816::Command<S>) -> Result<Request<'a>> {
160        apdu.as_view().try_into()
161    }
162}
163
164impl<'a> TryFrom<iso7816::command::CommandView<'a>> for Request<'a> {
165    type Error = Error;
166    #[inline(never)]
167    fn try_from(apdu: iso7816::command::CommandView<'a>) -> Result<Request<'a>> {
168        let cla = apdu.class().into_inner();
169        let ins = match apdu.instruction() {
170            iso7816::Instruction::Unknown(ins) => ins,
171            _ins => 0,
172        };
173        let p1 = apdu.p1;
174        let _p2 = apdu.p2;
175
176        if cla != 0 {
177            return Err(Error::ClassNotSupported);
178        }
179
180        if ins == 0x3 {
181            // for some weird historical reason, [0, 3, 0, 0, 0, 0, 0, 0, 0]
182            // is valid to send here.
183            return Ok(Request::Version);
184        };
185
186        let request = apdu.data();
187
188        match ins {
189            // register
190            0x1 => {
191                if request.len() != 64 {
192                    return Err(Error::IncorrectDataParameter);
193                }
194                Ok(Request::Register(Register {
195                    challenge: (&request[..32]).try_into().unwrap(),
196                    app_id: (&request[32..]).try_into().unwrap(),
197                }))
198            }
199
200            // authenticate
201            0x2 => {
202                let control_byte = ControlByte::try_from(p1)?;
203                if request.len() < 65 {
204                    return Err(Error::IncorrectDataParameter);
205                }
206                let key_handle_length = request[64] as usize;
207                if request.len() != 65 + key_handle_length {
208                    return Err(Error::IncorrectDataParameter);
209                }
210                Ok(Request::Authenticate(Authenticate {
211                    control_byte,
212                    challenge: (&request[..32]).try_into().unwrap(),
213                    app_id: (&request[32..64]).try_into().unwrap(),
214                    key_handle: &request[65..],
215                }))
216            }
217
218            // version
219            0x3 => Ok(Request::Version),
220
221            _ => Err(Error::InstructionNotSupportedOrInvalid),
222        }
223    }
224}
225
226/// CTAP1 (U2F) authenticator API
227///
228/// Note that all Authenticators automatically implement RPC with [`Request`] and
229/// [`Response`].
230pub trait Authenticator {
231    /// Register a U2F credential.
232    fn register(&mut self, request: &register::Request<'_>) -> Result<register::Response>;
233    /// Authenticate with a U2F credential.
234    fn authenticate(
235        &mut self,
236        request: &authenticate::Request<'_>,
237    ) -> Result<authenticate::Response>;
238    /// Supported U2F version.
239    fn version() -> [u8; 6] {
240        *b"U2F_V2"
241    }
242
243    #[inline(never)]
244    fn call_ctap1(&mut self, request: &Request<'_>) -> Result<Response> {
245        match request {
246            Request::Register(reg) => {
247                debug_now!("CTAP1.REG");
248                Ok(Response::Register(self.register(reg)?))
249            }
250            Request::Authenticate(auth) => {
251                debug_now!("CTAP1.AUTH");
252                Ok(Response::Authenticate(self.authenticate(auth)?))
253            }
254            Request::Version => Ok(Response::Version(Self::version())),
255        }
256    }
257}
258
259impl<A: Authenticator> crate::Rpc<Error, Request<'_>, Response> for A {
260    /// Dispatches the enum of possible requests into the appropriate trait method.
261    fn call(&mut self, request: &Request<'_>) -> Result<Response> {
262        self.call_ctap1(request)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use heapless::Vec;
270    use hex_literal::hex;
271    use iso7816::command::{
272        class::Class, instruction::Instruction, Command, CommandBuilder, ExpectedLen,
273    };
274
275    // examples taken from:
276    // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#examples
277
278    fn command(ins: u8, p1: u8, p2: u8, data: &[u8]) -> Command<1024> {
279        let builder = CommandBuilder::new(
280            Class::from_byte(0).unwrap(),
281            Instruction::from(ins),
282            p1,
283            p2,
284            data,
285            ExpectedLen::Max,
286        );
287        let mut apdu = Vec::<_, 1024>::new();
288        builder.serialize_into(&mut apdu).unwrap();
289        Command::try_from(&apdu).unwrap()
290    }
291
292    #[test]
293    fn test_register_request() {
294        let mut input = [0; 64];
295        // challenge
296        input[..32].copy_from_slice(&hex!(
297            "4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"
298        ));
299        // application
300        input[32..].copy_from_slice(&hex!(
301            "f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"
302        ));
303
304        let command = command(1, 0, 0, &input);
305        let request = Request::try_from(&command).unwrap();
306        let Request::Register(request) = request else {
307            panic!("expected register request, got: {:?}", request);
308        };
309        assert_eq!(request.challenge, &input[..32]);
310        assert_eq!(request.app_id, &input[32..]);
311    }
312
313    #[test]
314    fn test_register_response() {
315        let public_key = hex!("b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9");
316        let public_key = cosey::EcdhEsHkdf256PublicKey {
317            x: Bytes::from_slice(&public_key[..32]).unwrap(),
318            y: Bytes::from_slice(&public_key[32..]).unwrap(),
319        };
320        let key_handle = hex!("2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25");
321        let key_handle = Bytes::from_slice(&key_handle).unwrap();
322        let signature = hex!("304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871");
323        let signature = Bytes::from_slice(&signature).unwrap();
324        let attestation_certificate = hex!("3082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df");
325        let attestation_certificate = Bytes::from_slice(&attestation_certificate).unwrap();
326        let response = register::Response::new(
327            0x05,
328            &public_key,
329            key_handle,
330            signature,
331            attestation_certificate,
332        );
333        let mut output = Vec::<_, 1024>::new();
334        Response::Register(response).serialize(&mut output).unwrap();
335        assert_eq!(
336            output.as_slice(),
337            &hex!("0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871"),
338        );
339    }
340
341    #[test]
342    fn test_authenticate_request() {
343        let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57");
344        let application = &hex!("4b0be934baebb5d12d26011b69227fa5e86df94e7d94aa2949a89f2d493992ca");
345        let key_handle = &hex!("2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25");
346        let mut input = Vec::<_, 1024>::new();
347        input.extend_from_slice(challenge).unwrap();
348        input.extend_from_slice(application).unwrap();
349        input.push(u8::try_from(key_handle.len()).unwrap()).unwrap();
350        input.extend_from_slice(key_handle).unwrap();
351
352        let control_bytes = [
353            (0x07, ControlByte::CheckOnly),
354            (0x03, ControlByte::EnforceUserPresenceAndSign),
355            (0x08, ControlByte::DontEnforceUserPresenceAndSign),
356        ];
357
358        for (byte, variant) in control_bytes {
359            let command = command(2, byte, 0, &input);
360            let request = Request::try_from(&command).unwrap();
361            let Request::Authenticate(request) = request else {
362                panic!("expected authenticate request, got: {:?}", request);
363            };
364            assert_eq!(request.control_byte, variant);
365            assert_eq!(request.challenge, challenge);
366            assert_eq!(request.app_id, application);
367            assert_eq!(request.key_handle, key_handle);
368        }
369    }
370
371    #[test]
372    fn test_authenticate_response() {
373        let signature = &hex!("304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f");
374        let signature = Bytes::from_slice(signature).unwrap();
375        let response = authenticate::Response {
376            user_presence: 1,
377            count: 1,
378            signature,
379        };
380        let mut output = Vec::<_, 1024>::new();
381        Response::Authenticate(response)
382            .serialize(&mut output)
383            .unwrap();
384        assert_eq!(
385            output.as_slice(),
386            &hex!("0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f"),
387        );
388    }
389
390    #[test]
391    fn test_version_request() {
392        let command = command(3, 0, 0, &[]);
393        let request = Request::try_from(&command).unwrap();
394        assert_eq!(request, Request::Version);
395    }
396
397    #[test]
398    fn test_version_response() {
399        let response = Response::Version(*b"U2F_V2");
400        let mut output = Vec::<_, 1024>::new();
401        response.serialize(&mut output).unwrap();
402        assert_eq!(output.as_slice(), b"U2F_V2");
403    }
404}