From 805296e7c1db57af1b5516149bc1db17b50ba476 Mon Sep 17 00:00:00 2001 From: gtema Date: Mon, 8 Sep 2025 22:02:01 +0200 Subject: [PATCH] feat: Update passkey data deserialization In practice webauthn uses camelCase serialization and deserialization. That does not match to what we declare and generate. Also to enable eventual support for other libraries do explicit casting to the structure handling the Base64UrlSafeData ser/deser. --- Cargo.lock | 1 + Cargo.toml | 3 +- src/api/error.rs | 4 + src/api/v4/user/passkey/register_finish.rs | 62 +++++++- src/api/v4/user/passkey/register_start.rs | 148 +++++++++++++++++- src/api/v4/user/types/passkey.rs | 168 ++++++++++++++------- src/identity/backends/sql/passkey.rs | 4 +- 7 files changed, 325 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c161eeee..6b096905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2851,6 +2851,7 @@ dependencies = [ "utoipa-swagger-ui", "uuid", "webauthn-rs", + "webauthn-rs-proto", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6fb8725b..fa603138 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ harness = false [dependencies] async-trait = { version = "0.1" } axum = { version = "0.8", features = ["macros"] } -base64 = { version = "0.22" } +base64 = "0.22" bcrypt = { version = "0.17", features = ["alloc"] } bytes = { version = "1.10" } chrono = { version = "0.4" } @@ -60,6 +60,7 @@ utoipa-axum = { version = "0.2" } utoipa-swagger-ui = { version = "9.0", features = ["axum", "vendored"], default-features = false } uuid = { version = "1.18", features = ["v4"] } webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation"] } +webauthn-rs-proto = "0.5.2" [dev-dependencies] criterion = { version = "0.7", features = ["async_tokio"] } diff --git a/src/api/error.rs b/src/api/error.rs index 0daf6ae4..28aa904f 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -143,6 +143,10 @@ pub enum KeystoneApiError { source: serde_json::Error, }, + /// Base64 decoding error. + #[error(transparent)] + Base64Decode(#[from] base64::DecodeError), + #[error("domain id or name must be present")] DomainIdOrName, diff --git a/src/api/v4/user/passkey/register_finish.rs b/src/api/v4/user/passkey/register_finish.rs index 60eb67e9..cf725eae 100644 --- a/src/api/v4/user/passkey/register_finish.rs +++ b/src/api/v4/user/passkey/register_finish.rs @@ -18,14 +18,15 @@ use axum::{ http::StatusCode, response::IntoResponse, }; +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use mockall_double::double; -use serde_json::Value; use tracing::debug; -use webauthn_rs::prelude::*; use crate::api::auth::Auth; use crate::api::error::{KeystoneApiError, WebauthnError}; -use crate::api::v4::user::types::passkey::UserPasskeyRegistrationFinishRequest; +use crate::api::v4::user::types::passkey::{ + AuthenticatorTransport, CredentialProtectionPolicy, UserPasskeyRegistrationFinishRequest, +}; use crate::identity::IdentityApi; use crate::keystone::ServiceState; #[double] @@ -57,7 +58,7 @@ pub(super) async fn finish( Path(user_id): Path, State(state): State, mut policy: Policy, - Json(req): Json, + Json(req): Json, ) -> Result { let user = state .provider @@ -86,8 +87,10 @@ pub(super) async fn finish( .get_user_passkey_registration_state(&state.db, &user_id) .await? { - let v: RegisterPublicKeyCredential = serde_json::from_value(req)?; - match state.webauthn.finish_passkey_registration(&v, &s) { + match state + .webauthn + .finish_passkey_registration(&req.try_into()?, &s) + { Ok(sk) => { state .provider @@ -110,3 +113,50 @@ pub(super) async fn finish( } Ok((StatusCode::CREATED).into_response()) } + +impl TryFrom + for webauthn_rs::prelude::RegisterPublicKeyCredential +{ + type Error = KeystoneApiError; + fn try_from(val: UserPasskeyRegistrationFinishRequest) -> Result { + Ok(webauthn_rs::prelude::RegisterPublicKeyCredential { + id: val.id, + raw_id: URL_SAFE.decode(val.raw_id)?.into(), + type_: val.type_, + response: webauthn_rs_proto::attest::AuthenticatorAttestationResponseRaw { + attestation_object: URL_SAFE.decode(val.response.attestation_object)?.into(), + client_data_json: URL_SAFE.decode(val.response.client_data_json)?.into(), + transports: val.response.transports.map(|i| { + i.into_iter() + .map(|t| match t { + AuthenticatorTransport::Usb => webauthn_rs_proto::options::AuthenticatorTransport::Usb, + AuthenticatorTransport::Nfc => webauthn_rs_proto::options::AuthenticatorTransport::Nfc, + AuthenticatorTransport::Ble => webauthn_rs_proto::options::AuthenticatorTransport::Ble, + AuthenticatorTransport::Internal => webauthn_rs_proto::options::AuthenticatorTransport::Internal, + AuthenticatorTransport::Hybrid => webauthn_rs_proto::options::AuthenticatorTransport::Hybrid, + AuthenticatorTransport::Test => webauthn_rs_proto::options::AuthenticatorTransport::Test, + AuthenticatorTransport::Unknown => webauthn_rs_proto::options::AuthenticatorTransport::Unknown, + + }) + .collect::>() + }), + }, + extensions: webauthn_rs_proto::extensions::RegistrationExtensionsClientOutputs { + appid: val.extensions.appid, + cred_props: val + .extensions + .cred_props + .map(|x| webauthn_rs_proto::extensions::CredProps { rk: x.rk }), + hmac_secret: val.extensions.hmac_secret, + cred_protect: val.extensions.cred_protect.map(|x| { + match x { + CredentialProtectionPolicy::UserVerificationOptional => webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional, + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList, + CredentialProtectionPolicy::UserVerificationRequired => webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired + } + }), + min_pin_length: val.extensions.min_pin_length, + }, + }) + } +} diff --git a/src/api/v4/user/passkey/register_start.rs b/src/api/v4/user/passkey/register_start.rs index ff5c9a9d..c931abd1 100644 --- a/src/api/v4/user/passkey/register_start.rs +++ b/src/api/v4/user/passkey/register_start.rs @@ -17,13 +17,22 @@ use axum::{ extract::{Path, State}, response::IntoResponse, }; +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use mockall_double::double; use tracing::debug; use webauthn_rs::prelude::*; use crate::api::auth::Auth; use crate::api::error::{KeystoneApiError, WebauthnError}; -use crate::api::v4::user::types::passkey::UserPasskeyRegistrationStartResponse; +use crate::api::v4::user::types::passkey::{ + AttestationConveyancePreference, AttestationFormat, AuthenticatorAttachment, + AuthenticatorSelectionCriteria, AuthenticatorTransport, CredProtect, + CredentialProtectionPolicy, PubKeyCredParams, PublicKeyCredentialCreationOptions, + PublicKeyCredentialDescriptor, PublicKeyCredentialHints, RelyingParty, + RequestRegistrationExtensions, ResidentKeyRequirement, User, + UserPasskeyRegistrationStartRequest, UserPasskeyRegistrationStartResponse, + UserVerificationPolicy, +}; use crate::identity::IdentityApi; use crate::keystone::ServiceState; #[double] @@ -37,6 +46,7 @@ use crate::policy::Policy; post, path = "/register_start", operation_id = "/user/passkey/register:start", + request_body = UserPasskeyRegistrationStartRequest, params( ("user_id" = String, Path, description = "The ID of the user.") ), @@ -57,6 +67,7 @@ pub(super) async fn start( Path(user_id): Path, mut policy: Policy, State(state): State, + Json(req): Json, ) -> Result { let user = state .provider @@ -99,7 +110,7 @@ pub(super) async fn start( .get_identity_provider() .save_user_passkey_registration_state(&state.db, &user_id, reg_state) .await?; - Json(ccr) + Json(UserPasskeyRegistrationStartResponse::try_from(ccr)?) } Err(e) => { debug!("challenge_register -> {:?}", e); @@ -109,3 +120,136 @@ pub(super) async fn start( Ok(res) } + +impl TryFrom + for UserPasskeyRegistrationStartResponse +{ + type Error = KeystoneApiError; + fn try_from(val: webauthn_rs::prelude::CreationChallengeResponse) -> Result { + Ok(UserPasskeyRegistrationStartResponse { + public_key: PublicKeyCredentialCreationOptions { + attestation: val.public_key.attestation.map(|att| match att { + webauthn_rs_proto::options::AttestationConveyancePreference::Direct => { + AttestationConveyancePreference::Direct + } + webauthn_rs_proto::options::AttestationConveyancePreference::Indirect => { + AttestationConveyancePreference::Indirect + } + webauthn_rs_proto::options::AttestationConveyancePreference::None => { + AttestationConveyancePreference::None + } + }), + attestation_formats: val + .public_key + .attestation_formats + .map(|afs| { + afs.into_iter().map(|fmt| match fmt { + webauthn_rs_proto::options::AttestationFormat::AndroidKey => { + AttestationFormat::AndroidKey + } + webauthn_rs_proto::options::AttestationFormat::AndroidSafetyNet => { + AttestationFormat::AndroidSafetyNet + } + webauthn_rs_proto::options::AttestationFormat::AppleAnonymous => { + AttestationFormat::AppleAnonymous + } + webauthn_rs_proto::options::AttestationFormat::FIDOU2F => { + AttestationFormat::FIDOU2F + } + webauthn_rs_proto::options::AttestationFormat::None => { + AttestationFormat::None + } + webauthn_rs_proto::options::AttestationFormat::Packed => { + AttestationFormat::Packed + } + webauthn_rs_proto::options::AttestationFormat::Tpm => { + AttestationFormat::Tpm + } + }) + .collect::>() + }), + authenticator_selection: val.public_key.authenticator_selection.map(|authn| { + AuthenticatorSelectionCriteria { + authenticator_attachment: authn.authenticator_attachment.map(|attach| { + match attach { + webauthn_rs_proto::options::AuthenticatorAttachment::CrossPlatform => AuthenticatorAttachment::CrossPlatform, + webauthn_rs_proto::options::AuthenticatorAttachment::Platform => AuthenticatorAttachment::Platform, + } + }), + require_resident_key: authn.require_resident_key, + resident_key: authn.resident_key.map(|rk| + match rk { + webauthn_rs_proto::options::ResidentKeyRequirement::Discouraged => ResidentKeyRequirement::Discouraged, + webauthn_rs_proto::options::ResidentKeyRequirement::Preferred => ResidentKeyRequirement::Preferred, + webauthn_rs_proto::options::ResidentKeyRequirement::Required => ResidentKeyRequirement::Required, + } + ), + user_verification: match authn.user_verification { + webauthn_rs_proto::options::UserVerificationPolicy::Preferred => UserVerificationPolicy::Preferred, + webauthn_rs_proto::options::UserVerificationPolicy::Required => UserVerificationPolicy::Required, + webauthn_rs_proto::options::UserVerificationPolicy::Discouraged_DO_NOT_USE => UserVerificationPolicy::DiscouragedDoNotUse, + } + } + }), + challenge: URL_SAFE.encode(&val.public_key.challenge), + exclude_credentials: val.public_key.exclude_credentials.map(|ecs| ecs.into_iter().map(|descr| { + PublicKeyCredentialDescriptor{ + type_: descr.type_, + id: URL_SAFE.encode(&descr.id), + transports: descr.transports.map(|transports| transports.into_iter().map(|tr|{ + match tr { + webauthn_rs_proto::options::AuthenticatorTransport::Ble => AuthenticatorTransport::Ble, + webauthn_rs_proto::options::AuthenticatorTransport::Hybrid => AuthenticatorTransport::Hybrid, + webauthn_rs_proto::options::AuthenticatorTransport::Internal => AuthenticatorTransport::Internal, + webauthn_rs_proto::options::AuthenticatorTransport::Nfc => AuthenticatorTransport::Nfc, + webauthn_rs_proto::options::AuthenticatorTransport::Usb => AuthenticatorTransport::Usb, + webauthn_rs_proto::options::AuthenticatorTransport::Test => AuthenticatorTransport::Test, + webauthn_rs_proto::options::AuthenticatorTransport::Unknown => AuthenticatorTransport::Unknown, + } + }).collect::>()) + } + }).collect::>()), + extensions: val.public_key.extensions.map(|ext| RequestRegistrationExtensions{ + cred_props: ext.cred_props, + cred_protect: ext.cred_protect.map(|cp| + { + CredProtect { + credential_protection_policy: match cp.credential_protection_policy { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional => CredentialProtectionPolicy::UserVerificationOptional, + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList, + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired => CredentialProtectionPolicy::UserVerificationRequired, + + }, + enforce_credential_protection_policy: cp.enforce_credential_protection_policy, + } + }), + hmac_create_secret: ext.hmac_create_secret, + min_pin_length: ext.min_pin_length, + uvm: ext.uvm, + }), + hints: val.public_key.hints.map(|hints| hints.into_iter().map(|hint|{ + match hint { + webauthn_rs_proto::options::PublicKeyCredentialHints::ClientDevice => PublicKeyCredentialHints::ClientDevice, + webauthn_rs_proto::options::PublicKeyCredentialHints::Hybrid => PublicKeyCredentialHints::Hybrid, + webauthn_rs_proto::options::PublicKeyCredentialHints::SecurityKey => PublicKeyCredentialHints::SecurityKey, + } }).collect::>()), + pub_key_cred_params: val.public_key.pub_key_cred_params.into_iter().map(|pkcp| { + PubKeyCredParams{ + alg: pkcp.alg, + type_: pkcp.type_ + } + }).collect::>(), + rp: RelyingParty{ + id: val.public_key.rp.id, + name: val.public_key.rp.name, + }, + timeout: val.public_key.timeout, + user: User { + id: URL_SAFE.encode(&val.public_key.user.id), + name: val.public_key.user.name, + display_name: val.public_key.user.display_name, + } + }, + }) + } +} diff --git a/src/api/v4/user/types/passkey.rs b/src/api/v4/user/types/passkey.rs index 419f44d3..778d4eda 100644 --- a/src/api/v4/user/types/passkey.rs +++ b/src/api/v4/user/types/passkey.rs @@ -17,15 +17,17 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -/// Passkey challenge. +/// Passkey registration request. /// -/// This is an embedded version of the -/// [webauthn-rs::CreationChallengeResponse](https://docs.rs/webauthn-rs/latest/webauthn_rs/prelude/struct.CreationChallengeResponse.html) +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct UserPasskeyRegistrationStartRequest { + /// The description for the passkey (name). + pub description: Option, +} + +/// Passkey challenge. /// -/// A JSON serializable challenge which is issued to the user’s web browser for handling. This is -/// meant to be opaque, that is, you should not need to inspect or alter the content of the struct -/// - you should serialise it and transmit it to the client only. #[derive(Clone, Debug, -/// Deserialize, PartialEq, Serialize, ToSchema)] +/// This is the WebauthN challenge that need to be signed by the passkey/security device. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct UserPasskeyRegistrationStartResponse { /// The options. @@ -35,38 +37,51 @@ pub struct UserPasskeyRegistrationStartResponse { /// The requested options for the authentication. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct PublicKeyCredentialCreationOptions { - /// The relying party - pub rp: RelyingParty, - /// The user. - pub user: User, - /// The challenge that should be signed by the authenticator. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub challenge: Vec, - /// The set of cryptographic types allowed by this server. - pub pub_key_cred_params: Vec, - /// The timeout for the authenticator in case of no interaction. - pub timeout: Option, - /// Credential ID’s that are excluded from being able to be registered. - pub exclude_credentials: Option>, - /// Criteria defining which authenticators may be used in this operation. - pub authenticator_selection: Option, - /// Hints defining which types credentials may be used in this operation. - pub hints: Option>, /// The requested attestation level from the device. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub attestation: Option, /// The list of attestation formats that the RP will accept. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub attestation_formats: Option>, + /// Criteria defining which authenticators may be used in this operation. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub authenticator_selection: Option, + /// The challenge that should be signed by the authenticator. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub challenge: String, + /// Credential ID’s that are excluded from being able to be registered. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_credentials: Option>, /// extensions. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub extensions: Option, + /// Hints defining which types credentials may be used in this operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub hints: Option>, + /// The set of cryptographic types allowed by this server. + pub pub_key_cred_params: Vec, + /// The relying party + pub rp: RelyingParty, + /// The timeout for the authenticator in case of no interaction. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + /// The user. + pub user: User, } /// Relying Party Entity. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct RelyingParty { - /// The name of the relying party. - pub name: String, /// The id of the relying party. pub id: String, + /// The name of the relying party. + pub name: String, } /// User Entity. @@ -75,7 +90,7 @@ pub struct User { /// The user’s id in base64 form. This MUST be a unique id, and must NOT contain personally /// identifying information, as this value can NEVER be changed. If in doubt, use a UUID. #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub id: Vec, + pub id: String, /// A detailed name for the account, such as an email address. This value can change, so must /// not be used as a primary key. pub name: String, @@ -87,10 +102,10 @@ pub struct User { /// Public key cryptographic parameters #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct PubKeyCredParams { + /// The algorithm in use defined by CASE. + pub alg: i64, /// The type of public-key credential. pub type_: String, - /// The algorithm in use defined by COSE. - pub alg: i64, } /// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor @@ -100,16 +115,20 @@ pub struct PublicKeyCredentialDescriptor { pub type_: String, /// The credential id. #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub id: Vec, + pub id: String, /// The allowed transports for this credential. Note this is a hint, and is NOT enforced. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub transports: Option>, } -/// +/// /// Request in residentkey workflows that conditional mediation should be used in the UI, or not. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub enum Mediation { - /// Discovered credentials are presented to the user in a dialog. Conditional UI is used. See https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-Conditional-UI https://w3c.github.io/webappsec-credential-management/#enumdef-credentialmediationrequirement + /// Discovered credentials are presented to the user in a dialog. Conditional UI is used. See + /// https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-Conditional-UI + /// https://w3c.github.io/webappsec-credential-management/#enumdef-credentialmediationrequirement Conditional, } @@ -120,28 +139,31 @@ pub struct AllowCredentials { pub type_: String, /// The id of the credential. #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub id: Vec, + pub id: String, /// https://www.w3.org/TR/webauthn/#transport may be usb, nfc, ble, internal + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub transports: Option>, } /// https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub enum AuthenticatorTransport { - /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-usb - Usb, - /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-nfc - Nfc, /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-ble Ble, + /// Hybrid transport, formerly caBLE. Part of the level 3 draft specification. + /// https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid + Hybrid, /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-internal Internal, - /// Hybrid transport, formerly caBLE. Part of the level 3 draft specification. https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid - Hybrid, + /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-nfc + Nfc, /// Test transport; used for Windows 10. Test, /// An unknown transport was provided - it will be ignored. Unknown, + /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-usb + Usb, } /// https://www.w3.org/TR/webauthn/#dictdef-authenticatorselectioncriteria @@ -149,11 +171,15 @@ pub enum AuthenticatorTransport { pub struct AuthenticatorSelectionCriteria { /// How the authenticator should be attached to the client machine. Note this is only a hint. /// It is not enforced in anyway shape or form. https://www.w3.org/TR/webauthn/#attachment. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub authenticator_attachment: Option, /// Hint to the credential to create a resident key. Note this value should be a member of /// ResidentKeyRequirement, but client must ignore unknown values, treating an unknown value as /// if the member does not exist. /// https://www.w3.org/TR/webauthn-2/#dom-authenticatorselectioncriteria-residentkey. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub resident_key: Option, /// Hint to the credential to create a resident key. Note this can not be enforced or /// validated, so the authenticator may choose to ignore this parameter. @@ -165,15 +191,16 @@ pub struct AuthenticatorSelectionCriteria { pub user_verification: UserVerificationPolicy, } -/// The authenticator attachment hint. This is NOT enforced, and is only used to help a user select a relevant authenticator type. +/// The authenticator attachment hint. This is NOT enforced, and is only used to help a user select +/// a relevant authenticator type. /// /// https://www.w3.org/TR/webauthn/#attachment #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub enum AuthenticatorAttachment { - /// Request a device that is part of the machine aka inseperable. + /// Request a device that is part of the machine aka inseparable. /// https://www.w3.org/TR/webauthn/#attachment. Platform, - /// Request a device that can be seperated from the machine aka an external token. + /// Request a device that can be separated from the machine aka an external token. /// https://www.w3.org/TR/webauthn/#attachment. CrossPlatform, } @@ -197,12 +224,12 @@ pub enum ResidentKeyRequirement { /// https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub enum PublicKeyCredentialHints { - /// The credential is a removable security key. - SecurityKey, /// The credential is a platform authenticator. ClientDevice, /// The credential will come from an external device. Hybrid, + /// The credential is a removable security key. + SecurityKey, } /// https://www.w3.org/TR/webauthn/#enumdef-attestationconveyancepreference @@ -245,17 +272,28 @@ pub enum AttestationFormat { /// Implements [AuthenticatorExtensionsClientInputs] from the spec. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct RequestRegistrationExtensions { + /// ⚠️ - This extension result is always unsigned, and only indicates if the browser requests a + /// residentKey to be created. It has no bearing on the true rk state of the credential. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_props: Option, /// The credProtect extension options. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, - /// ⚠️ - Browsers do not support this! Uvm - pub uvm: Option, - /// ⚠️ - This extension result is always unsigned, and only indicates if the browser requests a residentKey to be created. It has no bearing on the true rk state of the credential. - pub cred_props: Option, - /// CTAP2.1 Minumum pin length. - pub min_pin_length: Option, /// ⚠️ - Browsers support the creation of the secret, but not the retrieval of it. CTAP2.1 /// create hmac secret. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub hmac_create_secret: Option, + /// CTAP2.1 Minimum pin length. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, + /// ⚠️ - Browsers do not support this! Uvm + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub uvm: Option, } /// The desired options for the client’s use of the credProtect extension @@ -265,7 +303,10 @@ pub struct RequestRegistrationExtensions { pub struct CredProtect { /// The credential policy to enact. pub credential_protection_policy: CredentialProtectionPolicy, - /// Whether it is better for the authenticator to fail to create a credential rather than ignore the protection policy If no value is provided, the client treats it as false. + /// Whether it is better for the authenticator to fail to create a credential rather than + /// ignore the protection policy If no value is provided, the client treats it as false. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub enforce_credential_protection_policy: Option, } @@ -285,7 +326,9 @@ pub enum CredentialProtectionPolicy { UserVerificationRequired = 3, } -/// Defines the User Authenticator Verification policy. This is documented https://w3c.github.io/webauthn/#enumdef-userverificationrequirement, and each variant lists it’s effects. +/// Defines the User Authenticator Verification policy. This is documented +/// https://w3c.github.io/webauthn/#enumdef-userverificationrequirement, and each variant lists +/// it’s effects. /// /// To be clear, Verification means that the Authenticator perform extra or supplementary /// interaction with the user to verify who they are. An example of this is Apple Touch Id required @@ -331,6 +374,9 @@ pub enum UserVerificationPolicy { /// However, in some cases use of this policy can lead to some credentials failing to verify /// correctly due to browser peripheral exchange bypasses. Preferred, + /// Discourage - but do not prevent - user verification from being supplied. Many CTAP devices + /// will attempt UV during registration but not authentication leading to user confusion. + DiscouragedDoNotUse, } /// A client response to a registration challenge. This contains all required information to assess @@ -351,7 +397,7 @@ pub struct UserPasskeyRegistrationFinishRequest { /// This is NEVER actually used in a real registration, because the true credential ID is taken /// from the attestation data. #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub raw_id: Vec, + pub raw_id: String, /// https://w3c.github.io/webauthn/#dom-publickeycredential-response. pub response: AuthenticatorAttestationResponseRaw, /// The type of credential. @@ -365,11 +411,13 @@ pub struct UserPasskeyRegistrationFinishRequest { pub struct AuthenticatorAttestationResponseRaw { /// https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-attestationobject. #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub attestation_object: Vec, + pub attestation_object: String, /// https://w3c.github.io/webauthn/#dom-authenticatorresponse-clientdatajson. #[schema(value_type = String, format = Binary, content_encoding = "base64")] pub client_data_json: String, /// https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-gettransports. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub transports: Option>, } @@ -378,15 +426,25 @@ pub struct AuthenticatorAttestationResponseRaw { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct RegistrationExtensionsClientOutputs { /// Indicates whether the client used the provided appid extension. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub appid: Option, /// Indicates if the client believes it created a resident key. This property is managed by the /// webbrowser, and is NOT SIGNED and CAN NOT be trusted! + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub cred_props: Option, /// Indicates if the client successfully applied a HMAC Secret. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, /// Indicates if the client successfully applied a credential protection policy. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, /// Indicates the current minimum PIN length. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, } @@ -394,7 +452,7 @@ pub struct RegistrationExtensionsClientOutputs { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct CredProps { /// A user agent supplied hint that this credential may have created a resident key. It is - /// retured from the user agent, not the authenticator meaning that this is an unreliable + /// returned from the user agent, not the authenticator meaning that this is an unreliable /// signal. /// /// Note that this extension is UNSIGNED and may have been altered by page javascript. diff --git a/src/identity/backends/sql/passkey.rs b/src/identity/backends/sql/passkey.rs index e5cfd0fe..3bf6bbfb 100644 --- a/src/identity/backends/sql/passkey.rs +++ b/src/identity/backends/sql/passkey.rs @@ -30,7 +30,9 @@ pub(super) async fn create>( let entry = webauthn_credential::ActiveModel { id: NotSet, user_id: Set(user_id.as_ref().to_string()), - credential_id: Set(passkey.cred_id().escape_ascii().to_string()), + credential_id: Set(serde_json::to_string(passkey.cred_id())? + .trim_matches('"') + .to_string()), passkey: Set(serde_json::to_string(&passkey)?), r#type: Set("cross-platform".to_string()), aaguid: NotSet,