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,