From 82aefc6b988ab4d0012b281de4e0e3e181b49419 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Fri, 26 Dec 2025 18:04:35 -0500 Subject: [PATCH] Use libwebauthn for JSON response serialization This commit migrates from custom JSON response serialization to libwebauthn's WebAuthnIDLResponse::to_inner_model() for both create credential (MakeCredential) and get credential (GetAssertion) responses. Changes: - Use libwebauthn's to_inner_model() to serialize responses, then modify the result to add transport and authenticator_attachment information that is known at the credential service level - Remove create_credential_request_try_into_ctap2's client_data_json return value (now extracted from the request by libwebauthn) - Remove get_credential_request_try_into_ctap2's client_data_json return value - Update gateway.rs to clone the request for response serialization - Remove unused modules: cbor.rs, cose.rs, serde/mod.rs - Simplify webauthn.rs to just re-exports from libwebauthn This removes ~800 lines of custom serialization code including: - CreatePublicKeyCredentialResponse and GetPublicKeyCredentialResponse - AttestationStatement enum and create_attestation_object function - All the extension output types (CredentialPropertiesOutput, etc.) - Custom CBOR writer for attestation object serialization - COSE key type helpers The response serialization now uses libwebauthn's implementation which: - Handles attestation object CBOR encoding - Handles all extension output serialization - Handles base64url encoding of binary fields - Produces WebAuthn Level 3 compliant JSON responses --- credentialsd/src/cbor.rs | 230 ----------------- credentialsd/src/cose.rs | 82 ------ credentialsd/src/dbus/gateway.rs | 48 ++-- credentialsd/src/dbus/model.rs | 122 +++------ credentialsd/src/main.rs | 3 - credentialsd/src/serde/mod.rs | 27 -- credentialsd/src/webauthn.rs | 412 +------------------------------ 7 files changed, 69 insertions(+), 855 deletions(-) delete mode 100644 credentialsd/src/cbor.rs delete mode 100644 credentialsd/src/cose.rs delete mode 100644 credentialsd/src/serde/mod.rs diff --git a/credentialsd/src/cbor.rs b/credentialsd/src/cbor.rs deleted file mode 100644 index 3c3eef6..0000000 --- a/credentialsd/src/cbor.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::convert::TryInto; -use std::io::{Error, Write}; - -pub(crate) struct CborWriter<'a, W> { - writer: &'a mut W, -} - -impl CborWriter<'_, W> -where - W: Write, -{ - pub fn new(writer: &'_ mut W) -> CborWriter<'_, W> { - CborWriter { writer } - } - - pub fn write_bytes(&mut self, data: T) -> Result<(), Error> - where - T: AsRef<[u8]>, - { - self.write_cbor_value( - MajorType::ByteString, - data.as_ref().len().try_into().unwrap(), - Some(data.as_ref()), - )?; - Ok(()) - } - - pub fn write_number(&mut self, num: i128) -> Result<(), Error> { - const POSITIVE_INTEGER_MASK: u8 = 0b000_00000; - const NEGATIVE_INTEGER_MASK: u8 = 0b001_00000; - let (mask, num) = if num >= 0 { - (POSITIVE_INTEGER_MASK, num as u64) - } else { - (NEGATIVE_INTEGER_MASK, (-num - 1) as u64) - }; - if num < 24 { - let d: u8 = num.try_into().unwrap(); - self.writer.write_all(&[mask | d])?; - Ok(()) - } else if num < 2u64.pow(8) { - let d: u8 = num.try_into().unwrap(); - self.writer.write_all(&[mask | 24])?; - self.writer.write_all(&d.to_be_bytes())?; - Ok(()) - } else if num < 2u64.pow(16) { - let d: u16 = num.try_into().unwrap(); - self.writer.write_all(&[mask | 25])?; - self.writer.write_all(&d.to_be_bytes())?; - Ok(()) - } else if num < 2u64.pow(32) { - let d: u32 = num.try_into().unwrap(); - self.writer.write_all(&[mask | 26])?; - self.writer.write_all(&d.to_be_bytes())?; - Ok(()) - } else if num < 2u64.pow(64) { - let d: u64 = num; - self.writer.write_all(&[mask | 27])?; - self.writer.write_all(&d.to_be_bytes())?; - Ok(()) - } else { - Err(Error::new( - std::io::ErrorKind::InvalidInput, - "value too large".to_string(), - )) - } - } - - pub fn write_map_start(&mut self, len: usize) -> Result<(), Error> { - self.write_cbor_value(MajorType::Map, len as u64, None)?; - Ok(()) - } - - pub fn write_array_start(&mut self, len: usize) -> Result<(), Error> { - self.write_cbor_value(MajorType::Array, len as u64, None)?; - Ok(()) - } - - pub fn write_text(&mut self, text: &str) -> Result<(), Error> { - let data = text.as_bytes(); - self.write_cbor_value( - MajorType::TextString, - data.len().try_into().unwrap(), - Some(data), - )?; - Ok(()) - } - - fn write_cbor_value( - &mut self, - major_type: MajorType, - len: u64, - data: Option<&[u8]>, - ) -> Result<(), Error> { - let major_type_mask = match major_type { - MajorType::PositiveInteger => 0b000_00000, - MajorType::NegativeInteger => 0b001_00000, - MajorType::ByteString => 0b010_00000, - MajorType::TextString => 0b011_00000, - MajorType::Array => 0b100_00000, - MajorType::Map => 0b101_00000, - MajorType::Tag => 0b110_00000, - MajorType::Float => 0b111_00000, - }; - - let mut major_type_buf = [0; 9]; - if len < 24 { - let l: u8 = len.try_into().unwrap(); - self.writer.write_all(&[l | major_type_mask])?; - } else if len < 2u64.pow(8) { - let l: u8 = len.try_into().unwrap(); - major_type_buf[0] = 24u8 | major_type_mask; - major_type_buf[1..2].copy_from_slice(&l.to_be_bytes()); - self.writer.write_all(&major_type_buf[0..2])?; - } else if len < 2u64.pow(16) { - let l: u16 = len.try_into().unwrap(); - major_type_buf[0] = 25u8 | major_type_mask; - major_type_buf[1..3].copy_from_slice(&l.to_be_bytes()); - self.writer.write_all(&major_type_buf[0..3])?; - } else if len < 2u64.pow(32) { - let l: u32 = len.try_into().unwrap(); - major_type_buf[0] = 26u8 | major_type_mask; - major_type_buf[1..5].copy_from_slice(&l.to_be_bytes()); - self.writer.write_all(&major_type_buf[0..5])?; - } else if len < 2u64.pow(64) { - let l: u64 = len; - major_type_buf[0] = 27u8 | major_type_mask; - major_type_buf[1..9].copy_from_slice(&l.to_be_bytes()); - self.writer.write_all(&major_type_buf[0..9])?; - } else { - return Err(Error::new( - std::io::ErrorKind::Unsupported, - "Value too large".to_string(), - )); - } - if let Some(data) = data { - self.writer.write_all(data)?; - } - Ok(()) - } -} - -#[allow(dead_code)] -enum MajorType { - PositiveInteger, - NegativeInteger, - ByteString, - TextString, - Array, - Map, - Tag, - Float, -} - -#[cfg(test)] -mod tests { - use super::CborWriter; - - #[test] - fn write_bytes() { - let mut buf: Vec = Vec::with_capacity(16); - let mut cbor_writer = CborWriter::new(&mut buf); - let data: &[u8] = &[0x01, 0x23, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xff]; - cbor_writer.write_bytes(data).unwrap(); - assert_eq!( - buf, - &[ - 0b010_01001, - 0x01, - 0x23, - 0x34, - 0x56, - 0x78, - 0x9a, - 0xbc, - 0xde, - 0xff - ] - ); - } - - #[test] - fn write_bytes_over24() { - let mut buf: Vec = Vec::new(); - let mut cbor_writer = CborWriter::new(&mut buf); - let data = vec![0; 32]; - cbor_writer.write_bytes(data.clone()).unwrap(); - assert_eq!(&buf[0..2], &[0b010_11000, 32u8]); - assert_eq!(&buf[2..34], &data); - } - - #[test] - fn write_uint() { - let mut buf: Vec = Vec::with_capacity(16); - let mut cbor_writer = CborWriter::new(&mut buf); - cbor_writer.write_number(22_i128).unwrap(); - assert_eq!(buf, &[0b000_10110]); - } - - #[test] - fn write_number_u8() { - let mut buf: Vec = Vec::with_capacity(16); - let mut cbor_writer = CborWriter::new(&mut buf); - cbor_writer.write_number(500_i128).unwrap(); - assert_eq!(buf, &[0b000_11001, 0x01, 0xf4]); - } - - #[test] - fn write_negative_number() { - let mut buf: Vec = Vec::with_capacity(16); - let mut cbor_writer = CborWriter::new(&mut buf); - cbor_writer.write_number(-22_i128).unwrap(); - assert_eq!(buf, &[0b001_10101]); - } - - #[test] - fn write_negative_number_u8() { - let mut buf: Vec = Vec::with_capacity(16); - let mut cbor_writer = CborWriter::new(&mut buf); - cbor_writer.write_number(-500_i128).unwrap(); - assert_eq!(buf, &[0b001_11001, 0x01, 0xf3]); - } - - #[test] - fn write_map_start() { - let mut buf: Vec = Vec::with_capacity(3); - let mut cbor_writer = CborWriter::new(&mut buf); - cbor_writer.write_map_start(800).unwrap(); - assert_eq!(buf, &[0b101_11001, 0b0000_0011, 0b0010_0000,]); - } -} diff --git a/credentialsd/src/cose.rs b/credentialsd/src/cose.rs deleted file mode 100644 index d55c043..0000000 --- a/credentialsd/src/cose.rs +++ /dev/null @@ -1,82 +0,0 @@ -use libwebauthn::proto::ctap2::Ctap2COSEAlgorithmIdentifier; -use tracing::debug; - -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(i64)] -pub(super) enum CoseKeyType { - Es256P256, - EddsaEd25519, - RS256, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CoseKeyAlgorithmIdentifier { - ES256, - EdDSA, - RS256, -} - -impl From for i64 { - fn from(value: CoseKeyAlgorithmIdentifier) -> Self { - match value { - CoseKeyAlgorithmIdentifier::ES256 => -7, - CoseKeyAlgorithmIdentifier::EdDSA => -8, - CoseKeyAlgorithmIdentifier::RS256 => -257, - } - } -} - -impl From for i128 { - fn from(value: CoseKeyAlgorithmIdentifier) -> Self { - match value { - CoseKeyAlgorithmIdentifier::ES256 => -7, - CoseKeyAlgorithmIdentifier::EdDSA => -8, - CoseKeyAlgorithmIdentifier::RS256 => -257, - } - } -} - -impl TryFrom for CoseKeyAlgorithmIdentifier { - type Error = Error; - - fn try_from(value: Ctap2COSEAlgorithmIdentifier) -> Result { - match value { - Ctap2COSEAlgorithmIdentifier::EDDSA => Ok(CoseKeyAlgorithmIdentifier::EdDSA), - Ctap2COSEAlgorithmIdentifier::ES256 => Ok(CoseKeyAlgorithmIdentifier::ES256), - Ctap2COSEAlgorithmIdentifier::TOPT => { - debug!("Unknown public key algorithm type: {:?}", value); - Err(Error::Unsupported) - } - Ctap2COSEAlgorithmIdentifier::Unknown => Err(Error::Unsupported), - } - } -} - -#[derive(Clone, Copy, PartialEq)] -pub enum CoseEllipticCurveIdentifier { - /// P-256 Elliptic Curve using uncompressed points. - P256, - /// P-384 Elliptic Curve using uncompressed points. - P384, - /// P-521 Elliptic Curve using uncompressed points. - P521, - /// Ed25519 Elliptic Curve using compressed points. - Ed25519, -} - -impl From for i64 { - fn from(value: CoseEllipticCurveIdentifier) -> Self { - match value { - CoseEllipticCurveIdentifier::P256 => 1, - CoseEllipticCurveIdentifier::P384 => 2, - CoseEllipticCurveIdentifier::P521 => 3, - CoseEllipticCurveIdentifier::Ed25519 => 6, - } - } -} - -#[derive(Debug)] -pub enum Error { - InvalidKey, - Unsupported, -} diff --git a/credentialsd/src/dbus/gateway.rs b/credentialsd/src/dbus/gateway.rs index ff698f1..ed70fcd 100644 --- a/credentialsd/src/dbus/gateway.rs +++ b/credentialsd/src/dbus/gateway.rs @@ -233,7 +233,7 @@ impl CredentialGateway CredentialGateway CredentialGateway CredentialGateway CredentialGateway for Error { #[cfg(test)] mod test { - use std::future::Future; - use credentialsd_common::model::WebAuthnError; use crate::dbus::gateway::check_origin; diff --git a/credentialsd/src/dbus/model.rs b/credentialsd/src/dbus/model.rs index f4160c8..04cd0d5 100644 --- a/credentialsd/src/dbus/model.rs +++ b/credentialsd/src/dbus/model.rs @@ -11,8 +11,7 @@ use credentialsd_common::{ }; use crate::webauthn::{ - self, GetAssertionRequest, GetPublicKeyCredentialUnsignedExtensionsResponse, - MakeCredentialRequest, RelyingPartyId, WebAuthnIDL, + GetAssertionRequest, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL, WebAuthnIDLResponse, }; /// Parses a WebAuthn create credential request from D-Bus into a CTAP2 MakeCredentialRequest. @@ -25,11 +24,9 @@ use crate::webauthn::{ /// - Authenticator selection criteria (residentKey, userVerification) /// - Excluded credentials list /// - Public key credential parameters -/// -/// Returns the parsed request and the client data JSON (needed for response serialization). pub(super) fn create_credential_request_try_into_ctap2( request: &CreateCredentialRequest, -) -> std::result::Result<(MakeCredentialRequest, String), WebAuthnError> { +) -> std::result::Result { if request.public_key.is_none() { return Err(WebAuthnError::NotSupportedError); } @@ -91,56 +88,34 @@ pub(super) fn create_credential_request_try_into_ctap2( make_cred_request.origin = origin; make_cred_request.cross_origin = request.is_same_origin.as_ref().map(|same| !same); - // Get the client data JSON from the request for response serialization - let client_data_json = - String::from_utf8(make_cred_request.client_data_json()).map_err(|_| { - tracing::info!("Failed to serialize client data JSON"); - WebAuthnError::TypeError - })?; - - Ok((make_cred_request, client_data_json)) + Ok(make_cred_request) } /// Serializes a CTAP2 MakeCredentialResponse to WebAuthn JSON format. +/// +/// Uses libwebauthn's `WebAuthnIDLResponse::to_inner_model()` for serialization, +/// then adds transport and authenticator attachment information that is known +/// at the credential service level. pub(super) fn create_credential_response_try_from_ctap2( response: &MakeCredentialResponseInternal, - client_data_json: String, + request: &MakeCredentialRequest, ) -> std::result::Result { - let auth_data = &response.ctap.authenticator_data; - let attested_credential = auth_data - .attested_credential - .as_ref() - .ok_or_else(|| "missing attested credential data".to_string())?; + let mut registration_json = response + .ctap + .to_inner_model(request) + .map_err(|err| format!("Failed to serialize registration response: {err}"))?; + + // TODO(libwebauthn#159): transports and authenticatorAttachment should be + // populated by libwebauthn once it has access to transport-level information. + registration_json.response.transports = response.transport.clone(); + registration_json.authenticator_attachment = Some(response.attachment_modality.clone()); - let unsigned_extensions = serde_json::to_string(&response.ctap.unsigned_extensions_output) - .map_err(|err| format!("failed to serialized unsigned extensions output: {err}")) - .unwrap(); - let authenticator_data_blob = auth_data - .to_response_bytes() - .map_err(|err| format!("failed to serialize authenticator data into bytes: {err}"))?; - let attestation_statement = (&response.ctap.attestation_statement) - .try_into() - .map_err(|_| "Could not serialize attestation statement".to_string())?; - let attestation_object = webauthn::create_attestation_object( - &authenticator_data_blob, - &attestation_statement, - response.ctap.enterprise_attestation.unwrap_or(false), - ) - .map_err(|_| "Failed to create attestation object".to_string())?; + let registration_response_json = serde_json::to_string(®istration_json) + .map_err(|err| format!("Failed to serialize registration response to JSON: {err}"))?; - let registration_response_json = webauthn::CreatePublicKeyCredentialResponse::new( - attested_credential.credential_id.clone(), - attestation_object, - client_data_json, - Some(response.transport.clone()), - unsigned_extensions, - response.attachment_modality.clone(), - ) - .to_json(); - let response = CreatePublicKeyCredentialResponse { + Ok(CreatePublicKeyCredentialResponse { registration_response_json, - }; - Ok(response) + }) } /// Parses a WebAuthn get credential request from D-Bus into a CTAP2 GetAssertionRequest. @@ -150,11 +125,9 @@ pub(super) fn create_credential_response_try_from_ctap2( /// - Allowed credentials list with transports /// - Extension parsing (getCredBlob, largeBlob, prf, hmac-secret) /// - User verification requirement -/// -/// Returns the parsed request and the client data JSON (needed for response serialization). pub(super) fn get_credential_request_try_into_ctap2( request: &GetCredentialRequest, -) -> std::result::Result<(GetAssertionRequest, String), WebAuthnError> { +) -> std::result::Result { if request.public_key.is_none() { return Err(WebAuthnError::NotSupportedError); } @@ -214,50 +187,31 @@ pub(super) fn get_credential_request_try_into_ctap2( get_assertion_request.origin = origin; get_assertion_request.cross_origin = request.is_same_origin.as_ref().map(|same| !same); - // Get the client data JSON from the request for response serialization - let client_data_json = - String::from_utf8(get_assertion_request.client_data_json()).map_err(|_| { - tracing::info!("Failed to serialize client data JSON"); - WebAuthnError::TypeError - })?; - - Ok((get_assertion_request, client_data_json)) + Ok(get_assertion_request) } /// Serializes a CTAP2 GetAssertionResponse to WebAuthn JSON format. +/// +/// Uses libwebauthn's `WebAuthnIDLResponse::to_inner_model()` for serialization, +/// then adds authenticator attachment information that is known at the +/// credential service level. pub(super) fn get_credential_response_try_from_ctap2( response: &GetAssertionResponseInternal, - client_data_json: String, + request: &GetAssertionRequest, ) -> std::result::Result { - let authenticator_data_blob = response + let mut authentication_json = response .ctap - .authenticator_data - .to_response_bytes() - .map_err(|err| format!("Failed to parse authenticator data: {err}"))?; + .to_inner_model(request) + .map_err(|err| format!("Failed to serialize authentication response: {err}"))?; - let unsigned_extensions = response - .ctap - .unsigned_extensions_output - .as_ref() - .map(GetPublicKeyCredentialUnsignedExtensionsResponse::from); + // TODO(libwebauthn#159): authenticatorAttachment should be populated by + // libwebauthn once it has access to transport-level information. + authentication_json.authenticator_attachment = Some(response.attachment_modality.clone()); - let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( - client_data_json, - response - .ctap - .credential_id - .as_ref() - .map(|c| c.id.clone().into_vec()), - authenticator_data_blob, - response.ctap.signature.clone(), - response.ctap.user.as_ref().map(|u| u.id.clone().into_vec()), - response.attachment_modality.clone(), - unsigned_extensions, - ) - .to_json(); + let authentication_response_json = serde_json::to_string(&authentication_json) + .map_err(|err| format!("Failed to serialize authentication response to JSON: {err}"))?; - let response = GetPublicKeyCredentialResponse { + Ok(GetPublicKeyCredentialResponse { authentication_response_json, - }; - Ok(response) + }) } diff --git a/credentialsd/src/main.rs b/credentialsd/src/main.rs index 4090985..173a091 100644 --- a/credentialsd/src/main.rs +++ b/credentialsd/src/main.rs @@ -1,8 +1,5 @@ -mod cbor; -mod cose; mod credential_service; mod dbus; -mod serde; mod webauthn; use std::{error::Error, sync::Arc}; diff --git a/credentialsd/src/serde/mod.rs b/credentialsd/src/serde/mod.rs deleted file mode 100644 index 353b0f2..0000000 --- a/credentialsd/src/serde/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub(crate) mod b64 { - use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; - - use serde::{de, Deserialize, Deserializer}; - - pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - URL_SAFE_NO_PAD.decode(s).map_err(de::Error::custom) - } -} - -pub(crate) mod duration { - use std::time::Duration; - - use serde::{Deserialize, Deserializer}; - - pub(crate) fn from_opt_ms<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - Option::::deserialize(deserializer) - .map(|ms_opt| ms_opt.map(|ms| Duration::from_millis(ms as u64))) - } -} diff --git a/credentialsd/src/webauthn.rs b/credentialsd/src/webauthn.rs index 86c0393..b7f0f02 100644 --- a/credentialsd/src/webauthn.rs +++ b/credentialsd/src/webauthn.rs @@ -1,411 +1,9 @@ -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use libwebauthn::proto::ctap2::Ctap2AttestationStatement; -use serde::Serialize; -use serde_json::json; -use tracing::debug; - -use crate::cose::CoseKeyAlgorithmIdentifier; +//! WebAuthn types re-exported from libwebauthn. +//! +//! This module re-exports the types needed for WebAuthn request parsing +//! and response serialization from the libwebauthn crate. // Re-exports from libwebauthn pub use libwebauthn::ops::webauthn::{ - GetAssertionRequest, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL, + GetAssertionRequest, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL, WebAuthnIDLResponse, }; - -#[derive(Debug)] -pub enum Error { - Unknown, - NotSupported, - InvalidState, - NotAllowed, - Constraint, - Internal(String), -} - -pub(crate) fn create_attestation_object( - authenticator_data: &[u8], - attestation_statement: &AttestationStatement, - _enterprise_attestation_possible: bool, -) -> Result, Error> { - let mut attestation_object = Vec::new(); - let mut cbor_writer = crate::cbor::CborWriter::new(&mut attestation_object); - cbor_writer.write_map_start(3).unwrap(); - cbor_writer.write_text("fmt").unwrap(); - match attestation_statement { - AttestationStatement::Packed { - algorithm, - signature, - certificates, - } => { - cbor_writer.write_text("packed").unwrap(); - cbor_writer.write_text("attStmt").unwrap(); - let len = if certificates.is_empty() { 2 } else { 3 }; - cbor_writer.write_map_start(len).unwrap(); - cbor_writer.write_text("alg").unwrap(); - cbor_writer.write_number((*algorithm).into()).unwrap(); - cbor_writer.write_text("sig").unwrap(); - cbor_writer.write_bytes(signature).unwrap(); - if !certificates.is_empty() { - cbor_writer.write_text("x5c").unwrap(); - cbor_writer.write_array_start(certificates.len()).unwrap(); - for cert in certificates.iter() { - cbor_writer.write_bytes(cert).unwrap(); - } - } - } - AttestationStatement::U2F { - signature, - certificate, - } => { - cbor_writer.write_text("fido-u2f").unwrap(); - cbor_writer.write_text("attStmt").unwrap(); - cbor_writer.write_map_start(2).unwrap(); - cbor_writer.write_text("x5c").unwrap(); - cbor_writer.write_array_start(1).unwrap(); - cbor_writer.write_bytes(certificate).unwrap(); - cbor_writer.write_text("sig").unwrap(); - cbor_writer.write_bytes(signature).unwrap(); - } - AttestationStatement::None => { - cbor_writer.write_text("none").unwrap(); - cbor_writer.write_text("attStmt").unwrap(); - cbor_writer.write_map_start(0).unwrap(); - } - }; - - cbor_writer.write_text("authData").unwrap(); - cbor_writer.write_bytes(authenticator_data).unwrap(); - - Ok(attestation_object) -} - -#[derive(Debug, PartialEq)] -pub(crate) enum AttestationStatement { - None, - U2F { - signature: Vec, - certificate: Vec, - }, - Packed { - algorithm: CoseKeyAlgorithmIdentifier, - signature: Vec, - certificates: Vec>, - }, -} - -impl TryFrom<&Ctap2AttestationStatement> for AttestationStatement { - type Error = Error; - - fn try_from(value: &Ctap2AttestationStatement) -> Result { - match value { - Ctap2AttestationStatement::None(_) => Ok(AttestationStatement::None), - Ctap2AttestationStatement::PackedOrAndroid(att_stmt) => { - let alg = att_stmt - .algorithm - .try_into() - .map_err(|_| Error::NotSupported)?; - Ok(Self::Packed { - algorithm: alg, - signature: att_stmt.signature.as_ref().to_vec(), - certificates: att_stmt - .certificates - .iter() - .map(|c| c.as_ref().to_vec()) - .collect(), - }) - } - Ctap2AttestationStatement::FidoU2F(att_stmt) => Ok(Self::U2F { - signature: att_stmt.signature.as_ref().to_vec(), - certificate: att_stmt - .certificates - .first() - .map(|c| c.as_ref().to_vec()) - .unwrap_or_default(), - }), - _ => { - debug!("Unsupported attestation type: {:?}", value); - Err(Error::NotSupported) - } - } - } -} - -pub struct CreatePublicKeyCredentialResponse { - /// Raw bytes of credential ID. - raw_id: Vec, - - response: AttestationResponse, - - /// JSON string of extension output - extensions: String, - - /// If the device used is builtin ("platform") or removable ("cross-platform", aka "roaming") - attachment_modality: String, -} - -#[derive(Debug, Clone, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CredentialPropertiesOutput { - /// This OPTIONAL property, known abstractly as the resident key credential property (i.e., client-side discoverable credential property), is a Boolean value indicating whether the PublicKeyCredential returned as a result of a registration ceremony is a client-side discoverable credential. If rk is true, the credential is a discoverable credential. if rk is false, the credential is a server-side credential. If rk is not present, it is not known whether the credential is a discoverable credential or a server-side credential. - #[serde(skip_serializing_if = "Option::is_none")] - pub rk: Option, -} - -#[derive(Debug, Clone, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationExtensionsLargeBlobOutputs { - /// true if, and only if, the created credential supports storing large blobs. Only present in registration outputs. - #[serde(skip_serializing_if = "Option::is_none")] - pub supported: Option, - /// The opaque byte string that was associated with the credential identified by rawId. Only valid if read was true. - #[serde(skip_serializing_if = "Option::is_none")] - pub blob: Option>, - /// A boolean that indicates that the contents of write were successfully stored on the authenticator, associated with the specified credential. - #[serde(skip_serializing_if = "Option::is_none")] - pub written: Option, -} - -#[derive(Debug, Clone, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationExtensionsPRFValues { - pub first: Vec, - pub second: Option>, -} - -#[derive(Debug, Clone, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationExtensionsPRFOutputs { - /// true if, and only if, the one or two PRFs are available for use with the created credential. This is only reported during registration and is not present in the case of authentication. - #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, - /// The results of evaluating the PRF for the inputs given in eval or evalByCredential. Outputs may not be available during registration; see comments in eval. - #[serde(skip_serializing_if = "Option::is_none")] - pub results: Option, -} - -#[derive(Debug, Clone, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreatePublicKeyExtensionsResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub cred_props: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub large_blob: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub prf: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cred_protect: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub min_pin_length: Option, -} - -/// Returned from a creation of a new public key credential. -pub struct AttestationResponse { - /// clientDataJSON. - client_data_json: String, - - /// Bytes containing authenticator data and an attestation statement. - attestation_object: Vec, - - /// Transports that the authenticator is believed to support, or an - /// empty sequence if the information is unavailable. - /// - /// Should be one of - /// - `usb` - /// - `nfc` - /// - `ble` - /// - `internal` - /// - /// but others may be specified. - transports: Vec, -} - -impl CreatePublicKeyCredentialResponse { - pub fn new( - id: Vec, - attestation_object: Vec, - client_data_json: String, - transports: Option>, - extension_output_json: String, - attachment_modality: String, - ) -> Self { - Self { - raw_id: id, - response: AttestationResponse { - client_data_json, - attestation_object, - transports: transports.unwrap_or_default(), - }, - extensions: extension_output_json, - attachment_modality, - } - } - - pub fn get_id(&self) -> String { - URL_SAFE_NO_PAD.encode(&self.raw_id) - } - - pub fn to_json(&self) -> String { - let response = json!({ - "clientDataJSON": URL_SAFE_NO_PAD.encode(self.response.client_data_json.as_bytes()), - "attestationObject": URL_SAFE_NO_PAD.encode(&self.response.attestation_object), - "transports": self.response.transports, - }); - let extensions: serde_json::Value = serde_json::from_str(&self.extensions) - .expect("Extensions json to be formatted properly"); - let output = json!({ - "id": self.get_id(), - "rawId": self.get_id(), - "response": response, - "authenticatorAttachment": self.attachment_modality, - "clientExtensionResults": extensions, - }); - output.to_string() - } -} - -pub struct GetPublicKeyCredentialResponse { - /// clientDataJSON. - pub(crate) client_data_json: String, - - /// Raw bytes of credential ID. Not returned if only one descriptor was - /// passed in the allow credentials list. - pub(crate) raw_id: Option>, - - /// Encodes contextual bindings made by the authenticator. These bindings - /// are controlled by the authenticator itself. - pub(crate) authenticator_data: Vec, - - pub(crate) signature: Vec, - - /// The user handle associated when this public key credential source was - /// created. This item is nullable, however user handle MUST always be - /// populated for discoverable credentials. - pub(crate) user_handle: Option>, - - /// Whether the used device is "cross-platform" (aka "roaming", i.e.: can be - /// removed from the platform) or is built-in ("platform"). - pub(crate) attachment_modality: String, - - /// Unsigned extension output - /// Unlike CreatePublicKey, we can't use a directly serialized JSON string here, - /// because we have to encode/decode the byte arrays for the JavaScript-communication - pub(crate) extensions: Option, -} - -#[derive(Clone, Debug, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetPublicKeyCredentialHMACGetSecretOutput { - // base64-encoded bytestring - pub output1: String, - #[serde(skip_serializing_if = "Option::is_none")] - // base64-encoded bytestring - pub output2: Option, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] -pub struct GetPublicKeyCredentialLargeBlobOutput { - #[serde(skip_serializing_if = "Option::is_none")] - // base64-encoded bytestring - pub blob: Option, - // Not yet supported - // #[serde(skip_serializing_if = "Option::is_none")] - // pub written: Option, -} - -#[derive(Debug, Default, Clone, Serialize)] -pub struct GetPublicKeyCredentialPrfOutput { - #[serde(skip_serializing_if = "Option::is_none")] - pub results: Option, -} - -#[derive(Debug, Default, Clone, Serialize)] -pub struct GetPublicKeyCredentialPRFValue { - // base64-encoded bytestring - pub first: String, - #[serde(skip_serializing_if = "Option::is_none")] - // base64-encoded bytestring - pub second: Option, -} - -#[derive(Debug, Default, Clone, Serialize)] -pub struct GetPublicKeyCredentialUnsignedExtensionsResponse { - pub hmac_get_secret: Option, - pub large_blob: Option, - pub prf: Option, -} - -// Unlike CreatePublicKey, for GetPublicKey, we have a lot of Byte arrays, -// so we need a lot of de/constructions, instead of serializing it directly -impl From<&libwebauthn::ops::webauthn::GetAssertionResponseUnsignedExtensions> - for GetPublicKeyCredentialUnsignedExtensionsResponse -{ - fn from(value: &libwebauthn::ops::webauthn::GetAssertionResponseUnsignedExtensions) -> Self { - Self { - hmac_get_secret: value.hmac_get_secret.as_ref().map(|x| { - GetPublicKeyCredentialHMACGetSecretOutput { - output1: URL_SAFE_NO_PAD.encode(x.output1), - output2: x.output2.map(|output2| URL_SAFE_NO_PAD.encode(output2)), - } - }), - large_blob: value - .large_blob - .as_ref() - .map(|x| GetPublicKeyCredentialLargeBlobOutput { - blob: x.blob.as_ref().map(|blob| URL_SAFE_NO_PAD.encode(blob)), - }), - prf: value.prf.as_ref().map(|x| GetPublicKeyCredentialPrfOutput { - results: x - .results - .as_ref() - .map(|results| GetPublicKeyCredentialPRFValue { - first: URL_SAFE_NO_PAD.encode(results.first), - second: results.second.map(|second| URL_SAFE_NO_PAD.encode(second)), - }), - }), - } - } -} - -impl GetPublicKeyCredentialResponse { - pub(crate) fn new( - client_data_json: String, - id: Option>, - authenticator_data: Vec, - signature: Vec, - user_handle: Option>, - attachment_modality: String, - extensions: Option, - ) -> Self { - Self { - client_data_json, - raw_id: id, - authenticator_data, - signature, - user_handle, - attachment_modality, - extensions, - } - } - pub fn to_json(&self) -> String { - let response = json!({ - "clientDataJSON": URL_SAFE_NO_PAD.encode(self.client_data_json.as_bytes()), - "authenticatorData": URL_SAFE_NO_PAD.encode(&self.authenticator_data), - "signature": URL_SAFE_NO_PAD.encode(&self.signature), - "userHandle": self.user_handle.as_ref().map(|h| URL_SAFE_NO_PAD.encode(h)) - }); - // TODO: I believe this optional since authenticators may omit sending the credential ID if it was - // unambiguously specified in the request. As a convenience, we should - // always return a credential ID, even if the authenticator doesn't. - // This means we'll have to remember the ID on the request if the allow-list has exactly one - // credential descriptor. This should probably be done in libwebauthn. - let id = self.raw_id.as_ref().map(|id| URL_SAFE_NO_PAD.encode(id)); - - let output = json!({ - "id": id, - "rawId": id, - "authenticatorAttachment": self.attachment_modality, - "response": response, - "clientExtensionResults": self.extensions, - }); - output.to_string() - } -}