libwebauthn/proto/ctap1/
model.rs

1use std::convert::TryFrom;
2use std::io::{BufRead, Cursor as IOCursor, Error as IOError, ErrorKind as IOErrorKind, Read};
3use std::time::Duration;
4
5use byteorder::{BigEndian, ReadBytesExt};
6use sha2::{Digest, Sha256};
7use x509_parser::prelude::{FromDer, X509Certificate};
8
9use crate::proto::ctap1::apdu::{ApduResponse, ApduResponseStatus};
10use crate::proto::ctap2::Ctap2Transport;
11use crate::webauthn::CtapError;
12
13#[derive(Debug, Clone, Copy)]
14pub enum Ctap1Transport {
15    Bt,
16    Ble,
17    Nfc,
18    Usb,
19}
20
21impl TryFrom<&Ctap2Transport> for Ctap1Transport {
22    type Error = CtapError;
23    fn try_from(ctap2: &Ctap2Transport) -> Result<Ctap1Transport, Self::Error> {
24        match ctap2 {
25            Ctap2Transport::Ble => Ok(Ctap1Transport::Ble),
26            Ctap2Transport::Usb => Ok(Ctap1Transport::Usb),
27            Ctap2Transport::Nfc => Ok(Ctap1Transport::Nfc),
28            Ctap2Transport::Internal => Err(CtapError::UnsupportedOption),
29            Ctap2Transport::Hybrid => Err(CtapError::UnsupportedOption),
30        }
31    }
32}
33
34#[derive(Debug, Clone, Copy)]
35pub enum Ctap1Version {
36    U2fV2,
37}
38
39#[derive(Debug, Clone)]
40pub struct Ctap1RegisteredKey {
41    pub version: Ctap1Version,
42    pub key_handle: Vec<u8>,
43    pub transports: Option<Vec<Ctap1Transport>>,
44    pub app_id: Option<String>,
45}
46
47impl Ctap1RegisteredKey {
48    pub fn new_u2f_v2(key_handle: &[u8]) -> Ctap1RegisteredKey {
49        Ctap1RegisteredKey {
50            version: Ctap1Version::U2fV2,
51            key_handle: Vec::from(key_handle),
52            transports: None,
53            app_id: None,
54        }
55    }
56}
57
58#[derive(Debug, Clone)]
59pub struct Ctap1RegisterRequest {
60    pub version: Ctap1Version,
61    pub app_id_hash: Vec<u8>,
62    pub challenge: Vec<u8>,
63    pub registered_keys: Vec<Ctap1RegisteredKey>,
64    pub timeout: Duration,
65    pub require_user_presence: bool,
66}
67
68impl Ctap1RegisterRequest {
69    pub fn new_u2f_v2(
70        app_id: &str,
71        challenge: &[u8],
72        registered_keys: Vec<Ctap1RegisteredKey>,
73        timeout: Duration,
74        require_user_presence: bool,
75    ) -> Ctap1RegisterRequest {
76        let mut hasher = Sha256::default();
77        hasher.update(app_id);
78        let app_id_hash = hasher.finalize().to_vec();
79
80        Ctap1RegisterRequest {
81            version: Ctap1Version::U2fV2,
82            app_id_hash,
83            challenge: Vec::from(challenge),
84            registered_keys,
85            timeout,
86            require_user_presence,
87        }
88    }
89
90    pub fn dummy(timeout: Duration) -> Self {
91        Ctap1RegisterRequest {
92            version: Ctap1Version::U2fV2,
93            app_id_hash: vec![0; 32],
94            challenge: vec![0; 32],
95            registered_keys: Vec::new(),
96            timeout,
97            require_user_presence: true,
98        }
99    }
100}
101
102#[derive(Debug)]
103pub struct Ctap1RegisterResponse {
104    pub version: Ctap1Version,
105    pub public_key: Vec<u8>,
106    pub key_handle: Vec<u8>,
107    pub attestation: Vec<u8>,
108    pub signature: Vec<u8>,
109}
110
111impl TryFrom<ApduResponse> for Ctap1RegisterResponse {
112    type Error = IOError;
113
114    fn try_from(apdu: ApduResponse) -> Result<Self, Self::Error> {
115        if apdu.status()? != ApduResponseStatus::NoError {
116            return Err(IOError::new(
117                IOErrorKind::InvalidInput,
118                "APDU packets need to have status NoError to be converted..",
119            ));
120        }
121
122        let data = apdu.data.ok_or(IOError::new(
123            IOErrorKind::InvalidInput,
124            "Emtpy APDU packet.",
125        ))?;
126
127        let mut cursor = IOCursor::new(data);
128        cursor.consume(1); // Reserved bytes.
129
130        let mut public_key = vec![0u8; 65];
131        cursor.read_exact(&mut public_key)?;
132
133        let key_handle_len = cursor.read_u8()? as u64;
134        let mut key_handle = vec![0u8; key_handle_len as usize];
135        cursor.read_exact(&mut key_handle)?;
136
137        let mut remaining = vec![];
138        cursor.read_to_end(&mut remaining)?;
139
140        let (signature, _) = X509Certificate::from_der(&remaining).or(Err(IOError::new(
141            IOErrorKind::InvalidData,
142            "Failed to parse X509 attestation data",
143        )))?;
144        let signature = Vec::from(signature);
145        let attestation = Vec::from(&remaining[0..remaining.len() - signature.len()]);
146
147        Ok(Ctap1RegisterResponse {
148            version: Ctap1Version::U2fV2,
149            public_key,
150            key_handle,
151            attestation,
152            signature,
153        })
154    }
155}
156
157impl Ctap1RegisterResponse {
158    pub fn as_registered_key(&self) -> Result<Ctap1RegisteredKey, IOError> {
159        Ok(Ctap1RegisteredKey::new_u2f_v2(&self.key_handle))
160    }
161}
162
163#[derive(Debug, Clone)]
164pub struct Ctap1SignRequest {
165    pub app_id_hash: Vec<u8>,
166    pub challenge: Vec<u8>,
167    pub key_handle: Vec<u8>,
168    pub timeout: Duration,
169    pub require_user_presence: bool,
170}
171
172impl Ctap1SignRequest {
173    pub fn new(
174        app_id: &str,
175        challenge: &[u8],
176        key_handle: &[u8],
177        timeout: Duration,
178        require_user_presence: bool,
179    ) -> Ctap1SignRequest {
180        let mut hasher = Sha256::default();
181        hasher.update(app_id);
182        let app_id_hash = hasher.finalize().to_vec();
183
184        Ctap1SignRequest {
185            app_id_hash: app_id_hash,
186            challenge: Vec::from(challenge),
187            key_handle: Vec::from(key_handle),
188            timeout,
189            require_user_presence,
190        }
191    }
192
193    pub fn new_preflight(
194        app_id_hash: &[u8],
195        challenge: &[u8],
196        key_handle: &[u8],
197        timeout: Duration,
198    ) -> Ctap1SignRequest {
199        Ctap1SignRequest {
200            app_id_hash: Vec::from(app_id_hash),
201            challenge: Vec::from(challenge),
202            key_handle: Vec::from(key_handle),
203            timeout,
204            require_user_presence: false,
205        }
206    }
207}
208
209#[derive(Debug)]
210pub struct Ctap1VersionRequest {}
211
212impl Ctap1VersionRequest {
213    pub fn new() -> Ctap1VersionRequest {
214        Ctap1VersionRequest {}
215    }
216}
217
218#[derive(Debug)]
219pub struct Ctap1VersionResponse {
220    pub version: Ctap1Version,
221}
222
223impl TryFrom<ApduResponse> for Ctap1VersionResponse {
224    type Error = IOError;
225
226    fn try_from(apdu: ApduResponse) -> Result<Self, Self::Error> {
227        if apdu.status()? != ApduResponseStatus::NoError {
228            return Err(IOError::new(
229                IOErrorKind::InvalidInput,
230                "APDU packets need to have status NoError to be converted..",
231            ));
232        }
233
234        let data = apdu.data.ok_or(IOError::new(
235            IOErrorKind::InvalidInput,
236            "Emtpy APDU packet.",
237        ))?;
238
239        let version_string = String::from_utf8(data).or(Err(IOError::new(
240            IOErrorKind::InvalidInput,
241            "Invalid UTF-8 bytes in CTAP1 version string",
242        )))?;
243
244        let version = match version_string.as_str() {
245            "U2F_V2" => Ctap1Version::U2fV2,
246            _ => {
247                return Err(IOError::new(
248                    IOErrorKind::InvalidInput,
249                    format!("Invalid CTAP1 version string: {:}", version_string),
250                ))
251            }
252        };
253
254        Ok(Ctap1VersionResponse { version })
255    }
256}
257
258#[derive(Debug, Clone)]
259pub struct Ctap1SignResponse {
260    pub user_presence_verified: bool,
261    pub counter: u32,
262    pub signature: Vec<u8>,
263}
264
265impl TryFrom<ApduResponse> for Ctap1SignResponse {
266    type Error = IOError;
267
268    fn try_from(apdu: ApduResponse) -> Result<Self, Self::Error> {
269        if apdu.status()? != ApduResponseStatus::NoError {
270            return Err(IOError::new(
271                IOErrorKind::InvalidInput,
272                "APDU packets need to have status NoError to be converted..",
273            ));
274        }
275
276        let data = apdu.data.ok_or(IOError::new(
277            IOErrorKind::InvalidInput,
278            "Emtpy APDU packet.",
279        ))?;
280
281        let mut cursor = IOCursor::new(data);
282        let user_presence_verified = match cursor.read_u8()? {
283            0x01 => true,
284            _ => false,
285        };
286        let counter = cursor.read_u32::<BigEndian>()?;
287
288        let mut signature = vec![];
289        cursor.read_to_end(&mut signature)?;
290
291        Ok(Ctap1SignResponse {
292            user_presence_verified,
293            counter,
294            signature,
295        })
296    }
297}
298
299pub trait Preflight<P>: Sized {
300    /// Modify request in place, removing items in exclusion list, in favour of generating new pre-fligh requests.
301    fn preflight(&self) -> Result<(Self, Vec<P>), CtapError>;
302}
303
304impl Preflight<Ctap1SignRequest> for Ctap1RegisterRequest {
305    fn preflight(&self) -> Result<(Self, Vec<Ctap1SignRequest>), CtapError> {
306        let preflight_requests: Vec<Ctap1SignRequest> = self
307            .registered_keys
308            .iter()
309            .map(|registered_key| {
310                Ctap1SignRequest::new_preflight(
311                    &self.app_id_hash,
312                    &[0u8; 32],
313                    &registered_key.key_handle,
314                    self.timeout,
315                )
316            })
317            .collect();
318
319        let mut modified = self.to_owned();
320        modified.registered_keys = vec![];
321        Ok((modified, preflight_requests))
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use crate::proto::ctap1::apdu::ApduResponse;
328    use crate::proto::ctap1::Ctap1RegisterResponse;
329    use std::convert::TryInto;
330
331    #[test]
332    fn register_response_apdu_to_ctap1() {
333        let apdu = hex::decode("05046DDBE3C25D974C9A403D6C648ED41C219D44734C43986B4053B325BE01C31E28F146731E5C21BA0E0E1938DA4C1FECAD650A2971A13CF6076BF52B52C19F8D0E40602CFD267868E84D4852BD5B008BC6CE0211D4858C8A647328A13B7D5C0A42B3893D63A58FCA7BD3EBB74F55CE537195DFF0113D4C561BBB7DFAC0C0ECD1AFB53082015930820100A003020102020102300A06082A8648CE3D0403023028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653022180F32303030303130313030303030305A180F32303939313233313233353935395A3028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653059301306072A8648CE3D020106082A8648CE3D030107034200040393AF897BE858E88C1953876A1A538477C4DA6E6EA14ACF0A2FD89A4DCCF95878A8CD2929029CC1D794BFFB9C37547CBBB5BB31AB3A6756ACF74F123CECD45CA31730153013060B2B0601040182E51C020101040403020470300A06082A8648CE3D040302034700304402207F958ABE6CF08CB2E9A03774D52DF8C0EA261E1AC0C283409FEDD8D36DFAF09302204EEB7501C720428D206E1B092D8D26CA8536B70F5F09AEA99562390BEF1BA7EC3044022031413D6E238A5F998B26B3931655C411847D99776B6E5CF15AA2E11BFAF325F00220098745DA82C11BB242934BAC6AE95155EAAD68520D695D46982DA9B2C94F94E3").unwrap();
334        let apdu = ApduResponse::new_success(&apdu);
335        let decoded: Ctap1RegisterResponse = apdu.try_into().unwrap();
336
337        assert_eq!(decoded.public_key, hex::decode("046DDBE3C25D974C9A403D6C648ED41C219D44734C43986B4053B325BE01C31E28F146731E5C21BA0E0E1938DA4C1FECAD650A2971A13CF6076BF52B52C19F8D0E").unwrap());
338        assert_eq!(decoded.key_handle, hex::decode("602CFD267868E84D4852BD5B008BC6CE0211D4858C8A647328A13B7D5C0A42B3893D63A58FCA7BD3EBB74F55CE537195DFF0113D4C561BBB7DFAC0C0ECD1AFB5").unwrap());
339        assert_eq!(decoded.attestation, hex::decode("3082015930820100A003020102020102300A06082A8648CE3D0403023028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653022180F32303030303130313030303030305A180F32303939313233313233353935395A3028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653059301306072A8648CE3D020106082A8648CE3D030107034200040393AF897BE858E88C1953876A1A538477C4DA6E6EA14ACF0A2FD89A4DCCF95878A8CD2929029CC1D794BFFB9C37547CBBB5BB31AB3A6756ACF74F123CECD45CA31730153013060B2B0601040182E51C020101040403020470300A06082A8648CE3D040302034700304402207F958ABE6CF08CB2E9A03774D52DF8C0EA261E1AC0C283409FEDD8D36DFAF09302204EEB7501C720428D206E1B092D8D26CA8536B70F5F09AEA99562390BEF1BA7EC").unwrap());
340        assert_eq!(decoded.signature, hex::decode("3044022031413D6E238A5F998B26B3931655C411847D99776B6E5CF15AA2E11BFAF325F00220098745DA82C11BB242934BAC6AE95155EAAD68520D695D46982DA9B2C94F94E3").unwrap());
341    }
342}