diff --git a/libwebauthn/examples/webauthn_cable.rs b/libwebauthn/examples/webauthn_cable.rs index bce86a9..c468f56 100644 --- a/libwebauthn/examples/webauthn_cable.rs +++ b/libwebauthn/examples/webauthn_cable.rs @@ -70,7 +70,7 @@ pub async fn main() -> Result<(), Box> { user_verification: UserVerificationRequirement::Preferred, algorithms: vec![Ctap2CredentialType::default()], exclude: None, - extensions_cbor: vec![], + extensions: None, timeout: TIMEOUT, }; @@ -93,13 +93,14 @@ pub async fn main() -> Result<(), Box> { .unwrap(); println!("WebAuthn MakeCredential response: {:?}", response); - let credential: Ctap2PublicKeyCredentialDescriptor = (&response).try_into().unwrap(); + let credential: Ctap2PublicKeyCredentialDescriptor = + (&response.authenticator_data).try_into().unwrap(); let get_assertion = GetAssertionRequest { relying_party_id: "example.org".to_owned(), hash: Vec::from(challenge), allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, - extensions_cbor: None, + extensions: None, timeout: TIMEOUT, }; diff --git a/libwebauthn/examples/webauthn_extensions_hid.rs b/libwebauthn/examples/webauthn_extensions_hid.rs new file mode 100644 index 0000000..f11dc4f --- /dev/null +++ b/libwebauthn/examples/webauthn_extensions_hid.rs @@ -0,0 +1,140 @@ +use std::convert::TryInto; +use std::error::Error; +use std::time::Duration; + +use ctap_types::ctap2::credential_management::CredentialProtectionPolicy; +use rand::{thread_rng, Rng}; +use tracing_subscriber::{self, EnvFilter}; + +use libwebauthn::ops::webauthn::{ + GetAssertionRequest, GetAssertionRequestExtensions, MakeCredentialRequest, + MakeCredentialsRequestExtensions, UserVerificationRequirement, +}; +use libwebauthn::pin::{PinProvider, StdinPromptPinProvider}; +use libwebauthn::proto::ctap2::{ + Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, + Ctap2PublicKeyCredentialUserEntity, +}; +use libwebauthn::transport::hid::list_devices; +use libwebauthn::transport::Device; +use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; + +const TIMEOUT: Duration = Duration::from_secs(10); + +fn setup_logging() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .without_time() + .init(); +} + +#[tokio::main] +pub async fn main() -> Result<(), Box> { + setup_logging(); + + let devices = list_devices().await.unwrap(); + println!("Devices found: {:?}", devices); + + let user_id: [u8; 32] = thread_rng().gen(); + let challenge: [u8; 32] = thread_rng().gen(); + + let pin_provider: Box = Box::new(StdinPromptPinProvider::new()); + + let extensions = MakeCredentialsRequestExtensions { + cred_protect: Some(CredentialProtectionPolicy::Required), + cred_blob: Some(r"My own little blob".into()), + large_blob_key: None, + min_pin_length: Some(true), + hmac_secret: Some(true), + }; + + for mut device in devices { + println!("Selected HID authenticator: {}", &device); + device.wink(TIMEOUT).await?; + + let mut channel = device.channel().await?; + + // Make Credentials ceremony + let make_credentials_request = MakeCredentialRequest { + origin: "example.org".to_owned(), + hash: Vec::from(challenge), + relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), + user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), + require_resident_key: true, + user_verification: UserVerificationRequirement::Preferred, + algorithms: vec![Ctap2CredentialType::default()], + exclude: None, + extensions: Some(extensions.clone()), + timeout: TIMEOUT, + }; + + let response = loop { + match channel + .webauthn_make_credential(&make_credentials_request, &pin_provider) + .await + { + Ok(response) => break Ok(response), + Err(WebAuthnError::Ctap(ctap_error)) => { + if ctap_error.is_retryable_user_error() { + println!("Oops, try again! Error: {}", ctap_error); + continue; + } + break Err(WebAuthnError::Ctap(ctap_error)); + } + Err(err) => break Err(err), + }; + } + .unwrap(); + // println!("WebAuthn MakeCredential response: {:?}", response); + println!( + "WebAuthn MakeCredential extensions: {:?}", + response.authenticator_data.extensions + ); + + let credential: Ctap2PublicKeyCredentialDescriptor = + (&response.authenticator_data).try_into().unwrap(); + let get_assertion = GetAssertionRequest { + relying_party_id: "example.org".to_owned(), + hash: Vec::from(challenge), + allow: vec![credential], + user_verification: UserVerificationRequirement::Discouraged, + extensions: Some(GetAssertionRequestExtensions { + cred_blob: Some(true), + }), + timeout: TIMEOUT, + }; + + let response = loop { + match channel + .webauthn_get_assertion(&get_assertion, &pin_provider) + .await + { + Ok(response) => break Ok(response), + Err(WebAuthnError::Ctap(ctap_error)) => { + if ctap_error.is_retryable_user_error() { + println!("Oops, try again! Error: {}", ctap_error); + continue; + } + break Err(WebAuthnError::Ctap(ctap_error)); + } + Err(err) => break Err(err), + }; + } + .unwrap(); + // println!("WebAuthn GetAssertion response: {:?}", response); + println!( + "WebAuthn GetAssertion extensions: {:?}", + response.assertions[0].authenticator_data.extensions + ); + let blob = if let Some(ext) = &response.assertions[0].authenticator_data.extensions { + ext.cred_blob + .clone() + .map(|x| String::from_utf8_lossy(&x).to_string()) + } else { + None + }; + println!("Credential blob: {blob:?}"); + } + + Ok(()) +} diff --git a/libwebauthn/examples/webauthn_hid.rs b/libwebauthn/examples/webauthn_hid.rs index c4c3675..0924472 100644 --- a/libwebauthn/examples/webauthn_hid.rs +++ b/libwebauthn/examples/webauthn_hid.rs @@ -54,7 +54,7 @@ pub async fn main() -> Result<(), Box> { user_verification: UserVerificationRequirement::Preferred, algorithms: vec![Ctap2CredentialType::default()], exclude: None, - extensions_cbor: vec![], + extensions: None, timeout: TIMEOUT, }; @@ -77,13 +77,14 @@ pub async fn main() -> Result<(), Box> { .unwrap(); println!("WebAuthn MakeCredential response: {:?}", response); - let credential: Ctap2PublicKeyCredentialDescriptor = (&response).try_into().unwrap(); + let credential: Ctap2PublicKeyCredentialDescriptor = + (&response.authenticator_data).try_into().unwrap(); let get_assertion = GetAssertionRequest { relying_party_id: "example.org".to_owned(), hash: Vec::from(challenge), allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, - extensions_cbor: None, + extensions: None, timeout: TIMEOUT, }; diff --git a/libwebauthn/src/fido.rs b/libwebauthn/src/fido.rs index 8450837..f27c84d 100644 --- a/libwebauthn/src/fido.rs +++ b/libwebauthn/src/fido.rs @@ -1,3 +1,23 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use ctap_types::cose::PublicKey; +use serde::{ + de::{DeserializeOwned, Error as DesError, Visitor}, + ser::Error as SerError, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use std::{ + fmt, + io::{Cursor, Read}, + marker::PhantomData, +}; +use tracing::warn; + +use crate::proto::{ + ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}, + CtapError, +}; + #[derive(Debug, PartialEq, Eq)] pub enum FidoProtocol { FIDO2, @@ -21,8 +41,8 @@ impl From for FidoProtocol { } } -/* bitflags! { + #[derive(Debug, Clone)] pub struct AuthenticatorDataFlags: u8 { const USER_PRESENT = 0x01; const RFU_1 = 0x02; @@ -30,26 +50,190 @@ bitflags! { const RFU_2_1 = 0x08; const RFU_2_2 = 0x10; const RFU_2_3 = 0x20; - const ATTESTED_CREDENTIALS = 0x30; - const EXTENSION_DATA = 0x40; + const ATTESTED_CREDENTIALS = 0x40; + const EXTENSION_DATA = 0x80; } } -#[derive(Debug, Copy)] +#[derive(Debug, Clone)] pub struct AttestedCredentialData { - pub raw: Vec, - pub aaguid: Vec, + pub aaguid: [u8; 16], pub credential_id: Vec, - pub credential_public_key: Vec, + pub credential_public_key: PublicKey, +} + +impl Serialize for AttestedCredentialData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Name | Length + // -------------------------------- + // aaguid | 16 + // credentialIdLenght | 2 + // credentialId | L + // credentialPublicKey | variable + let mut res = self.aaguid.to_vec(); + res.write_u16::(self.credential_id.len() as u16) + .map_err(SerError::custom)?; + res.extend(&self.credential_id); + let cose_encoded_public_key = + serde_cbor::to_vec(&self.credential_public_key).map_err(SerError::custom)?; + res.extend(cose_encoded_public_key); + serializer.serialize_bytes(&res) + } +} + +impl From<&AttestedCredentialData> for Ctap2PublicKeyCredentialDescriptor { + fn from(data: &AttestedCredentialData) -> Self { + Self { + r#type: Ctap2PublicKeyCredentialType::PublicKey, + id: ByteBuf::from(data.credential_id.clone()), + transports: None, + } + } } -#[derive(Debug, Copy)] -pub struct AuthenticatorData { - pub raw: Vec, - pub relying_party_id: Vec, +#[derive(Debug, Clone)] +pub struct AuthenticatorData { + pub rp_id_hash: [u8; 32], pub flags: AuthenticatorDataFlags, pub signature_count: u32, pub attested_credential: Option, - pub extensions_cbor: Option>, + pub extensions: Option, +} + +impl Serialize for AuthenticatorData +where + T: Clone + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Name | Length + // ----------------------------------- + // rpIdHash | 32 + // flags | 1 + // signCount | 4 + // attestedCredentialData | variable + // extensions | variable + let mut res = self.rp_id_hash.to_vec(); + res.push(self.flags.bits()); + res.write_u32::(self.signature_count) + .map_err(SerError::custom)?; + if let Some(att_data) = &self.attested_credential { + res.extend(serde_cbor::to_vec(att_data).map_err(SerError::custom)?); + } + if let Some(extensions) = &self.extensions { + res.extend(serde_cbor::to_vec(extensions).map_err(SerError::custom)?); + } + serializer.serialize_bytes(&res) + } +} + +impl TryFrom<&AuthenticatorData> for Ctap2PublicKeyCredentialDescriptor { + type Error = CtapError; + + fn try_from(data: &AuthenticatorData) -> Result { + if let Some(att_data) = &data.attested_credential { + Ok(att_data.into()) + } else { + warn!("Failed to parse credential ID: invalid authenticator data length"); + Err(CtapError::InvalidCredential) + } + } +} + +impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // This is a bit ugly. The Visitor needs _something_ of type T (which is Deserialize), + // for the compiler to grok this. So we have to add PhantomData of type T here, in + // order for us to be able to specify "type Value = AuthenticatorData" + struct AuthenticatorDataVisitor(PhantomData); + + impl<'de, T: DeserializeOwned> Visitor<'de> for AuthenticatorDataVisitor { + type Value = AuthenticatorData; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("ByteBuf: Authenticator data") + } + + fn visit_bytes(self, data: &[u8]) -> Result + where + E: DesError, + { + // Name | Length | Start index + // --------------------------------------------------- + // rpIdHash | 32 | 0 + // flags | 1 | 32 + // signCount | 4 | 33 + // attestedCredentialData | variable | + // aaguid | 16 | 37 + // credentialIdLenght | 2 | 53 + // credentialId | L | 55 + // credentialPublicKey | variable | + // extensions | variable | variable + + // -> 32 + 1 + 4 = 37 + if data.len() < 37 { + return Err(DesError::invalid_length(data.len(), &"37")); + } + + let mut cursor = Cursor::new(&data); + let mut rp_id_hash = [0u8; 32]; + cursor.read_exact(&mut rp_id_hash).unwrap(); // We checked the length + let flags_raw = cursor.read_u8().unwrap(); // We checked the length + let flags = AuthenticatorDataFlags::from_bits_truncate(flags_raw); + let signature_count = cursor.read_u32::().unwrap(); // We checked the length + + let mut attested_credential = None; + if flags.contains(AuthenticatorDataFlags::ATTESTED_CREDENTIALS) { + // -> 32 + 1 + 4 + 16 + 2 + X = 55 + if data.len() < 55 { + return Err(DesError::invalid_length(data.len(), &"55")); + } + + let mut aaguid = [0u8; 16]; + cursor.read_exact(&mut aaguid).unwrap(); // We checked the length + let credential_id_len = cursor.read_u16::().unwrap() as usize; // We checked the length + if data.len() < 55 + credential_id_len { + return Err(DesError::invalid_length(data.len(), &"55+L")); + } + let mut credential_id = vec![0u8; credential_id_len]; + cursor.read_exact(&mut credential_id).unwrap(); // We checked the length + + let mut deserializer = serde_cbor::Deserializer::from_reader(&mut cursor); + let credential_public_key: PublicKey = + Deserialize::deserialize(&mut deserializer).map_err(DesError::custom)?; + + attested_credential = Some(AttestedCredentialData { + aaguid, + credential_id, + credential_public_key, + }); + } + + let extensions: Option = + if flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) { + serde_cbor::from_reader(&mut cursor).map_err(DesError::custom)? + } else { + Default::default() + }; + + Ok(AuthenticatorData { + rp_id_hash, + flags, + signature_count, + attested_credential, + extensions, + }) + } + } + + deserializer.deserialize_bytes(AuthenticatorDataVisitor(PhantomData)) + } } -*/ diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index 4612a18..c006a60 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use byteorder::{BigEndian, WriteBytesExt}; use ctap_types::cose; use serde_bytes::ByteBuf; use serde_cbor::to_vec; @@ -9,6 +8,7 @@ use tracing::{error, trace}; use x509_parser::nom::AsBytes; use super::webauthn::MakeCredentialRequest; +use crate::fido::{AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags}; use crate::ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}; use crate::proto::ctap1::{Ctap1RegisterRequest, Ctap1SignRequest}; use crate::proto::ctap1::{Ctap1RegisterResponse, Ctap1SignResponse}; @@ -51,11 +51,6 @@ impl UpgradableResponse for Regis &self, request: &MakeCredentialRequest, ) -> Result { - // Initialize attestedCredData: - // Let credentialIdLength be a 2-byte unsigned big-endian integer representing length of the Credential ID - // initialized with CTAP1/U2F response key handle length. - let credential_id_len: usize = self.key_handle.len(); - // Let x9encodedUserPublicKeybe the user public key returned in the U2F registration response message [U2FRawMsgs]. // Let coseEncodedCredentialPublicKey be the result of converting x9encodedUserPublicKey’s value // from ANS X9.62 / Sec-1 v2 uncompressed curve point representation [SEC1V2] @@ -94,21 +89,20 @@ impl UpgradableResponse for Regis // credentialIdLength Credential ID. Initialized with credentialId bytes. // 77 The credential public key. Initialized with coseEncodedCredentialPublicKey bytes. - let mut attested_cred_data = Vec::new(); - attested_cred_data.extend(&[0u8; 16]); // aaguid zeros - attested_cred_data - .write_u16::(credential_id_len as u16) - .unwrap(); - attested_cred_data.extend(&self.key_handle); - attested_cred_data.extend(cose_encoded_public_key); + let attested_cred_data = AttestedCredentialData { + aaguid: [0u8; 16], // aaguid zeros + credential_id: self.key_handle.clone(), + credential_public_key: cose_public_key, + }; // Initialize authenticatorData: // Let flags be a byte whose zeroth bit (bit 0, UP) is set, and whose sixth bit (bit 6, AT) is set, // and all other bits are zero (bit zero is the least significant bit) - let flags: u8 = 0b00000001 | 0b01000000; + let flags = + AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED_CREDENTIALS; // Let signCount be a 4-byte unsigned integer initialized to zero. - let sign_count: [u8; 4] = [0u8; 4]; + let signature_count: u32 = 0; // Let authenticatorData be a byte string with the following structure: // @@ -118,14 +112,16 @@ impl UpgradableResponse for Regis // 1 Flags Initialized with flags' value. // 4 Signature counter (signCount). Initialized with signCount bytes. // Variable Length Attested credential data. Initialized with attestedCredData’s value. - let mut auth_data = Vec::new(); let mut hasher = Sha256::default(); hasher.update(request.relying_party.id.as_bytes()); - let rp_id_hash = hasher.finalize().to_vec(); - auth_data.extend(rp_id_hash); - auth_data.extend(&[flags]); - auth_data.extend(&sign_count); - auth_data.extend(&attested_cred_data); + let rp_id_hash = hasher.finalize().into(); + let authenticator_data = AuthenticatorData { + rp_id_hash, + flags, + signature_count, + attested_credential: Some(attested_cred_data), + extensions: None, + }; // Let attestationStatement be a CBOR map (see "attStmtTemplate" in Generating an Attestation Object [WebAuthn]) // with the following keys, whose values are as follows: @@ -146,8 +142,8 @@ impl UpgradableResponse for Regis // * Set "attStmt" to attestationStatement. Ok(Ctap2MakeCredentialResponse { format: String::from("fido-u2f"), - authenticator_data: ByteBuf::from(auth_data), - attestation_statement: attestation_statement, + authenticator_data, + attestation_statement, enterprise_attestation: None, large_blob_key: None, unsigned_extension_output: None, @@ -162,12 +158,12 @@ impl UpgradableResponse for SignResponse { // Copy bits 0 (the UP bit) and bit 1 from the CTAP2/U2F response user presence byte to bits 0 and 1 of the // CTAP2 flags, respectively. Set all other bits of flags to zero. Note: bit zero is the least significant bit. // See also Authenticator Data section of [WebAuthn]. - let mut flags: u8 = 0; - flags |= 0b00000001; // up always set - // bit 1 is unused, ignoring + // up always set + // bit 1 is unused, ignoring + let flags = AuthenticatorDataFlags::USER_PRESENT; // Let signCount be a 4-byte unsigned integer initialized with CTAP1/U2F response counter field. - let sign_count = self.counter; + let signature_count = self.counter; // Let authenticatorData is a byte string of following structure: // Length (in bytes) Description Value @@ -175,10 +171,13 @@ impl UpgradableResponse for SignResponse { // 32 SHA-256 hash of the rp.id. Initialized with rpIdHash bytes. // 1 Flags Initialized with flags' value. // 4 Signature counter (signCount) Initialized with signCount bytes. - let mut auth_data = Vec::new(); - auth_data.extend(&request.app_id_hash); - auth_data.extend(&[flags]); - auth_data.write_u32::(sign_count).unwrap(); + let authenticator_data = AuthenticatorData { + rp_id_hash: request.app_id_hash.clone().try_into().unwrap(), + flags, + signature_count, + attested_credential: None, + extensions: None, + }; // Let authenticatorGetAssertionResponse be a CBOR map with the following keys whose values are as follows: [..] let upgraded_response: GetAssertionResponse = Ctap2GetAssertionResponse { @@ -187,7 +186,7 @@ impl UpgradableResponse for SignResponse { id: ByteBuf::from(request.key_handle.clone()), transports: None, }), - authenticator_data: ByteBuf::from(auth_data), + authenticator_data, signature: ByteBuf::from(self.signature.clone()), user: None, credentials_count: None, diff --git a/libwebauthn/src/ops/webauthn.rs b/libwebauthn/src/ops/webauthn.rs index 3c3d4c1..834f63e 100644 --- a/libwebauthn/src/ops/webauthn.rs +++ b/libwebauthn/src/ops/webauthn.rs @@ -1,5 +1,7 @@ use std::time::Duration; +use ctap_types::ctap2::credential_management::CredentialProtectionPolicy; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tracing::{debug, instrument, trace}; @@ -61,18 +63,58 @@ pub struct MakeCredentialRequest { /// excludeCredentialDescriptorList pub exclude: Option>, /// extensions - pub extensions_cbor: Vec, + pub extensions: Option, pub timeout: Duration, } -#[derive(Debug, Clone)] -pub struct GetAssertionRequest { - pub relying_party_id: String, - pub hash: Vec, - pub allow: Vec, - pub extensions_cbor: Option>, - pub user_verification: UserVerificationRequirement, - pub timeout: Duration, +#[derive(Debug, Default, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MakeCredentialsRequestExtensions { + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + #[serde(skip_serializing_if = "Option::is_none", with = "serde_bytes")] + pub cred_blob: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, + // Thanks, FIDO-spec for this consistent naming scheme... + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, +} + +impl MakeCredentialsRequestExtensions { + pub fn skip_serializing(&self) -> bool { + self.cred_protect.is_none() + && self.cred_blob.is_none() + && self.large_blob_key.is_none() + && self.min_pin_length.is_none() + && self.hmac_secret.is_none() + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MakeCredentialsResponseExtensions { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + // If storing credBlob was successful + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, + // No output provided for largeBlobKey in MakeCredential requests + // pub large_blob_key: Option, + + // Current min PIN lenght + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, + + // Thanks, FIDO-spec for this consistent naming scheme... + #[serde( + rename = "hmac-secret", + default, + skip_serializing_if = "Option::is_none" + )] + pub hmac_secret: Option, } impl MakeCredentialRequest { @@ -84,7 +126,7 @@ impl MakeCredentialRequest { user: Ctap2PublicKeyCredentialUserEntity::dummy(), algorithms: vec![Ctap2CredentialType::default()], exclude: None, - extensions_cbor: vec![], + extensions: None, origin: "example.org".to_owned(), require_resident_key: false, user_verification: UserVerificationRequirement::Preferred, @@ -93,6 +135,50 @@ impl MakeCredentialRequest { } } +#[derive(Debug, Clone)] +pub struct GetAssertionRequest { + pub relying_party_id: String, + pub hash: Vec, + pub allow: Vec, + pub extensions: Option, + pub user_verification: UserVerificationRequirement, + pub timeout: Duration, +} + +#[derive(Debug, Default, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAssertionRequestExtensions { + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, + // Thanks, FIDO-spec for this consistent naming scheme... + // #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + // TODO: Do this properly with the salts + // pub hmac_secret: Option>, +} + +impl GetAssertionRequestExtensions { + pub fn skip_serializing(&self) -> bool { + self.cred_blob.is_none() /* && self.hmac_secret.is_none() */ + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAssertionResponseExtensions { + // Stored credBlob + #[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")] + pub cred_blob: Option>, + + // Thanks, FIDO-spec for this consistent naming scheme... + #[serde( + rename = "hmac-secret", + default, + skip_serializing_if = "Option::is_none", + with = "serde_bytes" + )] + pub hmac_secret: Option>, +} + #[derive(Debug, Clone)] pub struct GetAssertionResponse { pub assertions: Vec, diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index 57bdadf..91ef1d1 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -37,9 +37,6 @@ pub use credential_management::{ Ctap2CredentialManagementResponse, Ctap2RPData, }; -// 32 (rpIdHash) + 1 (flags) + 4 (signCount) + 16 (aaguid -const AUTHENTICATOR_DATA_PUBLIC_KEY_OFFSET: usize = 53; - #[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq, Serialize_repr)] #[repr(u8)] pub enum Ctap2CommandCode { diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 2ebea01..29ace17 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -1,4 +1,10 @@ -use crate::{ops::webauthn::GetAssertionRequest, pin::PinUvAuthProtocol}; +use crate::{ + fido::AuthenticatorData, + ops::webauthn::{ + GetAssertionRequest, GetAssertionRequestExtensions, GetAssertionResponseExtensions, + }, + pin::PinUvAuthProtocol, +}; use super::{ Ctap2AuthTokenPermissionRole, Ctap2COSEAlgorithmIdentifier, Ctap2GetInfoResponse, @@ -104,8 +110,8 @@ pub struct Ctap2GetAssertionRequest { pub allow: Vec, /// extensions (0x04) - #[serde(skip_serializing_if = "Option::is_none")] - pub extensions_cbor: Option>, + #[serde(skip_serializing_if = "Self::skip_serializing_extensions")] + pub extensions: Option, /// options (0x05) #[serde(skip_serializing_if = "Option::is_none")] @@ -120,13 +126,21 @@ pub struct Ctap2GetAssertionRequest { pub pin_auth_proto: Option, } +impl Ctap2GetAssertionRequest { + pub fn skip_serializing_extensions(extensions: &Option) -> bool { + extensions + .as_ref() + .map_or(true, |extensions| extensions.skip_serializing()) + } +} + impl From<&GetAssertionRequest> for Ctap2GetAssertionRequest { fn from(op: &GetAssertionRequest) -> Self { Self { relying_party_id: op.relying_party_id.clone(), client_data_hash: ByteBuf::from(op.hash.clone()), allow: op.allow.clone(), - extensions_cbor: op.extensions_cbor.clone(), + extensions: op.extensions.clone(), options: Some(Ctap2GetAssertionOptions { require_user_presence: true, require_user_verification: op.user_verification.is_required(), @@ -143,7 +157,7 @@ pub struct Ctap2GetAssertionResponse { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub credential_id: Option, - pub authenticator_data: ByteBuf, + pub authenticator_data: AuthenticatorData, pub signature: ByteBuf, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index 8b826bf..747821b 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -4,21 +4,17 @@ use super::{ Ctap2PublicKeyCredentialUserEntity, Ctap2UserVerifiableRequest, }; use crate::{ - ops::webauthn::MakeCredentialRequest, - pin::PinUvAuthProtocol, - proto::{ - ctap2::{model::AUTHENTICATOR_DATA_PUBLIC_KEY_OFFSET, Ctap2PublicKeyCredentialType}, - CtapError, + fido::AuthenticatorData, + ops::webauthn::{ + MakeCredentialRequest, MakeCredentialsRequestExtensions, MakeCredentialsResponseExtensions, }, + pin::PinUvAuthProtocol, }; -use byteorder::{BigEndian, ReadBytesExt}; use serde::Serialize; use serde_bytes::ByteBuf; use serde_cbor::Value; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; use std::collections::BTreeMap; -use std::io::Cursor as IOCursor; -use tracing::warn; #[derive(Debug, Clone, Copy, Serialize)] pub struct Ctap2MakeCredentialOptions { @@ -67,8 +63,8 @@ pub struct Ctap2MakeCredentialRequest { pub exclude: Option>, /// extensions (0x06) - #[serde(skip_serializing_if = "Option::is_none")] - pub extensions_cbor: Option>, + #[serde(skip_serializing_if = "Self::skip_serializing_extensions")] + pub extensions: Option, /// options (0x07) #[serde(skip_serializing_if = "Self::skip_serializing_options")] @@ -91,6 +87,14 @@ impl Ctap2MakeCredentialRequest { pub fn skip_serializing_options(options: &Option) -> bool { options.map_or(true, |options| options.skip_serializing()) } + + pub fn skip_serializing_extensions( + extensions: &Option, + ) -> bool { + extensions + .as_ref() + .map_or(true, |extensions| extensions.skip_serializing()) + } } impl From<&MakeCredentialRequest> for Ctap2MakeCredentialRequest { @@ -101,11 +105,7 @@ impl From<&MakeCredentialRequest> for Ctap2MakeCredentialRequest { user: op.user.clone(), algorithms: op.algorithms.clone(), exclude: op.exclude.clone(), - extensions_cbor: if op.extensions_cbor.is_empty() { - None - } else { - Some(op.extensions_cbor.clone()) - }, + extensions: op.extensions.clone(), options: Some(Ctap2MakeCredentialOptions { require_resident_key: if op.require_resident_key { Some(true) @@ -125,7 +125,7 @@ impl From<&MakeCredentialRequest> for Ctap2MakeCredentialRequest { #[serde_indexed(offset = 1)] pub struct Ctap2MakeCredentialResponse { pub format: String, - pub authenticator_data: ByteBuf, + pub authenticator_data: AuthenticatorData, pub attestation_statement: Ctap2AttestationStatement, #[serde(skip_serializing_if = "Option::is_none")] @@ -180,30 +180,3 @@ impl Ctap2UserVerifiableRequest for Ctap2MakeCredentialRequest { // No-op } } - -impl TryFrom<&Ctap2MakeCredentialResponse> for Ctap2PublicKeyCredentialDescriptor { - type Error = CtapError; - fn try_from(response: &Ctap2MakeCredentialResponse) -> Result { - if response.authenticator_data.len() < AUTHENTICATOR_DATA_PUBLIC_KEY_OFFSET + 2 { - warn!("Failed to parse credential ID: invalid authenticator data length"); - return Err(CtapError::InvalidCredential); - } - - let mut cursor = IOCursor::new(response.authenticator_data.as_ref()); - cursor.set_position(AUTHENTICATOR_DATA_PUBLIC_KEY_OFFSET as u64); - let len = cursor.read_u16::().unwrap() as usize; - let offset = AUTHENTICATOR_DATA_PUBLIC_KEY_OFFSET + 2; - if response.authenticator_data.len() < offset + len { - warn!("Failed to parse credential ID: not enough bytes"); - return Err(CtapError::InvalidCredential); - } - - let credential_id = response.authenticator_data[offset..offset + len].to_vec(); - assert_eq!(len, credential_id.len()); - Ok(Ctap2PublicKeyCredentialDescriptor { - r#type: Ctap2PublicKeyCredentialType::PublicKey, - id: ByteBuf::from(credential_id), - transports: None, - }) - } -}