Skip to content

Commit

Permalink
20231025 attestation ca devices (#390)
Browse files Browse the repository at this point in the history
  • Loading branch information
Firstyear authored Nov 25, 2023
1 parent fd5648d commit 6518c69
Show file tree
Hide file tree
Showing 18 changed files with 554 additions and 319 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ webauthn-rs-core = { path = "./webauthn-rs-core" }
webauthn-rs-proto = { path = "./webauthn-rs-proto" }
webauthn-attestation-ca = { path = "./attestation-ca" }
webauthn-rs-device-catalog = { path = "./device-catalog" }
fido-mds = { path = "./fido-mds" }

async-std = { version = "1.6", features = ["attributes"] }
base64 = "0.21"
Expand Down
207 changes: 117 additions & 90 deletions attestation-ca/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,35 @@ use base64urlsafedata::Base64UrlSafeData;
use openssl::error::ErrorStack as OpenSSLErrorStack;
use openssl::{hash, x509};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;

use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceDescription {
pub(crate) en: String,
pub(crate) localised: BTreeMap<String, String>,
}

impl DeviceDescription {
/// A default description of device.
pub fn description_en(&self) -> &str {
self.en.as_str()
}

/// A map of locale identifiers to a localised description of the device.
/// If the request locale is not found, you should try other user preferenced locales
/// falling back to the default value.
pub fn description_localised(&self) -> &BTreeMap<String, String> {
&self.localised
}
}

/// A serialised Attestation CA.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerialisableAttestationCa {
pub(crate) ca: Base64UrlSafeData,
pub(crate) aaguids: BTreeSet<Uuid>,
pub(crate) aaguids: BTreeMap<Uuid, DeviceDescription>,
}

/// A structure representing an Attestation CA and other options associated to this CA.
Expand All @@ -24,11 +44,11 @@ pub struct SerialisableAttestationCa {
)]
pub struct AttestationCa {
/// The x509 root CA of the attestation chain that a security key will be attested to.
pub ca: x509::X509,
ca: x509::X509,
/// If not empty, the set of acceptable AAGUIDS (Device Ids) that are allowed to be
/// attested as trusted by this CA. AAGUIDS that are not in this set, but signed by
/// this CA will NOT be trusted.
pub aaguids: BTreeSet<Uuid>,
aaguids: BTreeMap<Uuid, DeviceDescription>,
}

#[allow(clippy::from_over_into)]
Expand All @@ -52,42 +72,41 @@ impl TryFrom<SerialisableAttestationCa> for AttestationCa {
}
}

impl TryFrom<&[u8]> for AttestationCa {
type Error = OpenSSLErrorStack;
impl AttestationCa {
pub fn ca(&self) -> &x509::X509 {
&self.ca
}

fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
Ok(AttestationCa {
ca: x509::X509::from_pem(data)?,
aaguids: Default::default(),
})
pub fn aaguids(&self) -> &BTreeMap<Uuid, DeviceDescription> {
&self.aaguids
}
}

impl AttestationCa {
/// Retrieve the Key Identifier for this Attestation Ca
pub fn get_kid(&self) -> Result<Vec<u8>, OpenSSLErrorStack> {
self.ca
.digest(hash::MessageDigest::sha256())
.map(|bytes| bytes.to_vec())
}

/// Update the set of aaguids this Attestation CA allows. If an empty btreeset is provided then
/// this Attestation CA allows all Aaguids.
pub fn set_aaguids(&mut self, aaguids: BTreeSet<Uuid>) {
self.aaguids = aaguids;
}

/// Update the set of aaguids this Attestation CA allows by adding this AAGUID to the allowed
/// set.
pub fn insert_aaguid(&mut self, aaguid: Uuid) {
self.aaguids.insert(aaguid);
fn insert_device(
&mut self,
aaguid: Uuid,
desc_english: String,
desc_localised: BTreeMap<String, String>,
) {
self.aaguids.insert(
aaguid,
DeviceDescription {
en: desc_english,
localised: desc_localised,
},
);
}

/// Create a customised attestation CA from a DER public key.
pub fn new_from_der(data: &[u8]) -> Result<Self, OpenSSLErrorStack> {
fn new_from_pem(data: &[u8]) -> Result<Self, OpenSSLErrorStack> {
Ok(AttestationCa {
ca: x509::X509::from_der(data)?,
aaguids: BTreeSet::default(),
ca: x509::X509::from_pem(data)?,
aaguids: BTreeMap::default(),
})
}
}
Expand All @@ -99,79 +118,17 @@ pub struct AttestationCaList {
pub cas: BTreeMap<Base64UrlSafeData, AttestationCa>,
}

impl TryFrom<AttestationCa> for AttestationCaList {
type Error = OpenSSLErrorStack;

fn try_from(att_ca: AttestationCa) -> Result<Self, Self::Error> {
let mut new = Self::default();
new.insert(att_ca)?;
Ok(new)
}
}

impl TryFrom<&[u8]> for AttestationCaList {
type Error = OpenSSLErrorStack;

fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
let mut new = Self::default();
let att_ca = AttestationCa::try_from(data)?;
let att_ca = AttestationCa::new_from_pem(data)?;
new.insert(att_ca)?;
Ok(new)
}
}

impl TryFrom<&[(&[u8], Uuid)]> for AttestationCaList {
type Error = OpenSSLErrorStack;

fn try_from(iter: &[(&[u8], Uuid)]) -> Result<Self, Self::Error> {
let mut cas = BTreeMap::default();

for (der, aaguid) in iter {
let ca = x509::X509::from_der(der)?;

let kid = ca.digest(hash::MessageDigest::sha256())?;

if !cas.contains_key(kid.as_ref()) {
let mut aaguids = BTreeSet::default();
aaguids.insert(*aaguid);
let att_ca = AttestationCa { ca, aaguids };
cas.insert(kid.to_vec().into(), att_ca);
} else {
let att_ca = cas.get_mut(kid.as_ref()).expect("Can not fail!");
// just add the aaguid
att_ca.aaguids.insert(*aaguid);
};
}

Ok(AttestationCaList { cas })
}
}

impl AttestationCaList {
pub fn from_iter<I: IntoIterator<Item = (x509::X509, Uuid)>>(
iter: I,
) -> Result<Self, OpenSSLErrorStack> {
let mut cas = BTreeMap::default();

for (ca, aaguid) in iter {
let kid = ca.digest(hash::MessageDigest::sha256())?;

if !cas.contains_key(kid.as_ref()) {
let mut aaguids = BTreeSet::default();
aaguids.insert(aaguid);
let att_ca = AttestationCa { ca, aaguids };
cas.insert(kid.to_vec().into(), att_ca);
} else {
let att_ca = cas.get_mut(kid.as_ref()).expect("Can not fail!");
// just add the aaguid
att_ca.aaguids.insert(aaguid);
};
}

Ok(AttestationCaList { cas })
}
}

impl AttestationCaList {
/// Determine if this attestation list contains any members.
pub fn is_empty(&self) -> bool {
Expand All @@ -188,3 +145,73 @@ impl AttestationCaList {
Ok(self.cas.insert(att_ca_dgst.into(), att_ca))
}
}

#[derive(Default)]
pub struct AttestationCaListBuilder {
cas: BTreeMap<Vec<u8>, AttestationCa>,
}

impl AttestationCaListBuilder {
pub fn new() -> Self {
Self::default()
}

pub fn insert_device_x509(
&mut self,
ca: x509::X509,
aaguid: Uuid,
desc_english: String,
desc_localised: BTreeMap<String, String>,
) -> Result<(), OpenSSLErrorStack> {
let kid = ca
.digest(hash::MessageDigest::sha256())
.map(|bytes| bytes.to_vec())?;

let mut att_ca = if let Some(att_ca) = self.cas.remove(&kid) {
att_ca
} else {
AttestationCa {
ca,
aaguids: BTreeMap::default(),
}
};

att_ca.insert_device(aaguid, desc_english, desc_localised);

self.cas.insert(kid, att_ca);

Ok(())
}

pub fn insert_device_der(
&mut self,
ca_der: &[u8],
aaguid: Uuid,
desc_english: String,
desc_localised: BTreeMap<String, String>,
) -> Result<(), OpenSSLErrorStack> {
let ca = x509::X509::from_der(ca_der)?;
self.insert_device_x509(ca, aaguid, desc_english, desc_localised)
}

pub fn insert_device_pem(
&mut self,
ca_pem: &[u8],
aaguid: Uuid,
desc_english: String,
desc_localised: BTreeMap<String, String>,
) -> Result<(), OpenSSLErrorStack> {
let ca = x509::X509::from_pem(ca_pem)?;
self.insert_device_x509(ca, aaguid, desc_english, desc_localised)
}

pub fn build(self) -> AttestationCaList {
let cas = self
.cas
.into_iter()
.map(|(kid, att_ca)| (kid.into(), att_ca))
.collect();

AttestationCaList { cas }
}
}
3 changes: 1 addition & 2 deletions compat_tester/webauthn-rs-demo-shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ edition = "2021"
rust-version = "1.70.0"

[features]
core = ["webauthn-rs-core", "webauthn-rs-device-catalog"]
core = ["webauthn-rs-core"]

[dependencies]
serde.workspace = true

webauthn-rs-core = { workspace = true, optional = true }
webauthn-rs-proto = { workspace = true }
webauthn-rs-device-catalog = { workspace = true, optional = true }
25 changes: 1 addition & 24 deletions compat_tester/webauthn-rs-demo-shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
use serde::{Deserialize, Serialize};
#[cfg(feature = "core")]
use webauthn_rs_core::error::WebauthnError;
#[cfg(feature = "core")]
use webauthn_rs_core::proto::AttestationCaList;

pub use webauthn_rs_proto::{
AttestationConveyancePreference, AuthenticationExtensions, AuthenticatorAttachment,
Expand All @@ -15,34 +13,13 @@ pub use webauthn_rs_proto::{
RequestChallengeResponse, RequestRegistrationExtensions, UserVerificationPolicy,
};

#[cfg(feature = "core")]
use webauthn_rs_device_catalog::Data;

#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum AttestationLevel {
None,
AnyKnown,
AnyKnownFido,
Strict,
}

#[cfg(feature = "core")]
#[allow(clippy::from_over_into)]
impl Into<Option<AttestationCaList>> for AttestationLevel {
fn into(self) -> Option<AttestationCaList> {
match self {
AttestationLevel::None => None,
AttestationLevel::AnyKnown => {
let data = Data::all_known_devices();
(&data).try_into().ok()
}
AttestationLevel::Strict => {
let data = Data::strict();
(&data).try_into().ok()
}
}
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegisterStart {
pub username: String,
Expand Down
4 changes: 2 additions & 2 deletions compat_tester/webauthn-rs-demo-wasm/src/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ impl Demo {
<td>
<select class="form-select" id="strict_attestation_required">
<option selected=true value="n">{ "None" }</option>
<option value="a">{ "Any Known Manufacturer" }</option>
<option value="a">{ "Any Known FIDO Device" }</option>
<option value="s">{ "Strict" }</option>
</select>
</td>
Expand Down Expand Up @@ -635,7 +635,7 @@ impl Component for Demo {
utils::get_select_value_from_element_id("strict_attestation_required")
.and_then(|v| match v.as_str() {
"s" => Some(AttestationLevel::Strict),
"a" => Some(AttestationLevel::AnyKnown),
"a" => Some(AttestationLevel::AnyKnownFido),
_ => None,
})
.unwrap_or(AttestationLevel::None);
Expand Down
7 changes: 6 additions & 1 deletion compat_tester/webauthn-rs-demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ webauthn-rs-core.workspace = true
webauthn-rs = { workspace = true, features = ["conditional-ui", "attestation", "resident-key-support", "danger-allow-state-serialisation"] }

tide.workspace = true
async-std.workspace = true
tokio.workspace = true

structopt = { version = "0.3", default-features = false }
tracing.workspace = true
tracing-subscriber.workspace = true
Expand All @@ -26,6 +27,10 @@ url = { workspace = true , features = ["serde"] }

serde.workspace = true

webauthn-rs-device-catalog = { workspace = true }
fido-mds = { workspace = true }
reqwest = "0.11"

[dependencies.tide-openssl]
git = "https://github.com/victorcwai/tide-openssl.git"
rev = "7d0e2215f2f1ebfa71aa30d132213ed45dd95cbf"
Binary file modified compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm
Binary file not shown.
Loading

0 comments on commit 6518c69

Please sign in to comment.