use ciborium::cbor;
use serde::{Deserialize, Serialize};
use crate::{
Bytes,
ctap2::AuthenticatorData,
utils::serde::{ignore_unknown_opt_vec, ignore_unknown_vec},
webauthn,
};
#[cfg(doc)]
use {
crate::webauthn::{
CollectedClientData, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor,
},
ciborium::value::Value,
};
use super::extensions::{AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs, HmacGetSecretInput};
serde_workaround! {
#[derive(Debug, PartialEq)]
pub struct Request {
#[serde(rename = 0x01)]
pub client_data_hash: Bytes,
#[serde(rename = 0x02)]
pub rp: PublicKeyCredentialRpEntity,
#[serde(rename = 0x03)]
pub user: webauthn::PublicKeyCredentialUserEntity,
#[serde(rename = 0x04, deserialize_with = ignore_unknown_vec)]
pub pub_key_cred_params: Vec<webauthn::PublicKeyCredentialParameters>,
#[serde(
rename = 0x05;
default,
skip_serializing_if = Option::is_none,
deserialize_with = ignore_unknown_opt_vec
)]
pub exclude_list: Option<Vec<webauthn::PublicKeyCredentialDescriptor>>,
#[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)]
pub extensions: Option<ExtensionInputs>,
#[serde(rename = 0x07; default)]
pub options: Options,
#[serde(rename = 0x08; default, skip_serializing_if = Option::is_none)]
pub pin_auth: Option<Bytes>,
#[serde(rename = 0x09; default, skip_serializing_if = Option::is_none)]
pub pin_protocol: Option<u8>,
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicKeyCredentialRpEntity {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicKeyCredentialUserEntity {
pub id: Bytes,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
}
impl From<webauthn::PublicKeyCredentialUserEntity> for PublicKeyCredentialUserEntity {
fn from(value: webauthn::PublicKeyCredentialUserEntity) -> Self {
Self {
id: value.id,
name: Some(value.name),
display_name: Some(value.display_name),
icon_url: None,
}
}
}
impl TryFrom<PublicKeyCredentialUserEntity> for webauthn::PublicKeyCredentialUserEntity {
type Error = &'static str;
fn try_from(value: PublicKeyCredentialUserEntity) -> Result<Self, Self::Error> {
match (value.name, value.display_name) {
(Some(name), Some(display_name)) => Ok(Self {
id: value.id,
name,
display_name,
}),
_ => Err(
"PublicKeyCredentialUserEntity is missing one or more required fields: name, display_name",
),
}
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct MissingRpId {
pub rp_name: String,
}
impl TryFrom<webauthn::PublicKeyCredentialRpEntity> for PublicKeyCredentialRpEntity {
type Error = MissingRpId;
fn try_from(value: webauthn::PublicKeyCredentialRpEntity) -> Result<Self, Self::Error> {
if let Some(id) = value.id {
Ok(Self {
id,
name: Some(value.name),
})
} else {
Err(MissingRpId {
rp_name: value.name,
})
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Options {
#[serde(default)]
pub rk: bool,
#[serde(default = "default_true")]
pub up: bool,
#[serde(default)]
pub uv: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
rk: false,
up: true,
uv: false,
}
}
}
const fn default_true() -> bool {
true
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
pub struct ExtensionInputs {
#[serde(
rename = "hmac-secret",
default,
skip_serializing_if = "Option::is_none"
)]
pub hmac_secret: Option<bool>,
#[serde(
rename = "hmac-secret-mc",
default,
skip_serializing_if = "Option::is_none"
)]
pub hmac_secret_mc: Option<HmacGetSecretInput>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prf: Option<AuthenticatorPrfInputs>,
}
impl ExtensionInputs {
pub fn zip_contents(self) -> Option<Self> {
let Self {
hmac_secret,
hmac_secret_mc,
prf,
} = &self;
let has_hmac_secret = hmac_secret.is_some();
let has_hmac_secret_mc = hmac_secret_mc.is_some();
let has_prf = prf.is_some();
(has_hmac_secret || has_hmac_secret_mc || has_prf).then_some(self)
}
}
serde_workaround! {
#[derive(Debug)]
pub struct Response {
#[serde(rename = 0x01)]
pub fmt: String,
#[serde(rename = 0x02)]
pub auth_data: AuthenticatorData,
#[serde(rename = 0x03)]
pub att_stmt: ciborium::value::Value,
#[serde(rename = 0x04; default, skip_serializing_if = Option::is_none)]
pub ep_att: Option<bool>,
#[serde(rename = 0x05; default, skip_serializing_if = Option::is_none)]
pub large_blob_key: Option<Bytes>,
#[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)]
pub unsigned_extension_outputs: Option<UnsignedExtensionOutputs>,
}
}
impl Response {
pub fn as_webauthn_bytes(&self) -> Bytes {
let mut attestation_object = Vec::with_capacity(128);
let attestation_object_value = cbor!({
"fmt" => "none",
"attStmt" => {},
"authData" => ciborium::value::Value::Bytes(self.auth_data.to_vec()),
})
.unwrap();
ciborium::ser::into_writer(&attestation_object_value, &mut attestation_object).unwrap();
attestation_object.into()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SignedExtensionOutputs {
#[serde(
rename = "hmac-secret",
default,
skip_serializing_if = "Option::is_none"
)]
pub hmac_secret: Option<bool>,
#[serde(
rename = "hmac-secret-mc",
default,
skip_serializing_if = "Option::is_none"
)]
pub hmac_secret_mc: Option<Bytes>,
}
impl SignedExtensionOutputs {
pub fn zip_contents(self) -> Option<Self> {
let Self {
hmac_secret,
hmac_secret_mc,
} = &self;
let has_hmac_secret = hmac_secret.is_some();
let has_hmac_secret_mc = hmac_secret_mc.is_some();
(has_hmac_secret || has_hmac_secret_mc).then_some(self)
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UnsignedExtensionOutputs {
pub prf: Option<AuthenticatorPrfMakeOutputs>,
}
impl UnsignedExtensionOutputs {
pub fn zip_contents(self) -> Option<Self> {
let Self { prf } = &self;
prf.is_some().then_some(self)
}
}
#[cfg(test)]
mod tests;