ctap_types/
webauthn.rs

1//! Subset of WebAuthn types that crept into CTAP.
2
3use crate::sizes::*;
4use crate::{Bytes, String};
5use serde::{de::Deserializer, Deserialize, Serialize};
6
7#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
8pub struct PublicKeyCredentialRpEntity {
9    pub id: String<256>,
10    #[serde(
11        default,
12        skip_serializing_if = "Option::is_none",
13        deserialize_with = "deserialize_from_str_and_truncate"
14    )]
15    pub name: Option<String<64>>,
16    /// This field has been removed in Webauthn 2 but CTAP 2.2 requires implementors to accept it.
17    ///
18    /// The content of this field must not be stored.  Therefore we use the [`Icon`][] helper type.
19    ///
20    /// See [issue #9][] for more information.
21    ///
22    /// [issue #9]: https://github.com/solokeys/ctap-types/issues/9
23    #[serde(skip_serializing, alias = "url")]
24    pub icon: Option<Icon>,
25}
26
27/// Helper type for the `icon` field of [`PublicKeyCredentialRpEntity`][].
28///
29/// This field must be parsed but not used or stored.  Therefore this wrapper type can be
30/// deserialized from a string but does not store any data.
31#[derive(Clone, Debug, Eq, PartialEq)]
32#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
33pub struct Icon;
34
35impl<'de> Deserialize<'de> for Icon {
36    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37    where
38        D: Deserializer<'de>,
39    {
40        let _s: &'de str = Deserialize::deserialize(deserializer)?;
41        Ok(Self)
42    }
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct PublicKeyCredentialUserEntity {
48    pub id: Bytes<64>,
49    #[serde(
50        default,
51        deserialize_with = "deserialize_from_str_and_skip_if_too_long"
52    )]
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub icon: Option<String<128>>,
55    #[serde(
56        default,
57        skip_serializing_if = "Option::is_none",
58        deserialize_with = "deserialize_from_str_and_truncate"
59    )]
60    pub name: Option<String<64>>,
61    #[serde(
62        default,
63        skip_serializing_if = "Option::is_none",
64        deserialize_with = "deserialize_from_str_and_truncate"
65    )]
66    pub display_name: Option<String<64>>,
67}
68
69fn deserialize_from_str_and_skip_if_too_long<'de, D, const L: usize>(
70    deserializer: D,
71) -> Result<Option<String<L>>, D::Error>
72where
73    D: serde::Deserializer<'de>,
74{
75    let s: &'de str = Deserialize::deserialize(deserializer)?;
76    // String::from(s) could panic and is not really infallibe.  It is removed in heapless 0.8.
77    #[allow(clippy::unnecessary_fallible_conversions)]
78    match String::try_from(s) {
79        Ok(string) => Ok(Some(string)),
80        Err(_err) => {
81            info_now!("skipping field: {:?}", _err);
82            Ok(None)
83        }
84    }
85}
86
87fn deserialize_from_str_and_truncate<'de, D, const L: usize>(
88    deserializer: D,
89) -> Result<Option<String<L>>, D::Error>
90where
91    D: serde::Deserializer<'de>,
92{
93    let s: Option<&str> = serde::Deserialize::deserialize(deserializer)?;
94    Ok(s.map(truncate))
95}
96
97fn truncate<const L: usize>(s: &str) -> String<L> {
98    let split = floor_char_boundary(s, L);
99    let mut truncated = String::new();
100    // floor_char_boundary(s, L) <= L, so this cannot fail
101    truncated.push_str(&s[..split]).unwrap();
102    truncated
103}
104
105// Copy of the nightly str::floor_char_boundary function
106fn floor_char_boundary(s: &str, index: usize) -> usize {
107    if index >= s.len() {
108        s.len()
109    } else {
110        let lower_bound = index.saturating_sub(3);
111        let new_index = s.as_bytes()[lower_bound..=index]
112            .iter()
113            .rposition(|b| is_utf8_char_boundary(*b));
114
115        // SAFETY: we know that the character boundary will be within four bytes
116        unsafe { lower_bound + new_index.unwrap_unchecked() }
117    }
118}
119
120// Copy of the private u8::is_utf8_char_boundary function
121#[inline]
122const fn is_utf8_char_boundary(b: u8) -> bool {
123    // This is bit magic equivalent to: b < 128 || b >= 192
124    (b as i8) >= -0x40
125}
126
127impl PublicKeyCredentialUserEntity {
128    pub fn from(id: Bytes<64>) -> Self {
129        Self {
130            id,
131            icon: None,
132            name: None,
133            display_name: None,
134        }
135    }
136}
137
138#[derive(Clone, Debug, Eq, PartialEq)]
139pub struct KnownPublicKeyCredentialParameters {
140    pub alg: i32,
141}
142
143impl From<KnownPublicKeyCredentialParameters> for PublicKeyCredentialParameters {
144    fn from(value: KnownPublicKeyCredentialParameters) -> Self {
145        Self {
146            alg: value.alg,
147            key_type: String::from("public-key"),
148        }
149    }
150}
151
152pub enum UnknownPKCredentialParam {
153    UnknownType,
154    UnknownAlg,
155}
156
157/// ECDSA w/ SHA-256
158pub const ES256: i32 = -7;
159/// EdDSA
160pub const ED_DSA: i32 = -8;
161
162pub const COUNT_KNOWN_ALGS: usize = 2;
163pub const KNOWN_ALGS: [i32; COUNT_KNOWN_ALGS] = [ES256, ED_DSA];
164
165impl TryFrom<PublicKeyCredentialParameters> for KnownPublicKeyCredentialParameters {
166    type Error = UnknownPKCredentialParam;
167
168    fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
169        if value.key_type != "public-key" {
170            Err(UnknownPKCredentialParam::UnknownType)
171        } else if KNOWN_ALGS.contains(&value.alg) {
172            Ok(Self { alg: value.alg })
173        } else {
174            Err(UnknownPKCredentialParam::UnknownAlg)
175        }
176    }
177}
178
179/// Struct of filtered PublicKeyCredentialParameters, that drops unknown algorithms while parsing
180#[derive(Clone, Debug, Eq, PartialEq)]
181pub struct FilteredPublicKeyCredentialParameters(
182    pub heapless::Vec<KnownPublicKeyCredentialParameters, COUNT_KNOWN_ALGS>,
183);
184
185impl Serialize for FilteredPublicKeyCredentialParameters {
186    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
187    where
188        S: serde::Serializer,
189    {
190        use serde::ser::SerializeSeq;
191        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
192        for element in &self.0 {
193            let el: PublicKeyCredentialParameters = element.clone().into();
194            seq.serialize_element(&el)?
195        }
196        seq.end()
197    }
198}
199
200impl<'de> Deserialize<'de> for FilteredPublicKeyCredentialParameters {
201    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202    where
203        D: serde::Deserializer<'de>,
204    {
205        struct ValueVisitor;
206        impl<'de> serde::de::Visitor<'de> for ValueVisitor {
207            type Value = FilteredPublicKeyCredentialParameters;
208
209            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
210                formatter.write_str("a sequence")
211            }
212
213            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
214            where
215                A: serde::de::SeqAccess<'de>,
216            {
217                let mut values = FilteredPublicKeyCredentialParameters(Default::default());
218                while let Some(value) = seq.next_element::<PublicKeyCredentialParameters>()? {
219                    let Ok(el) = value.try_into() else {
220                        // Drop unknown algorithms
221                        continue;
222                    };
223                    // We drop too many elements. This shouldn't happen as we have enough space for all known algorithms.
224                    // This can only happen in case of duplicates.
225                    values.0.push(el).ok();
226                }
227                Ok(values)
228            }
229        }
230
231        deserializer.deserialize_seq(ValueVisitor)
232    }
233}
234
235#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
236pub struct PublicKeyCredentialParameters {
237    pub alg: i32,
238    #[serde(rename = "type")]
239    pub key_type: String<32>,
240}
241
242impl PublicKeyCredentialParameters {
243    pub fn public_key_with_alg(alg: i32) -> Self {
244        Self {
245            alg,
246            key_type: String::from("public-key"),
247        }
248    }
249}
250
251#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct PublicKeyCredentialDescriptor {
254    // NB: if this is too small, get a nasty error
255    // See serde::error/custom for more info
256    pub id: Bytes<MAX_CREDENTIAL_ID_LENGTH>,
257    #[serde(rename = "type")]
258    pub key_type: String<32>,
259    // https://w3c.github.io/webauthn/#enumdef-authenticatortransport
260    // transports: ...
261}
262
263#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265/// Same as PublicKeyCredentialDescriptor but which deserializes using references
266pub struct PublicKeyCredentialDescriptorRef<'a> {
267    pub id: &'a serde_bytes::Bytes,
268    #[serde(rename = "type")]
269    pub key_type: &'a str,
270    // https://w3c.github.io/webauthn/#enumdef-authenticatortransport
271    // transports: ...
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_truncate() {
280        // Example from ยง 6.4.1 String Truncation in the Webauthn spec
281        let v = vec![0x61, 0x67, 0xcc, 0x88];
282        let s = std::str::from_utf8(&v).unwrap();
283
284        assert_eq!(truncate::<1>(s), "a");
285        assert_eq!(truncate::<2>(s), "ag");
286        assert_eq!(truncate::<3>(s), "ag");
287        assert_eq!(truncate::<4>(s), s);
288        assert_eq!(truncate::<5>(s), s);
289        assert_eq!(truncate::<64>(s), s);
290    }
291}