webauthn_attestation_ca/
lib.rs

1use base64urlsafedata::Base64UrlSafeData;
2use openssl::error::ErrorStack as OpenSSLErrorStack;
3use openssl::{hash, x509};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct DeviceDescription {
11    pub(crate) en: String,
12    pub(crate) localised: BTreeMap<String, String>,
13}
14
15impl DeviceDescription {
16    /// A default description of device.
17    pub fn description_en(&self) -> &str {
18        self.en.as_str()
19    }
20
21    /// A map of locale identifiers to a localised description of the device.
22    /// If the request locale is not found, you should try other user preferenced locales
23    /// falling back to the default value.
24    pub fn description_localised(&self) -> &BTreeMap<String, String> {
25        &self.localised
26    }
27}
28
29/// A serialised Attestation CA.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct SerialisableAttestationCa {
32    pub(crate) ca: Base64UrlSafeData,
33    pub(crate) aaguids: BTreeMap<Uuid, DeviceDescription>,
34    pub(crate) blanket_allow: bool,
35}
36
37/// A structure representing an Attestation CA and other options associated to this CA.
38///
39/// Generally depending on the Attestation CA in use, this can help determine properties
40/// of the authenticator that is in use.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(
43    try_from = "SerialisableAttestationCa",
44    into = "SerialisableAttestationCa"
45)]
46pub struct AttestationCa {
47    /// The x509 root CA of the attestation chain that a security key will be attested to.
48    ca: x509::X509,
49    /// If not empty, the set of acceptable AAGUIDS (Device Ids) that are allowed to be
50    /// attested as trusted by this CA. AAGUIDS that are not in this set, but signed by
51    /// this CA will NOT be trusted.
52    aaguids: BTreeMap<Uuid, DeviceDescription>,
53    blanket_allow: bool,
54}
55
56#[allow(clippy::from_over_into)]
57impl Into<SerialisableAttestationCa> for AttestationCa {
58    fn into(self) -> SerialisableAttestationCa {
59        SerialisableAttestationCa {
60            ca: Base64UrlSafeData::from(self.ca.to_der().expect("Invalid DER")),
61            aaguids: self.aaguids,
62            blanket_allow: self.blanket_allow,
63        }
64    }
65}
66
67impl TryFrom<SerialisableAttestationCa> for AttestationCa {
68    type Error = OpenSSLErrorStack;
69
70    fn try_from(data: SerialisableAttestationCa) -> Result<Self, Self::Error> {
71        Ok(AttestationCa {
72            ca: x509::X509::from_der(data.ca.as_slice())?,
73            aaguids: data.aaguids,
74            blanket_allow: data.blanket_allow,
75        })
76    }
77}
78
79impl AttestationCa {
80    pub fn ca(&self) -> &x509::X509 {
81        &self.ca
82    }
83
84    pub fn aaguids(&self) -> &BTreeMap<Uuid, DeviceDescription> {
85        &self.aaguids
86    }
87
88    pub fn blanket_allow(&self) -> bool {
89        self.blanket_allow
90    }
91
92    /// Retrieve the Key Identifier for this Attestation Ca
93    pub fn get_kid(&self) -> Result<Vec<u8>, OpenSSLErrorStack> {
94        self.ca
95            .digest(hash::MessageDigest::sha256())
96            .map(|bytes| bytes.to_vec())
97    }
98
99    fn insert_device(
100        &mut self,
101        aaguid: Uuid,
102        desc_english: String,
103        desc_localised: BTreeMap<String, String>,
104    ) {
105        self.blanket_allow = false;
106        self.aaguids.insert(
107            aaguid,
108            DeviceDescription {
109                en: desc_english,
110                localised: desc_localised,
111            },
112        );
113    }
114
115    fn new_from_pem(data: &[u8]) -> Result<Self, OpenSSLErrorStack> {
116        Ok(AttestationCa {
117            ca: x509::X509::from_pem(data)?,
118            aaguids: BTreeMap::default(),
119            blanket_allow: true,
120        })
121    }
122
123    fn union(&mut self, other: &Self) {
124        // if either is a blanket allow, we just do that.
125        if self.blanket_allow || other.blanket_allow {
126            self.blanket_allow = true;
127            self.aaguids.clear();
128        } else {
129            self.blanket_allow = false;
130            for (o_aaguid, o_device) in other.aaguids.iter() {
131                // We can use the entry api here since o_aaguid is copy.
132                self.aaguids
133                    .entry(*o_aaguid)
134                    .or_insert_with(|| o_device.clone());
135            }
136        }
137    }
138
139    fn intersection(&mut self, other: &Self) {
140        // If they are a blanket allow, do nothing, we are already
141        // more restrictive, or we also are a blanket allow
142        if other.blanket_allow() {
143            // Do nothing
144        } else if self.blanket_allow {
145            // Just set our aaguids to other, and remove our blanket allow.
146            self.blanket_allow = false;
147            self.aaguids = other.aaguids.clone();
148        } else {
149            // Only keep what is also in other.
150            self.aaguids
151                .retain(|s_aaguid, _| other.aaguids.contains_key(s_aaguid))
152        }
153    }
154
155    fn can_retain(&self) -> bool {
156        // Only retain a CA if it's a blanket allow, or has aaguids remaining.
157        self.blanket_allow || !self.aaguids.is_empty()
158    }
159}
160
161/// A list of AttestationCas and associated options.
162#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
163pub struct AttestationCaList {
164    /// The set of CA's that we trust in this Operation
165    cas: BTreeMap<Base64UrlSafeData, AttestationCa>,
166}
167
168impl TryFrom<&[u8]> for AttestationCaList {
169    type Error = OpenSSLErrorStack;
170
171    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
172        let mut new = Self::default();
173        let att_ca = AttestationCa::new_from_pem(data)?;
174        new.insert(att_ca)?;
175        Ok(new)
176    }
177}
178
179impl AttestationCaList {
180    pub fn cas(&self) -> &BTreeMap<Base64UrlSafeData, AttestationCa> {
181        &self.cas
182    }
183
184    pub fn clear(&mut self) {
185        self.cas.clear()
186    }
187
188    pub fn len(&self) -> usize {
189        self.cas.len()
190    }
191
192    /// Determine if this attestation list contains any members.
193    pub fn is_empty(&self) -> bool {
194        self.cas.is_empty()
195    }
196
197    /// Insert a new att_ca into this Attestation Ca List
198    pub fn insert(
199        &mut self,
200        att_ca: AttestationCa,
201    ) -> Result<Option<AttestationCa>, OpenSSLErrorStack> {
202        // Get the key id (kid, digest).
203        let att_ca_dgst = att_ca.get_kid()?;
204        Ok(self.cas.insert(att_ca_dgst.into(), att_ca))
205    }
206
207    /// Join two CA lists into one, taking all elements from both.
208    pub fn union(&mut self, other: &Self) {
209        for (o_kid, o_att_ca) in other.cas.iter() {
210            if let Some(s_att_ca) = self.cas.get_mut(o_kid) {
211                s_att_ca.union(o_att_ca)
212            } else {
213                self.cas.insert(o_kid.clone(), o_att_ca.clone());
214            }
215        }
216    }
217
218    /// Retain only the CA's and devices that exist in self and other.
219    pub fn intersection(&mut self, other: &Self) {
220        self.cas.retain(|s_kid, s_att_ca| {
221            // First, does this exist in our partner?
222            if let Some(o_att_ca) = other.cas.get(s_kid) {
223                // Now, intersect.
224                s_att_ca.intersection(o_att_ca);
225                if s_att_ca.can_retain() {
226                    // Still as elements, retain.
227                    true
228                } else {
229                    // Nothing remains, remove.
230                    false
231                }
232            } else {
233                // Not in other, remove.
234                false
235            }
236        })
237    }
238}
239
240#[derive(Default)]
241pub struct AttestationCaListBuilder {
242    cas: BTreeMap<Vec<u8>, AttestationCa>,
243}
244
245impl AttestationCaListBuilder {
246    pub fn new() -> Self {
247        Self::default()
248    }
249
250    pub fn insert_device_x509(
251        &mut self,
252        ca: x509::X509,
253        aaguid: Uuid,
254        desc_english: String,
255        desc_localised: BTreeMap<String, String>,
256    ) -> Result<(), OpenSSLErrorStack> {
257        let kid = ca
258            .digest(hash::MessageDigest::sha256())
259            .map(|bytes| bytes.to_vec())?;
260
261        let mut att_ca = if let Some(att_ca) = self.cas.remove(&kid) {
262            att_ca
263        } else {
264            AttestationCa {
265                ca,
266                aaguids: BTreeMap::default(),
267                blanket_allow: false,
268            }
269        };
270
271        att_ca.insert_device(aaguid, desc_english, desc_localised);
272
273        self.cas.insert(kid, att_ca);
274
275        Ok(())
276    }
277
278    pub fn insert_device_der(
279        &mut self,
280        ca_der: &[u8],
281        aaguid: Uuid,
282        desc_english: String,
283        desc_localised: BTreeMap<String, String>,
284    ) -> Result<(), OpenSSLErrorStack> {
285        let ca = x509::X509::from_der(ca_der)?;
286        self.insert_device_x509(ca, aaguid, desc_english, desc_localised)
287    }
288
289    pub fn insert_device_pem(
290        &mut self,
291        ca_pem: &[u8],
292        aaguid: Uuid,
293        desc_english: String,
294        desc_localised: BTreeMap<String, String>,
295    ) -> Result<(), OpenSSLErrorStack> {
296        let ca = x509::X509::from_pem(ca_pem)?;
297        self.insert_device_x509(ca, aaguid, desc_english, desc_localised)
298    }
299
300    pub fn build(self) -> AttestationCaList {
301        let cas = self
302            .cas
303            .into_iter()
304            .map(|(kid, att_ca)| (kid.into(), att_ca))
305            .collect();
306
307        AttestationCaList { cas }
308    }
309}