authenticator_ctap2_2021/ctap2/
client_data.rs

1use serde::de::{self, Deserializer, Error as SerdeError, MapAccess, Visitor};
2use serde::ser::SerializeMap;
3use serde::{Deserialize, Serialize, Serializer};
4use serde_json as json;
5use sha2::{Digest, Sha256};
6use std::fmt;
7
8/// https://w3c.github.io/webauthn/#dom-collectedclientdata-tokenbinding
9// tokenBinding, of type TokenBinding
10//
11//    This OPTIONAL member contains information about the state of the Token
12//    Binding protocol [TokenBinding] used when communicating with the Relying
13//    Party. Its absence indicates that the client doesn’t support token
14//    binding.
15//
16//    status, of type TokenBindingStatus
17//
18//        This member is one of the following:
19//
20//        supported
21//
22//            Indicates the client supports token binding, but it was not
23//            negotiated when communicating with the Relying Party.
24//
25//        present
26//
27//            Indicates token binding was used when communicating with the
28//            Relying Party. In this case, the id member MUST be present.
29//
30//    id, of type DOMString
31//
32//        This member MUST be present if status is present, and MUST be a
33//        base64url encoding of the Token Binding ID that was used when
34//        communicating with the Relying Party.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum TokenBinding {
37    Present(String),
38    Supported,
39}
40
41impl Serialize for TokenBinding {
42    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43    where
44        S: Serializer,
45    {
46        let mut map = serializer.serialize_map(Some(2))?;
47        match *self {
48            TokenBinding::Supported => {
49                map.serialize_entry(&"status", &"supported")?;
50            }
51            TokenBinding::Present(ref v) => {
52                map.serialize_entry(&"status", "present")?;
53                // Verify here, that `v` is valid base64 encoded?
54                // base64::decode_config(&v, base64::URL_SAFE_NO_PAD);
55                // For now: Let the token do that.
56                map.serialize_entry(&"id", &v)?;
57            }
58        }
59        map.end()
60    }
61}
62
63impl<'de> Deserialize<'de> for TokenBinding {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: Deserializer<'de>,
67    {
68        struct TokenBindingVisitor;
69
70        impl<'de> Visitor<'de> for TokenBindingVisitor {
71            type Value = TokenBinding;
72
73            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
74                formatter.write_str("a byte string")
75            }
76
77            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
78            where
79                M: MapAccess<'de>,
80            {
81                let mut id = None;
82                let mut status = None;
83
84                while let Some(key) = map.next_key()? {
85                    match key {
86                        "status" => {
87                            status = Some(map.next_value()?);
88                        }
89                        "id" => {
90                            id = Some(map.next_value()?);
91                        }
92                        k => {
93                            return Err(M::Error::custom(format!("unexpected key: {:?}", k)));
94                        }
95                    }
96                }
97
98                if let Some(stat) = status {
99                    match stat {
100                        "present" => {
101                            if let Some(id) = id {
102                                Ok(TokenBinding::Present(id))
103                            } else {
104                                Err(SerdeError::missing_field("id"))
105                            }
106                        }
107                        "supported" => Ok(TokenBinding::Supported),
108                        k => {
109                            return Err(M::Error::custom(format!(
110                                "unexpected status key: {:?}",
111                                k
112                            )));
113                        }
114                    }
115                } else {
116                    Err(SerdeError::missing_field("status"))
117                }
118            }
119        }
120
121        deserializer.deserialize_map(TokenBindingVisitor)
122    }
123}
124
125/// https://w3c.github.io/webauthn/#dom-collectedclientdata-type
126// type, of type DOMString
127//
128//    This member contains the string "webauthn.create" when creating new
129//    credentials, and "webauthn.get" when getting an assertion from an
130//    existing credential. The purpose of this member is to prevent certain
131//    types of signature confusion attacks (where an attacker substitutes one
132//    legitimate signature for another).
133#[derive(Debug, Copy, Clone, PartialEq, Eq)]
134pub enum WebauthnType {
135    Create,
136    Get,
137}
138
139impl Serialize for WebauthnType {
140    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141    where
142        S: Serializer,
143    {
144        match *self {
145            WebauthnType::Create => serializer.serialize_str(&"webauthn.create"),
146            WebauthnType::Get => serializer.serialize_str(&"webauthn.get"),
147        }
148    }
149}
150
151impl<'de> Deserialize<'de> for WebauthnType {
152    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153    where
154        D: Deserializer<'de>,
155    {
156        struct WebauthnTypeVisitor;
157
158        impl<'de> Visitor<'de> for WebauthnTypeVisitor {
159            type Value = WebauthnType;
160
161            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
162                formatter.write_str("a string")
163            }
164
165            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
166            where
167                E: de::Error,
168            {
169                match v {
170                    "webauthn.create" => Ok(WebauthnType::Create),
171                    "webauthn.get" => Ok(WebauthnType::Get),
172                    _ => Err(E::custom("unexpected webauthn_type")),
173                }
174            }
175        }
176
177        deserializer.deserialize_str(WebauthnTypeVisitor)
178    }
179}
180
181#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
182pub struct Challenge(pub String);
183
184impl Challenge {
185    pub fn new(input: Vec<u8>) -> Self {
186        let value = base64::encode_config(&input, base64::URL_SAFE_NO_PAD);
187        Challenge(value)
188    }
189}
190
191impl From<Vec<u8>> for Challenge {
192    fn from(v: Vec<u8>) -> Challenge {
193        Challenge::new(v)
194    }
195}
196
197impl AsRef<[u8]> for Challenge {
198    fn as_ref(&self) -> &[u8] {
199        self.0.as_bytes()
200    }
201}
202
203pub type Origin = String;
204
205#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
206pub struct CollectedClientData {
207    #[serde(rename = "type")]
208    pub webauthn_type: WebauthnType,
209    pub challenge: Challenge,
210    pub origin: Origin,
211    // It is optional, according to https://www.w3.org/TR/webauthn/#collectedclientdata-hash-of-the-serialized-client-data
212    // But we are serializing it, so we *have to* set crossOrigin (if not given, we have to set it to false)
213    // Thus, on our side, it is not optional. For deserializing, we provide a default (bool's default == False)
214    #[serde(rename = "crossOrigin", default)]
215    pub cross_origin: bool,
216    #[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")]
217    pub token_binding: Option<TokenBinding>,
218}
219
220#[derive(Debug, Eq, PartialEq)]
221pub struct ClientDataHash([u8; 32]);
222
223impl PartialEq<[u8]> for ClientDataHash {
224    fn eq(&self, other: &[u8]) -> bool {
225        self.0.eq(other)
226    }
227}
228
229impl AsRef<[u8]> for ClientDataHash {
230    fn as_ref(&self) -> &[u8] {
231        &self.0
232    }
233}
234
235impl Serialize for ClientDataHash {
236    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
237    where
238        S: Serializer,
239    {
240        serializer.serialize_bytes(&self.0)
241    }
242}
243
244#[cfg(test)]
245impl<'de> Deserialize<'de> for ClientDataHash {
246    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
247    where
248        D: Deserializer<'de>,
249    {
250        struct ClientDataHashVisitor;
251
252        impl<'de> Visitor<'de> for ClientDataHashVisitor {
253            type Value = ClientDataHash;
254
255            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
256                formatter.write_str("a byte string")
257            }
258
259            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
260            where
261                E: de::Error,
262            {
263                let mut out = [0u8; 32];
264                if out.len() != v.len() {
265                    return Err(E::invalid_length(v.len(), &"32"));
266                }
267                out.copy_from_slice(v);
268                Ok(ClientDataHash(out))
269            }
270        }
271
272        deserializer.deserialize_bytes(ClientDataHashVisitor)
273    }
274}
275
276impl CollectedClientData {
277    pub fn hash(&self) -> json::Result<ClientDataHash> {
278        // WebIDL's dictionary definition specifies that the order of the struct
279        // is exactly as the WebIDL specification declares it, with an algorithm
280        // for partial dictionaries, so that's how interop works for these
281        // things.
282        // See: https://heycam.github.io/webidl/#dfn-dictionary
283
284        let data = json::to_vec(&self)?;
285        let mut hasher = Sha256::new();
286        hasher.update(&data);
287
288        let mut output = [0u8; 32];
289        output.copy_from_slice(hasher.finalize().as_slice());
290
291        Ok(ClientDataHash(output))
292    }
293}
294
295#[cfg(test)]
296mod test {
297    use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType};
298    use serde_json as json;
299
300    #[test]
301    fn test_token_binding_status() {
302        let tok = TokenBinding::Present("AAECAw".to_string());
303
304        let json_value = json::to_string(&tok).unwrap();
305        assert_eq!(json_value, "{\"status\":\"present\",\"id\":\"AAECAw\"}");
306
307        let tok = TokenBinding::Supported;
308
309        let json_value = json::to_string(&tok).unwrap();
310        assert_eq!(json_value, "{\"status\":\"supported\"}");
311    }
312
313    #[test]
314    fn test_webauthn_type() {
315        let t = WebauthnType::Create;
316
317        let json_value = json::to_string(&t).unwrap();
318        assert_eq!(json_value, "\"webauthn.create\"");
319
320        let t = WebauthnType::Get;
321        let json_value = json::to_string(&t).unwrap();
322        assert_eq!(json_value, "\"webauthn.get\"");
323    }
324
325    #[test]
326    fn test_collected_client_data_parsing() {
327        let original_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
328        let parsed: CollectedClientData = serde_json::from_str(&original_str).unwrap();
329        let expected = CollectedClientData {
330            webauthn_type: WebauthnType::Create,
331            challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
332            origin: String::from("example.com"),
333            cross_origin: false,
334            token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
335        };
336        assert_eq!(parsed, expected);
337
338        let back_again = serde_json::to_string(&expected).unwrap();
339        assert_eq!(back_again, original_str);
340    }
341
342    #[test]
343    fn test_collected_client_data_defaults() {
344        let cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
345        let no_cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
346        let parsed: CollectedClientData = serde_json::from_str(&no_cross_origin_str).unwrap();
347        let expected = CollectedClientData {
348            webauthn_type: WebauthnType::Create,
349            challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
350            origin: String::from("example.com"),
351            cross_origin: false,
352            token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
353        };
354        assert_eq!(parsed, expected);
355
356        let back_again = serde_json::to_string(&expected).unwrap();
357        assert_eq!(back_again, cross_origin_str);
358    }
359
360    #[test]
361    fn test_collected_client_data() {
362        let client_data = CollectedClientData {
363            webauthn_type: WebauthnType::Create,
364            challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
365            origin: String::from("example.com"),
366            cross_origin: false,
367            token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
368        };
369
370        assert_eq!(
371            client_data.hash().unwrap(),
372            //  echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t
373            ClientDataHash {
374                0: [
375                    0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11,
376                    0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f,
377                    0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80
378                ]
379            }
380        );
381    }
382}