diff --git a/.github/actions/deploy_keystone/action.yml b/.github/actions/deploy_keystone/action.yml index b18ccd5d..a50cbd8b 100644 --- a/.github/actions/deploy_keystone/action.yml +++ b/.github/actions/deploy_keystone/action.yml @@ -38,7 +38,7 @@ runs: mkdir -p etc/fernet-keys cat < etc/keystone.conf [auth] - methods = password,token,openid,application_credential + methods = password,token,openid,application_credential,x509 [database] connection = postgresql://keystone:1234@postgres:5432/keystone [fernet_receipts] diff --git a/Cargo.lock b/Cargo.lock index 64271675..1bf513c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,9 +381,9 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "base64urlsafedata" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "215ee31f8a88f588c349ce2d20108b2ed96089b96b9c2b03775dc35dd72938e8" +checksum = "42f7f6be94fa637132933fd0a68b9140bcb60e3d46164cb68e82a2bb8d102b3a" dependencies = [ "base64 0.21.7", "pastey", @@ -417,6 +417,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -1447,6 +1453,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1532,6 +1539,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -2145,7 +2153,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", "redox_syscall", ] @@ -2338,7 +2346,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -2395,6 +2403,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2518,7 +2537,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -2617,6 +2636,7 @@ dependencies = [ "utoipa-swagger-ui", "uuid", "validator", + "webauthn-authenticator-rs", "webauthn-rs", "webauthn-rs-proto", ] @@ -3144,7 +3164,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -3468,7 +3488,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -3783,7 +3803,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -4190,7 +4210,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -4237,7 +4257,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -4396,7 +4416,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation", "system-configuration-sys", ] @@ -4655,6 +4675,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -4723,7 +4744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ "async-compression", - "bitflags", + "bitflags 2.10.0", "bytes", "futures-core", "futures-util", @@ -4754,9 +4775,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -4766,9 +4787,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -4777,9 +4798,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -5183,9 +5204,9 @@ dependencies = [ [[package]] name = "webauthn-attestation-ca" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77a2892ec44032e6c48dad9aad1b05fada09c346ada11d8d32db119b4b4f205" +checksum = "fafcf13f7dc1fb292ed4aea22cdd3757c285d7559e9748950ee390249da4da6b" dependencies = [ "base64urlsafedata", "openssl", @@ -5195,11 +5216,44 @@ dependencies = [ "uuid", ] +[[package]] +name = "webauthn-authenticator-rs" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b41ed08aba475a969094226ae0691a286686210ae497bb2c5d0ed722d8d526" +dependencies = [ + "async-stream", + "async-trait", + "base64 0.21.7", + "base64urlsafedata", + "bitflags 1.3.2", + "futures", + "hex", + "nom", + "num-derive", + "num-traits", + "openssl", + "openssl-sys", + "serde", + "serde_bytes", + "serde_cbor_2", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", + "unicode-normalization", + "url", + "uuid", + "webauthn-rs-core", + "webauthn-rs-proto", +] + [[package]] name = "webauthn-rs" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7c3a2f9c8bddd524e47bbd427bcf3a28aa074de55d74470b42a91a41937b8e" +checksum = "1b24d082d3360258fefb6ffe56123beef7d6868c765c779f97b7a2fcf06727f8" dependencies = [ "base64urlsafedata", "serde", @@ -5211,9 +5265,9 @@ dependencies = [ [[package]] name = "webauthn-rs-core" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f1d80f3146382529fe70a3ab5d0feb2413a015204ed7843f9377cd39357fc4" +checksum = "15784340a24c170ce60567282fb956a0938742dbfbf9eff5df793a686a009b8b" dependencies = [ "base64 0.21.7", "base64urlsafedata", @@ -5222,8 +5276,8 @@ dependencies = [ "nom", "openssl", "openssl-sys", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", + "rand_chacha 0.9.0", "serde", "serde_cbor_2", "serde_json", @@ -5238,9 +5292,9 @@ dependencies = [ [[package]] name = "webauthn-rs-proto" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e786894f89facb9aaf1c5f6559670236723c98382e045521c76f3d5ca5047bd" +checksum = "16a1fb2580ce73baa42d3011a24de2ceab0d428de1879ece06e02e8c416e497c" dependencies = [ "base64 0.21.7", "base64urlsafedata", diff --git a/Cargo.toml b/Cargo.toml index 1a6f7b60..1a6bc007 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ serde_urlencoded = { version = "0.7" } thirtyfour = "0.36" tracing-test = { version = "0.2", features = ["no-env-filter"] } url = { version = "2.5" } +webauthn-authenticator-rs = { version = "0.5", features = ["softtoken"] } webauthn-rs = { version = "0.5", features = ["danger-credential-internals"] } [features] diff --git a/src/db_migration/m20250301_000001_passkey.rs b/src/db_migration/m20250301_000001_passkey.rs index d5525b58..50fccbcc 100644 --- a/src/db_migration/m20250301_000001_passkey.rs +++ b/src/db_migration/m20250301_000001_passkey.rs @@ -31,7 +31,7 @@ impl MigrationTrait for Migration { .col(pk_auto(WebauthnCredential::Id)) .col(string_len(WebauthnCredential::UserId, 64)) .col(string_len(WebauthnCredential::CredentialId, 1024)) - .col(string_len(WebauthnCredential::Description, 64)) + .col(string_len_null(WebauthnCredential::Description, 64)) .col(text(WebauthnCredential::Passkey)) .col(unsigned(WebauthnCredential::Counter)) .col(string_len(WebauthnCredential::Type, 25)) diff --git a/src/webauthn/api.rs b/src/webauthn/api.rs index 8b742baa..022bd374 100644 --- a/src/webauthn/api.rs +++ b/src/webauthn/api.rs @@ -24,7 +24,7 @@ use crate::keystone::ServiceState; mod auth; mod register; -mod types; +pub mod types; use crate::webauthn::driver::SqlDriver; use types::{CombinedExtensionState, ExtensionState}; diff --git a/src/webauthn/api/types.rs b/src/webauthn/api/types.rs index 606cf4e6..95a0b71f 100644 --- a/src/webauthn/api/types.rs +++ b/src/webauthn/api/types.rs @@ -25,8 +25,31 @@ use crate::policy::Policy; use crate::webauthn::{WebauthnError, driver::SqlDriver}; +mod allow_credentials; +mod attestation_conveyance_preference; +mod attestation_format; pub mod auth; +mod authentication_extensions_client_outputs; +mod authenticator_assertion_response_raw; +mod authenticator_attachment; +mod authenticator_selection_criteria; +mod authenticator_transport; +mod cred_protect; +mod credential_protection_policy; +mod hmac_get_secret_input; +mod hmac_get_secret_output; +mod pub_key_cred_params; +mod public_key_credential_creation_options; +mod public_key_credential_descriptor; +mod public_key_credential_hints; +mod public_key_credential_request_options; pub mod register; +mod relying_party; +mod request_authentication_extensions; +mod request_registration_extension; +mod resident_key_requirement; +mod user; +mod user_verification_policy; /// WebAuthN extension state. #[derive()] diff --git a/src/webauthn/api/types/allow_credentials.rs b/src/webauthn/api/types/allow_credentials.rs new file mode 100644 index 00000000..15d999bb --- /dev/null +++ b/src/webauthn/api/types/allow_credentials.rs @@ -0,0 +1,60 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::authenticator_transport::AuthenticatorTransport; +use crate::webauthn::WebauthnError; + +/// A descriptor of a credential that can be used. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct AllowCredentials { + /// The id of the credential. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub id: String, + /// may be usb, nfc, ble, internal + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub transports: Option>, + /// The type of credential. + pub type_: String, +} + +impl From for AllowCredentials { + fn from(val: webauthn_rs_proto::options::AllowCredentials) -> Self { + Self { + id: URL_SAFE.encode(val.id), + transports: val + .transports + .map(|tr| tr.into_iter().map(Into::into).collect::>()), + type_: val.type_, + } + } +} + +impl TryFrom for webauthn_rs_proto::options::AllowCredentials { + type Error = WebauthnError; + + fn try_from(val: AllowCredentials) -> Result { + Ok(Self { + id: URL_SAFE.decode(val.id)?.into(), + transports: val + .transports + .map(|tr| tr.into_iter().map(Into::into).collect::>()), + type_: val.type_, + }) + } +} diff --git a/src/webauthn/api/types/attestation_conveyance_preference.rs b/src/webauthn/api/types/attestation_conveyance_preference.rs new file mode 100644 index 00000000..16d01ac4 --- /dev/null +++ b/src/webauthn/api/types/attestation_conveyance_preference.rs @@ -0,0 +1,65 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum AttestationConveyancePreference { + /// Do not request attestation. + /// . + None, + /// Request attestation in a semi-anonymized form. + /// . + Indirect, + /// Request attestation in a direct form. + /// . + Direct, +} + +impl From + for AttestationConveyancePreference +{ + fn from(val: webauthn_rs_proto::options::AttestationConveyancePreference) -> Self { + match val { + webauthn_rs_proto::options::AttestationConveyancePreference::Direct => { + AttestationConveyancePreference::Direct + } + webauthn_rs_proto::options::AttestationConveyancePreference::Indirect => { + AttestationConveyancePreference::Indirect + } + webauthn_rs_proto::options::AttestationConveyancePreference::None => { + AttestationConveyancePreference::None + } + } + } +} + +impl From + for webauthn_rs_proto::options::AttestationConveyancePreference +{ + fn from(val: AttestationConveyancePreference) -> Self { + match val { + AttestationConveyancePreference::Direct => { + webauthn_rs_proto::options::AttestationConveyancePreference::Direct + } + AttestationConveyancePreference::Indirect => { + webauthn_rs_proto::options::AttestationConveyancePreference::Indirect + } + AttestationConveyancePreference::None => { + webauthn_rs_proto::options::AttestationConveyancePreference::None + } + } + } +} diff --git a/src/webauthn/api/types/attestation_format.rs b/src/webauthn/api/types/attestation_format.rs new file mode 100644 index 00000000..a9952385 --- /dev/null +++ b/src/webauthn/api/types/attestation_format.rs @@ -0,0 +1,76 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// The type of attestation on the credential. +/// +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum AttestationFormat { + /// Packed attestation. + Packed, + /// TPM attestation (like Microsoft). + Tpm, + /// Android hardware attestation. + AndroidKey, + /// Older Android Safety Net. + AndroidSafetyNet, + /// Old U2F attestation type. + FIDOU2F, + /// Apple touchID/faceID. + AppleAnonymous, + /// No attestation. + None, +} + +impl From for AttestationFormat { + fn from(value: webauthn_rs_proto::options::AttestationFormat) -> Self { + match value { + 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, + } + } +} + +impl From for webauthn_rs_proto::options::AttestationFormat { + fn from(value: AttestationFormat) -> Self { + match value { + AttestationFormat::AndroidKey => { + webauthn_rs_proto::options::AttestationFormat::AndroidKey + } + AttestationFormat::AndroidSafetyNet => { + webauthn_rs_proto::options::AttestationFormat::AndroidSafetyNet + } + AttestationFormat::AppleAnonymous => { + webauthn_rs_proto::options::AttestationFormat::AppleAnonymous + } + AttestationFormat::FIDOU2F => webauthn_rs_proto::options::AttestationFormat::FIDOU2F, + AttestationFormat::None => webauthn_rs_proto::options::AttestationFormat::None, + AttestationFormat::Packed => webauthn_rs_proto::options::AttestationFormat::Packed, + AttestationFormat::Tpm => webauthn_rs_proto::options::AttestationFormat::Tpm, + } + } +} diff --git a/src/webauthn/api/types/auth.rs b/src/webauthn/api/types/auth.rs index 7ed0007b..b9437d28 100644 --- a/src/webauthn/api/types/auth.rs +++ b/src/webauthn/api/types/auth.rs @@ -18,7 +18,10 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use validator::Validate; -use crate::api::KeystoneApiError; +use super::authentication_extensions_client_outputs::AuthenticationExtensionsClientOutputs; +use super::authenticator_assertion_response_raw::AuthenticatorAssertionResponseRaw; +use super::public_key_credential_request_options::PublicKeyCredentialRequestOptions; +use crate::webauthn::WebauthnError; /// Request for initialization of the passkey authentication. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] @@ -52,34 +55,16 @@ pub struct PasskeyAuthenticationStartResponse { pub mediation: Option, } -/// The requested options for the authentication. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct PublicKeyCredentialRequestOptions { - /// The set of credentials that are allowed to sign this challenge. - #[validate(nested)] - pub allow_credentials: Vec, - /// The challenge that should be signed by the authenticator. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub challenge: String, - /// extensions. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - #[validate(nested)] - pub extensions: Option, - /// Hints defining which types credentials may be used in this operation. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - pub hints: Option>, - /// The relying party ID. - #[validate(length(max = 64))] - pub rp_id: String, - /// The timeout for the authenticator in case of no interaction. - pub timeout: Option, - /// The verification policy the browser will request. - pub user_verification: UserVerificationPolicy, +impl From for PasskeyAuthenticationStartResponse { + fn from(val: webauthn_rs::prelude::RequestChallengeResponse) -> Self { + Self { + public_key: val.public_key.into(), + mediation: val.mediation.map(Into::into), + } + } } -/// Request in residentkey workflows that conditional mediation should be used +/// Request in resident key workflows that conditional mediation should be used /// in the UI, or not. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub enum Mediation { @@ -89,144 +74,20 @@ pub enum Mediation { Conditional, } -/// A descriptor of a credential that can be used. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct AllowCredentials { - /// The id of the credential. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub id: String, - /// may be usb, nfc, ble, internal - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - pub transports: Option>, - /// The type of credential. - pub type_: String, -} - -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum AuthenticatorTransport { - /// - Ble, - /// Hybrid transport, formerly caBLE. Part of the level 3 draft specification. - Hybrid, - /// - Internal, - /// - Nfc, - /// Test transport; used for Windows 10. - Test, - /// An unknown transport was provided - it will be ignored. - Unknown, - /// - Usb, -} - -/// Defines the User Authenticator Verification policy. This is documented -/// , 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 a fingerprint to be verified, or a yubico -/// device requiring a pin in addition to a touch event. -/// -/// An example of a non-verified interaction is a yubico device with no pin -/// where touch is the only interaction - we only verify a user is present, but -/// we don't have extra details to the legitimacy of that user. -/// -/// As UserVerificationPolicy is only used in credential registration, this -/// stores the verification state of the credential in the persisted credential. -/// These persisted credentials define which UserVerificationPolicy is issued -/// during authentications. -/// -/// IMPORTANT - Due to limitations of the webauthn specification, CTAP devices, -/// and browser implementations, the only secure choice as an RP is required. -/// -/// ⚠️ WARNING - discouraged is marked with a warning, as some authenticators -/// will FORCE verification during registration but NOT during authentication. -/// This makes it impossible for a relying party to consistently enforce user -/// verification, which can confuse users and lead them to distrust user -/// verification is being enforced. -/// -/// ⚠️ WARNING - preferred can lead to authentication errors in some cases due -/// to browser peripheral exchange allowing authentication verification -/// bypass. Webauthn RS is not vulnerable to these bypasses due to our -/// tracking of UV during registration through authentication, however -/// preferred can cause legitimate credentials to not prompt for UV correctly -/// due to browser perhipheral exchange leading Webauthn RS to deny them in what -/// should otherwise be legitimate operations. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum UserVerificationPolicy { - /// Require user verification bit to be set, and fail the registration or - /// authentication if false. If the authenticator is not able to perform - /// verification, it will not be usable with this policy. - /// - /// This policy is the default as it is the only secure and consistent user - /// verification option. - Required, - /// Prefer UV if possible, but ignore if not present. In other webauthn - /// deployments this is bypassable as it implies the library will not - /// check UV is set correctly for this credential. Webauthn-RS is not - /// vulnerable to this as we check the UV state always based on - /// it's presence at registration. - /// - /// 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 hint as to the class of device that is expected to fufil this operation. -/// -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum PublicKeyCredentialHint { - /// The credential is a platform authenticator. - ClientDevice, - /// The credential will come from an external device. - Hybrid, - /// The credential is a removable security key. - SecurityKey, -} - -/// Extension option inputs for PublicKeyCredentialRequestOptions -/// -/// Implements AuthenticatorExtensionsClientInputs from the spec -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct RequestAuthenticationExtensions { - /// The appid extension options. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - pub appid: Option, - /// ⚠️ - Browsers do not support this! - /// Hmac get secret. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - #[validate(nested)] - pub hmac_get_secret: Option, - /// ⚠️ - Browsers do not support this! Uvm. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - pub uvm: Option, +impl From for Mediation { + fn from(value: webauthn_rs_proto::auth::Mediation) -> Self { + match value { + webauthn_rs_proto::auth::Mediation::Conditional => Mediation::Conditional, + } + } } -/// The inputs to the hmac secret if it was created during registration. -/// -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct HmacGetSecretInput { - /// Retrieve a symmetric secrets from the authenticator with this input. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub output1: String, - /// Rotate the secret in the same operation. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - #[serde(skip_serializing_if = "Option::is_none")] - pub output2: Option, +impl From for webauthn_rs_proto::auth::Mediation { + fn from(value: Mediation) -> Self { + match value { + Mediation::Conditional => webauthn_rs_proto::auth::Mediation::Conditional, + } + } } /// A client response to an authentication challenge. This contains all required @@ -255,98 +116,8 @@ pub struct PasskeyAuthenticationFinishRequest { pub user_id: String, } -/// [AuthenticatorAssertionResponseRaw](https://w3c.github.io/webauthn/#authenticatorassertionresponse) -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct AuthenticatorAssertionResponseRaw { - /// Raw authenticator data. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub authenticator_data: String, - /// Signed client data. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub client_data_json: String, - /// Signature. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub signature: String, - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - /// Optional userhandle. - pub user_handle: Option, -} - -/// [AuthenticationExtensionsClientOutputs](https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs) -/// -/// The default option here for Options are None, so it can be derived -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct AuthenticationExtensionsClientOutputs { - /// Indicates whether the client used the provided appid extension. - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - pub appid: Option, - /// The response to a hmac get secret request. - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - #[validate(nested)] - pub hmac_get_secret: Option, -} - -/// The response to a hmac get secret request. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct HmacGetSecretOutput { - /// Output of HMAC(Salt 1 || Client Secret). - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - pub output1: String, - /// Output of HMAC(Salt 2 || Client Secret). - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false, value_type = String, format = Binary, content_encoding = "base64")] - #[validate(required)] - pub output2: Option, -} - -impl TryFrom for webauthn_rs_proto::extensions::HmacGetSecretOutput { - type Error = KeystoneApiError; - fn try_from(val: HmacGetSecretOutput) -> Result { - Ok(Self { - output1: URL_SAFE.decode(val.output1)?.into(), - output2: val - .output2 - .map(|s2| URL_SAFE.decode(s2)) - .transpose()? - .map(Into::into), - }) - } -} - -impl TryFrom - for webauthn_rs_proto::extensions::AuthenticationExtensionsClientOutputs -{ - type Error = KeystoneApiError; - fn try_from(val: AuthenticationExtensionsClientOutputs) -> Result { - Ok(Self { - appid: val.appid, - hmac_get_secret: val.hmac_get_secret.map(TryInto::try_into).transpose()?, - }) - } -} - -impl TryFrom - for webauthn_rs_proto::auth::AuthenticatorAssertionResponseRaw -{ - type Error = KeystoneApiError; - fn try_from(val: AuthenticatorAssertionResponseRaw) -> Result { - Ok(Self { - authenticator_data: URL_SAFE.decode(val.authenticator_data)?.into(), - client_data_json: URL_SAFE.decode(val.client_data_json)?.into(), - signature: URL_SAFE.decode(val.signature)?.into(), - user_handle: val - .user_handle - .map(|uh| URL_SAFE.decode(uh)) - .transpose()? - .map(Into::into), - }) - } -} - impl TryFrom for webauthn_rs::prelude::PublicKeyCredential { - type Error = KeystoneApiError; + type Error = WebauthnError; fn try_from(req: PasskeyAuthenticationFinishRequest) -> Result { Ok(webauthn_rs::prelude::PublicKeyCredential { id: req.id, @@ -357,106 +128,3 @@ impl TryFrom for webauthn_rs::prelude::Publi }) } } - -impl From for HmacGetSecretInput { - fn from(val: webauthn_rs_proto::extensions::HmacGetSecretInput) -> Self { - Self { - output1: URL_SAFE.encode(val.output1), - output2: val.output2.map(|s2| URL_SAFE.encode(s2)), - } - } -} - -impl From - for RequestAuthenticationExtensions -{ - fn from(val: webauthn_rs_proto::extensions::RequestAuthenticationExtensions) -> Self { - Self { - appid: val.appid, - hmac_get_secret: val.hmac_get_secret.map(Into::into), - uvm: val.uvm, - } - } -} - -impl From for AuthenticatorTransport { - fn from(val: webauthn_rs_proto::options::AuthenticatorTransport) -> Self { - match val { - webauthn_rs_proto::options::AuthenticatorTransport::Ble => Self::Ble, - webauthn_rs_proto::options::AuthenticatorTransport::Hybrid => Self::Hybrid, - webauthn_rs_proto::options::AuthenticatorTransport::Internal => Self::Internal, - webauthn_rs_proto::options::AuthenticatorTransport::Nfc => Self::Nfc, - webauthn_rs_proto::options::AuthenticatorTransport::Test => Self::Test, - webauthn_rs_proto::options::AuthenticatorTransport::Unknown => Self::Unknown, - webauthn_rs_proto::options::AuthenticatorTransport::Usb => Self::Usb, - } - } -} -impl From for UserVerificationPolicy { - fn from(val: webauthn_rs_proto::options::UserVerificationPolicy) -> Self { - match val { - webauthn_rs_proto::options::UserVerificationPolicy::Required => Self::Required, - webauthn_rs_proto::options::UserVerificationPolicy::Preferred => Self::Preferred, - webauthn_rs_proto::options::UserVerificationPolicy::Discouraged_DO_NOT_USE => { - Self::DiscouragedDoNotUse - } - } - } -} - -impl From for PublicKeyCredentialHint { - fn from(val: webauthn_rs_proto::options::PublicKeyCredentialHints) -> Self { - match val { - webauthn_rs_proto::options::PublicKeyCredentialHints::ClientDevice => { - Self::ClientDevice - } - webauthn_rs_proto::options::PublicKeyCredentialHints::Hybrid => Self::Hybrid, - webauthn_rs_proto::options::PublicKeyCredentialHints::SecurityKey => Self::SecurityKey, - } - } -} - -impl From for AllowCredentials { - fn from(val: webauthn_rs_proto::options::AllowCredentials) -> Self { - Self { - id: URL_SAFE.encode(val.id), - transports: val - .transports - .map(|tr| tr.into_iter().map(Into::into).collect::>()), - type_: val.type_, - } - } -} - -impl From - for PublicKeyCredentialRequestOptions -{ - fn from(val: webauthn_rs_proto::auth::PublicKeyCredentialRequestOptions) -> Self { - Self { - allow_credentials: val - .allow_credentials - .into_iter() - .map(Into::into) - .collect::>(), - challenge: URL_SAFE.encode(val.challenge), - extensions: val.extensions.map(Into::into), - hints: val - .hints - .map(|hints| hints.into_iter().map(Into::into).collect::>()), - rp_id: val.rp_id, - timeout: val.timeout, - user_verification: val.user_verification.into(), - } - } -} - -impl From for PasskeyAuthenticationStartResponse { - fn from(val: webauthn_rs::prelude::RequestChallengeResponse) -> Self { - Self { - public_key: val.public_key.into(), - mediation: val.mediation.map(|med| match med { - webauthn_rs_proto::auth::Mediation::Conditional => Mediation::Conditional, - }), - } - } -} diff --git a/src/webauthn/api/types/authentication_extensions_client_outputs.rs b/src/webauthn/api/types/authentication_extensions_client_outputs.rs new file mode 100644 index 00000000..6f137e52 --- /dev/null +++ b/src/webauthn/api/types/authentication_extensions_client_outputs.rs @@ -0,0 +1,58 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::hmac_get_secret_output::HmacGetSecretOutput; +use crate::webauthn::WebauthnError; + +/// [AuthenticationExtensionsClientOutputs](https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs) +/// +/// The default option here for Options are None, so it can be derived +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct AuthenticationExtensionsClientOutputs { + /// Indicates whether the client used the provided appid extension. + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + pub appid: Option, + /// The response to a hmac get secret request. + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + #[validate(nested)] + pub hmac_get_secret: Option, +} + +impl TryFrom + for webauthn_rs_proto::extensions::AuthenticationExtensionsClientOutputs +{ + type Error = WebauthnError; + fn try_from(val: AuthenticationExtensionsClientOutputs) -> Result { + Ok(Self { + appid: val.appid, + hmac_get_secret: val.hmac_get_secret.map(TryInto::try_into).transpose()?, + }) + } +} + +impl From + for AuthenticationExtensionsClientOutputs +{ + fn from(value: webauthn_rs_proto::extensions::AuthenticationExtensionsClientOutputs) -> Self { + Self { + appid: value.appid, + hmac_get_secret: value.hmac_get_secret.map(Into::into), + } + } +} diff --git a/src/webauthn/api/types/authenticator_assertion_response_raw.rs b/src/webauthn/api/types/authenticator_assertion_response_raw.rs new file mode 100644 index 00000000..4072de56 --- /dev/null +++ b/src/webauthn/api/types/authenticator_assertion_response_raw.rs @@ -0,0 +1,67 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use crate::webauthn::WebauthnError; + +/// [AuthenticatorAssertionResponseRaw](https://w3c.github.io/webauthn/#authenticatorassertionresponse) +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct AuthenticatorAssertionResponseRaw { + /// Raw authenticator data. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub authenticator_data: String, + /// Signed client data. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub client_data_json: String, + /// Signature. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub signature: String, + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + /// Optional user handle. + pub user_handle: Option, +} + +impl TryFrom + for webauthn_rs_proto::auth::AuthenticatorAssertionResponseRaw +{ + type Error = WebauthnError; + fn try_from(val: AuthenticatorAssertionResponseRaw) -> Result { + Ok(Self { + authenticator_data: URL_SAFE.decode(val.authenticator_data)?.into(), + client_data_json: URL_SAFE.decode(val.client_data_json)?.into(), + signature: URL_SAFE.decode(val.signature)?.into(), + user_handle: val + .user_handle + .map(|uh| URL_SAFE.decode(uh)) + .transpose()? + .map(Into::into), + }) + } +} + +impl From + for AuthenticatorAssertionResponseRaw +{ + fn from(val: webauthn_rs_proto::auth::AuthenticatorAssertionResponseRaw) -> Self { + Self { + authenticator_data: URL_SAFE.encode(val.authenticator_data), + client_data_json: URL_SAFE.encode(val.client_data_json), + signature: URL_SAFE.encode(val.signature), + user_handle: val.user_handle.map(|uh| URL_SAFE.encode(uh)), + } + } +} diff --git a/src/webauthn/api/types/authenticator_attachment.rs b/src/webauthn/api/types/authenticator_attachment.rs new file mode 100644 index 00000000..13834e31 --- /dev/null +++ b/src/webauthn/api/types/authenticator_attachment.rs @@ -0,0 +1,55 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// The authenticator attachment hint. This is NOT enforced, and is only used to +/// help a user select a relevant authenticator type. +/// +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum AuthenticatorAttachment { + /// Request a device that is part of the machine aka inseparable. + /// . + Platform, + /// Request a device that can be separated from the machine aka an external + /// token. . + CrossPlatform, +} + +impl From for webauthn_rs_proto::options::AuthenticatorAttachment { + fn from(value: AuthenticatorAttachment) -> Self { + match value { + AuthenticatorAttachment::CrossPlatform => { + webauthn_rs_proto::options::AuthenticatorAttachment::CrossPlatform + } + AuthenticatorAttachment::Platform => { + webauthn_rs_proto::options::AuthenticatorAttachment::Platform + } + } + } +} + +impl From for AuthenticatorAttachment { + fn from(value: webauthn_rs_proto::options::AuthenticatorAttachment) -> Self { + match value { + webauthn_rs_proto::options::AuthenticatorAttachment::CrossPlatform => { + AuthenticatorAttachment::CrossPlatform + } + webauthn_rs_proto::options::AuthenticatorAttachment::Platform => { + AuthenticatorAttachment::Platform + } + } + } +} diff --git a/src/webauthn/api/types/authenticator_selection_criteria.rs b/src/webauthn/api/types/authenticator_selection_criteria.rs new file mode 100644 index 00000000..8450cb02 --- /dev/null +++ b/src/webauthn/api/types/authenticator_selection_criteria.rs @@ -0,0 +1,72 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::authenticator_attachment::AuthenticatorAttachment; +use super::resident_key_requirement::ResidentKeyRequirement; +use super::user_verification_policy::UserVerificationPolicy; + +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +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. . + #[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. . + #[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. . + pub require_resident_key: bool, + /// The user verification level to request during registration. Depending on + /// if this authenticator provides verification may affect future + /// interactions as this is associated to the credential during + /// registration. + pub user_verification: UserVerificationPolicy, +} + +impl From + for webauthn_rs_proto::options::AuthenticatorSelectionCriteria +{ + fn from(value: AuthenticatorSelectionCriteria) -> Self { + Self { + authenticator_attachment: value.authenticator_attachment.map(Into::into), + resident_key: value.resident_key.map(Into::into), + require_resident_key: value.require_resident_key, + user_verification: value.user_verification.into(), + } + } +} + +impl From + for AuthenticatorSelectionCriteria +{ + fn from(value: webauthn_rs_proto::options::AuthenticatorSelectionCriteria) -> Self { + Self { + authenticator_attachment: value.authenticator_attachment.map(Into::into), + require_resident_key: value.require_resident_key, + resident_key: value.resident_key.map(Into::into), + user_verification: value.user_verification.into(), + } + } +} diff --git a/src/webauthn/api/types/authenticator_transport.rs b/src/webauthn/api/types/authenticator_transport.rs new file mode 100644 index 00000000..29336c26 --- /dev/null +++ b/src/webauthn/api/types/authenticator_transport.rs @@ -0,0 +1,79 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum AuthenticatorTransport { + /// + Ble, + /// Hybrid transport, formerly caBLE. Part of the level 3 draft + /// specification. + Hybrid, + /// + Internal, + /// + Nfc, + /// Test transport; used for Windows 10. + Test, + /// An unknown transport was provided - it will be ignored. + Unknown, + /// + Usb, +} + +impl From for webauthn_rs_proto::options::AuthenticatorTransport { + fn from(value: AuthenticatorTransport) -> Self { + match value { + AuthenticatorTransport::Ble => webauthn_rs_proto::options::AuthenticatorTransport::Ble, + AuthenticatorTransport::Hybrid => { + webauthn_rs_proto::options::AuthenticatorTransport::Hybrid + } + AuthenticatorTransport::Internal => { + webauthn_rs_proto::options::AuthenticatorTransport::Internal + } + AuthenticatorTransport::Nfc => webauthn_rs_proto::options::AuthenticatorTransport::Nfc, + AuthenticatorTransport::Test => { + webauthn_rs_proto::options::AuthenticatorTransport::Test + } + AuthenticatorTransport::Unknown => { + webauthn_rs_proto::options::AuthenticatorTransport::Unknown + } + AuthenticatorTransport::Usb => webauthn_rs_proto::options::AuthenticatorTransport::Usb, + } + } +} + +impl From for AuthenticatorTransport { + fn from(value: webauthn_rs_proto::options::AuthenticatorTransport) -> Self { + match value { + 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::Test => { + AuthenticatorTransport::Test + } + webauthn_rs_proto::options::AuthenticatorTransport::Unknown => { + AuthenticatorTransport::Unknown + } + webauthn_rs_proto::options::AuthenticatorTransport::Usb => AuthenticatorTransport::Usb, + } + } +} diff --git a/src/webauthn/api/types/cred_protect.rs b/src/webauthn/api/types/cred_protect.rs new file mode 100644 index 00000000..332351f9 --- /dev/null +++ b/src/webauthn/api/types/cred_protect.rs @@ -0,0 +1,51 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::credential_protection_policy::CredentialProtectionPolicy; + +/// The desired options for the client's use of the credProtect extension +/// +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct CredProtect { + /// The credential policy to enforce. + 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. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub enforce_credential_protection_policy: Option, +} + +impl From for webauthn_rs_proto::extensions::CredProtect { + fn from(value: CredProtect) -> Self { + Self { + credential_protection_policy: value.credential_protection_policy.into(), + enforce_credential_protection_policy: value.enforce_credential_protection_policy, + } + } +} + +impl From for CredProtect { + fn from(value: webauthn_rs_proto::extensions::CredProtect) -> Self { + Self { + credential_protection_policy: value.credential_protection_policy.into(), + enforce_credential_protection_policy: value.enforce_credential_protection_policy, + } + } +} diff --git a/src/webauthn/api/types/credential_protection_policy.rs b/src/webauthn/api/types/credential_protection_policy.rs new file mode 100644 index 00000000..69a76cdf --- /dev/null +++ b/src/webauthn/api/types/credential_protection_policy.rs @@ -0,0 +1,64 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// Valid credential protection policies +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[repr(u8)] +pub enum CredentialProtectionPolicy { + /// This reflects “FIDO_2_0” semantics. In this configuration, performing + /// some form of user verification is optional with or without + /// credentialID list. This is the default state of the credential if + /// the extension is not specified. + Optional = 1, + /// In this configuration, credential is discovered only when its + /// credentialID is provided by the platform or when some form of user + /// verification is performed. + OptionalWithCredentialIDList = 2, + /// This reflects that discovery and usage of the credential MUST be + /// preceded by some form of user verification. + Required = 3, +} + +impl From + for webauthn_rs_proto::extensions::CredentialProtectionPolicy +{ + fn from(value: CredentialProtectionPolicy) -> Self { + match value { + CredentialProtectionPolicy::Optional => { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional + } + CredentialProtectionPolicy::OptionalWithCredentialIDList => { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList + } + CredentialProtectionPolicy::Required => { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired + } + } + } +} + +impl From + for CredentialProtectionPolicy +{ + fn from(value: webauthn_rs_proto::extensions::CredentialProtectionPolicy) -> Self { + match value { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional => CredentialProtectionPolicy::Optional, + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => CredentialProtectionPolicy::OptionalWithCredentialIDList, + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired => CredentialProtectionPolicy::Required, + + } + } +} diff --git a/src/webauthn/api/types/hmac_get_secret_input.rs b/src/webauthn/api/types/hmac_get_secret_input.rs new file mode 100644 index 00000000..4473c051 --- /dev/null +++ b/src/webauthn/api/types/hmac_get_secret_input.rs @@ -0,0 +1,57 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use crate::webauthn::WebauthnError; + +/// The inputs to the hmac secret if it was created during registration. +/// +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct HmacGetSecretInput { + /// Retrieve a symmetric secrets from the authenticator with this input. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub output1: String, + /// Rotate the secret in the same operation. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + #[serde(skip_serializing_if = "Option::is_none")] + pub output2: Option, +} + +impl From for HmacGetSecretInput { + fn from(val: webauthn_rs_proto::extensions::HmacGetSecretInput) -> Self { + Self { + output1: URL_SAFE.encode(val.output1), + output2: val.output2.map(|s2| URL_SAFE.encode(s2)), + } + } +} + +impl TryFrom for webauthn_rs_proto::extensions::HmacGetSecretInput { + type Error = WebauthnError; + + fn try_from(val: HmacGetSecretInput) -> Result { + Ok(Self { + output1: URL_SAFE.decode(val.output1)?.into(), + output2: val + .output2 + .map(|s2| URL_SAFE.decode(s2)) + .transpose()? + .map(Into::into), + }) + } +} diff --git a/src/webauthn/api/types/hmac_get_secret_output.rs b/src/webauthn/api/types/hmac_get_secret_output.rs new file mode 100644 index 00000000..ac677eac --- /dev/null +++ b/src/webauthn/api/types/hmac_get_secret_output.rs @@ -0,0 +1,56 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use crate::webauthn::WebauthnError; + +/// The response to a hmac get secret request. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct HmacGetSecretOutput { + /// Output of HMAC(Salt 1 || Client Secret). + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub output1: String, + /// Output of HMAC(Salt 2 || Client Secret). + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false, value_type = String, format = Binary, content_encoding = "base64")] + #[validate(required)] + pub output2: Option, +} + +impl TryFrom for webauthn_rs_proto::extensions::HmacGetSecretOutput { + type Error = WebauthnError; + + fn try_from(val: HmacGetSecretOutput) -> Result { + Ok(Self { + output1: URL_SAFE.decode(val.output1)?.into(), + output2: val + .output2 + .map(|s2| URL_SAFE.decode(s2)) + .transpose()? + .map(Into::into), + }) + } +} + +impl From for HmacGetSecretOutput { + fn from(val: webauthn_rs_proto::extensions::HmacGetSecretOutput) -> Self { + Self { + output1: URL_SAFE.encode(val.output1), + output2: val.output2.map(|s2| URL_SAFE.encode(s2)), + } + } +} diff --git a/src/webauthn/api/types/pub_key_cred_params.rs b/src/webauthn/api/types/pub_key_cred_params.rs new file mode 100644 index 00000000..f273dd50 --- /dev/null +++ b/src/webauthn/api/types/pub_key_cred_params.rs @@ -0,0 +1,43 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +/// Public key cryptographic parameters +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct PubKeyCredParams { + /// The algorithm in use defined by CASE. + pub alg: i64, + /// The type of public-key credential. + pub type_: String, +} + +impl From for PubKeyCredParams { + fn from(value: webauthn_rs_proto::options::PubKeyCredParams) -> Self { + Self { + alg: value.alg, + type_: value.type_, + } + } +} + +impl From for webauthn_rs_proto::options::PubKeyCredParams { + fn from(value: PubKeyCredParams) -> Self { + Self { + alg: value.alg, + type_: value.type_, + } + } +} diff --git a/src/webauthn/api/types/public_key_credential_creation_options.rs b/src/webauthn/api/types/public_key_credential_creation_options.rs new file mode 100644 index 00000000..5bdc529d --- /dev/null +++ b/src/webauthn/api/types/public_key_credential_creation_options.rs @@ -0,0 +1,146 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::attestation_conveyance_preference::AttestationConveyancePreference; +use super::attestation_format::AttestationFormat; +use super::authenticator_selection_criteria::AuthenticatorSelectionCriteria; +use super::pub_key_cred_params::PubKeyCredParams; +use super::public_key_credential_descriptor::PublicKeyCredentialDescriptor; +use super::public_key_credential_hints::PublicKeyCredentialHints; +use super::relying_party::RelyingParty; +use super::request_registration_extension::RequestRegistrationExtensions; +use super::user::User; +use crate::webauthn::WebauthnError; + +/// The requested options for the authentication. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct PublicKeyCredentialCreationOptions { + /// 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")] + #[validate(nested)] + 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")] + #[validate(nested)] + pub exclude_credentials: Option>, + /// extensions. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] + 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. + #[validate(nested)] + pub pub_key_cred_params: Vec, + /// The relying party + #[validate(nested)] + pub rp: RelyingParty, + /// The timeout for the authenticator in case of no interaction. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + #[validate(range(min = 1))] + pub timeout: Option, + /// The user. + #[validate(nested)] + pub user: User, +} + +impl TryFrom + for PublicKeyCredentialCreationOptions +{ + type Error = WebauthnError; + fn try_from( + value: webauthn_rs_proto::attest::PublicKeyCredentialCreationOptions, + ) -> Result { + Ok(Self { + attestation: value.attestation.map(Into::into), + attestation_formats: value + .attestation_formats + .map(|afs| afs.into_iter().map(Into::into).collect::>()), + authenticator_selection: value.authenticator_selection.map(Into::into), + challenge: URL_SAFE.encode(&value.challenge), + exclude_credentials: value + .exclude_credentials + .map(|ecs| ecs.into_iter().map(Into::into).collect::>()), + extensions: value.extensions.map(Into::into), + hints: value + .hints + .map(|hints| hints.into_iter().map(Into::into).collect::>()), + pub_key_cred_params: value + .pub_key_cred_params + .into_iter() + .map(Into::into) + .collect::>(), + rp: value.rp.into(), + timeout: value.timeout, + user: value.user.into(), + }) + } +} + +impl TryFrom + for webauthn_rs_proto::attest::PublicKeyCredentialCreationOptions +{ + type Error = WebauthnError; + + fn try_from(val: PublicKeyCredentialCreationOptions) -> Result { + Ok(Self { + attestation: val.attestation.map(Into::into), + attestation_formats: val + .attestation_formats + .map(|ats| ats.into_iter().map(Into::into).collect::>()), + authenticator_selection: val.authenticator_selection.map(Into::into), + challenge: URL_SAFE.decode(val.challenge)?.into(), + exclude_credentials: val + .exclude_credentials + .map(|ecs| { + ecs.into_iter() + .map(TryInto::try_into) + .collect::, _>>() + }) + .transpose()?, + extensions: val.extensions.map(Into::into), + hints: val + .hints + .map(|hints| hints.into_iter().map(Into::into).collect::>()), + pub_key_cred_params: val + .pub_key_cred_params + .into_iter() + .map(Into::into) + .collect::>(), + rp: val.rp.into(), + timeout: val.timeout, + user: val.user.try_into()?, + }) + } +} diff --git a/src/webauthn/api/types/public_key_credential_descriptor.rs b/src/webauthn/api/types/public_key_credential_descriptor.rs new file mode 100644 index 00000000..bd6226df --- /dev/null +++ b/src/webauthn/api/types/public_key_credential_descriptor.rs @@ -0,0 +1,64 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::authenticator_transport::AuthenticatorTransport; +use crate::webauthn::WebauthnError; + +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct PublicKeyCredentialDescriptor { + /// The type of credential. + pub type_: String, + /// The credential id. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + 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>, +} + +impl TryFrom + for webauthn_rs_proto::options::PublicKeyCredentialDescriptor +{ + type Error = WebauthnError; + fn try_from(value: PublicKeyCredentialDescriptor) -> Result { + Ok(Self { + id: URL_SAFE.decode(value.id)?.into(), + type_: value.type_, + transports: value + .transports + .map(|trs| trs.into_iter().map(Into::into).collect::>()), + }) + } +} + +impl From + for PublicKeyCredentialDescriptor +{ + fn from(value: webauthn_rs_proto::options::PublicKeyCredentialDescriptor) -> Self { + Self { + type_: value.type_, + id: URL_SAFE.encode(&value.id), + transports: value + .transports + .map(|transports| transports.into_iter().map(Into::into).collect::>()), + } + } +} diff --git a/src/webauthn/api/types/public_key_credential_hints.rs b/src/webauthn/api/types/public_key_credential_hints.rs new file mode 100644 index 00000000..0d903cfb --- /dev/null +++ b/src/webauthn/api/types/public_key_credential_hints.rs @@ -0,0 +1,62 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// A hint as to the class of device that is expected to fulfill this operation. +/// +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum PublicKeyCredentialHints { + /// The credential is a platform authenticator. + ClientDevice, + + /// The credential will come from an external device. + Hybrid, + + /// The credential is a removable security key. + SecurityKey, +} + +impl From for PublicKeyCredentialHints { + fn from(value: webauthn_rs_proto::options::PublicKeyCredentialHints) -> Self { + match value { + webauthn_rs_proto::options::PublicKeyCredentialHints::ClientDevice => { + PublicKeyCredentialHints::ClientDevice + } + webauthn_rs_proto::options::PublicKeyCredentialHints::Hybrid => { + PublicKeyCredentialHints::Hybrid + } + webauthn_rs_proto::options::PublicKeyCredentialHints::SecurityKey => { + PublicKeyCredentialHints::SecurityKey + } + } + } +} + +impl From for webauthn_rs_proto::options::PublicKeyCredentialHints { + fn from(value: PublicKeyCredentialHints) -> Self { + match value { + PublicKeyCredentialHints::ClientDevice => { + webauthn_rs_proto::options::PublicKeyCredentialHints::ClientDevice + } + PublicKeyCredentialHints::Hybrid => { + webauthn_rs_proto::options::PublicKeyCredentialHints::Hybrid + } + PublicKeyCredentialHints::SecurityKey => { + webauthn_rs_proto::options::PublicKeyCredentialHints::SecurityKey + } + } + } +} diff --git a/src/webauthn/api/types/public_key_credential_request_options.rs b/src/webauthn/api/types/public_key_credential_request_options.rs new file mode 100644 index 00000000..3d84fdd6 --- /dev/null +++ b/src/webauthn/api/types/public_key_credential_request_options.rs @@ -0,0 +1,96 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::allow_credentials::AllowCredentials; +use super::public_key_credential_hints::PublicKeyCredentialHints; +use super::request_authentication_extensions::RequestAuthenticationExtensions; +use super::user_verification_policy::UserVerificationPolicy; +use crate::webauthn::WebauthnError; + +/// The requested options for the authentication. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct PublicKeyCredentialRequestOptions { + /// The set of credentials that are allowed to sign this challenge. + #[validate(nested)] + pub allow_credentials: Vec, + /// The challenge that should be signed by the authenticator. + #[schema(value_type = String, format = Binary, content_encoding = "base64")] + pub challenge: String, + /// extensions. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] + pub extensions: Option, + /// Hints defining which types credentials may be used in this operation. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub hints: Option>, + /// The relying party ID. + #[validate(length(max = 64))] + pub rp_id: String, + /// The timeout for the authenticator in case of no interaction. + pub timeout: Option, + /// The verification policy the browser will request. + pub user_verification: UserVerificationPolicy, +} + +impl From + for PublicKeyCredentialRequestOptions +{ + fn from(val: webauthn_rs_proto::auth::PublicKeyCredentialRequestOptions) -> Self { + Self { + allow_credentials: val + .allow_credentials + .into_iter() + .map(Into::into) + .collect::>(), + challenge: URL_SAFE.encode(val.challenge), + extensions: val.extensions.map(Into::into), + hints: val + .hints + .map(|hints| hints.into_iter().map(Into::into).collect::>()), + rp_id: val.rp_id, + timeout: val.timeout, + user_verification: val.user_verification.into(), + } + } +} + +impl TryFrom + for webauthn_rs_proto::auth::PublicKeyCredentialRequestOptions +{ + type Error = WebauthnError; + + fn try_from(value: PublicKeyCredentialRequestOptions) -> Result { + Ok(Self { + allow_credentials: value + .allow_credentials + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + challenge: URL_SAFE.decode(value.challenge)?.into(), + extensions: value.extensions.map(TryInto::try_into).transpose()?, + hints: value + .hints + .map(|hints| hints.into_iter().map(Into::into).collect::>()), + rp_id: value.rp_id, + timeout: value.timeout, + user_verification: value.user_verification.into(), + }) + } +} diff --git a/src/webauthn/api/types/register.rs b/src/webauthn/api/types/register.rs index d349d9b9..a73e819a 100644 --- a/src/webauthn/api/types/register.rs +++ b/src/webauthn/api/types/register.rs @@ -20,7 +20,11 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use validator::Validate; +use super::authenticator_transport::AuthenticatorTransport; +use super::credential_protection_policy::CredentialProtectionPolicy; +use super::public_key_credential_creation_options::PublicKeyCredentialCreationOptions; use crate::api::KeystoneApiError; +use crate::webauthn::WebauthnError; /// Passkey registration request. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] @@ -30,8 +34,10 @@ pub struct UserPasskeyRegistrationStartRequest { pub passkey: PasskeyCreate, } +// TODO: +// - remove description from register_start request /// Passkey information. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PasskeyCreate { /// Passkey description #[schema(nullable = false, max_length = 64)] @@ -48,6 +54,14 @@ pub struct PasskeyResponse { pub passkey: Passkey, } +impl From for PasskeyResponse { + fn from(value: crate::webauthn::types::WebauthnCredential) -> Self { + Self { + passkey: value.into(), + } + } +} + /// Passkey information. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Passkey { @@ -59,6 +73,15 @@ pub struct Passkey { pub description: Option, } +impl From for Passkey { + fn from(value: crate::webauthn::types::WebauthnCredential) -> Self { + Self { + credential_id: value.credential_id, + description: value.description, + } + } +} + /// Passkey challenge. /// /// This is the WebauthN challenge that need to be signed by the @@ -70,376 +93,17 @@ pub struct UserPasskeyRegistrationStartResponse { pub public_key: PublicKeyCredentialCreationOptions, } -/// The requested options for the authentication. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct PublicKeyCredentialCreationOptions { - /// 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")] - #[validate(nested)] - 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")] - #[validate(nested)] - pub exclude_credentials: Option>, - /// extensions. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - #[validate(nested)] - 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. - #[validate(nested)] - pub pub_key_cred_params: Vec, - /// The relying party - #[validate(nested)] - pub rp: RelyingParty, - /// The timeout for the authenticator in case of no interaction. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - #[validate(range(min = 1))] - pub timeout: Option, - /// The user. - #[validate(nested)] - pub user: User, -} - -/// Relying Party Entity. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct RelyingParty { - /// The id of the relying party. - #[validate(length(max = 64))] - pub id: String, - /// The name of the relying party. - #[validate(length(max = 255))] - pub name: String, -} - -/// User Entity. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -#[schema(as = PasskeyUser)] -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: 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. - #[validate(length(max = 255))] - pub name: String, - /// The user's preferred name for display. This value can change, so must - /// not be used as a primary key. - #[validate(length(max = 255))] - pub display_name: String, -} - -/// Public key cryptographic parameters -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct PubKeyCredParams { - /// The algorithm in use defined by CASE. - pub alg: i64, - /// The type of public-key credential. - pub type_: String, -} - -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct PublicKeyCredentialDescriptor { - /// The type of credential. - pub type_: String, - /// The credential id. - #[schema(value_type = String, format = Binary, content_encoding = "base64")] - 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 -// /// -// Conditional, -// } -// -// /// A descriptor of a credential that can be used. -// #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, -// Validate)] pub struct AllowCredentials { -// /// The type of credential. -// pub type_: String, -// /// The id of the credential. -// #[schema(value_type = String, format = Binary, content_encoding = -// "base64")] pub id: String, -// /// may be usb, nfc, ble, internal -// #[schema(nullable = false)] -// #[serde(skip_serializing_if = "Option::is_none")] -// pub transports: Option>, -// } - -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum AuthenticatorTransport { - /// - Ble, - /// Hybrid transport, formerly caBLE. Part of the level 3 draft - /// specification. - Hybrid, - /// - Internal, - /// - Nfc, - /// Test transport; used for Windows 10. - Test, - /// An unknown transport was provided - it will be ignored. - Unknown, - /// - Usb, -} - -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -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. . - #[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. . - #[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. . - pub require_resident_key: bool, - /// The user verification level to request during registration. Depending on - /// if this authenticator provides verification may affect future - /// interactions as this is associated to the credential during - /// registration. - 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. -/// -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum AuthenticatorAttachment { - /// Request a device that is part of the machine aka inseparable. - /// . - Platform, - /// Request a device that can be separated from the machine aka an external - /// token. . - CrossPlatform, -} - -/// The Relying Party's requirements for client-side discoverable credentials. -/// -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum ResidentKeyRequirement { - /// . - Discouraged, - /// ⚠️ In all major browsers preferred is identical in behaviour to - /// required. You should use required instead. . - Preferred, - /// . - Required, -} - -/// A hint as to the class of device that is expected to fufil this operation. -/// -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum PublicKeyCredentialHints { - /// The credential is a platform authenticator. - ClientDevice, - /// The credential will come from an external device. - Hybrid, - /// The credential is a removable security key. - SecurityKey, -} - -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum AttestationConveyancePreference { - /// Do not request attestation. - /// . - None, - /// Request attestation in a semi-anonymized form. - /// . - Indirect, - /// Request attestation in a direct form. - /// . - Direct, -} - -/// The type of attestation on the credential. -/// -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum AttestationFormat { - /// Packed attestation. - Packed, - /// TPM attestation (like Microsoft). - Tpm, - /// Android hardware attestation. - AndroidKey, - /// Older Android Safety Net. - AndroidSafetyNet, - /// Old U2F attestation type. - FIDOU2F, - /// Apple touchID/faceID. - AppleAnonymous, - /// No attestation. - None, -} - -/// Extension option inputs for PublicKeyCredentialCreationOptions. -/// -/// Implements `AuthenticatorExtensionsClientInputs` from the spec. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -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")] - #[validate(nested)] - pub cred_protect: 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 -/// -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -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. - #[schema(nullable = false)] - #[serde(skip_serializing_if = "Option::is_none")] - pub enforce_credential_protection_policy: Option, -} - -/// Valid credential protection policies -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -#[repr(u8)] -pub enum CredentialProtectionPolicy { - /// This reflects “FIDO_2_0” semantics. In this configuration, performing - /// some form of user verification is optional with or without - /// credentialID list. This is the default state of the credential if - /// the extension is not specified. - Optional = 1, - /// In this configuration, credential is discovered only when its - /// credentialID is provided by the platform or when some form of user - /// verification is performed. - OptionalWithCredentialIDList = 2, - /// This reflects that discovery and usage of the credential MUST be - /// preceded by some form of user verification. - Required = 3, -} - -/// Defines the User Authenticator Verification policy. This is documented -/// , 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 a fingerprint to be verified, or a yubico -/// device requiring a pin in addition to a touch event. -/// -/// An example of a non-verified interaction is a yubico device with no pin -/// where touch is the only interaction - we only verify a user is present, but -/// we don't have extra details to the legitimacy of that user. -/// -/// As UserVerificationPolicy is only used in credential registration, this -/// stores the verification state of the credential in the persisted credential. -/// These persisted credentials define which UserVerificationPolicy is issued -/// during authentications. -/// -/// IMPORTANT - Due to limitations of the webauthn specification, CTAP devices, -/// and browser implementations, the only secure choice as an RP is required. -/// -/// ⚠️ WARNING - discouraged is marked with a warning, as some authenticators -/// will FORCE verification during registration but NOT during authentication. -/// This makes it impossible for a relying party to consistently enforce user -/// verification, which can confuse users and lead them to distrust user -/// verification is being enforced. -/// -/// ⚠️ WARNING - preferred can lead to authentication errors in some cases due -/// to browser peripheral exchange allowing authentication verification -/// bypass. Webauthn RS is not vulnerable to these bypasses due to our -/// tracking of UV during registration through authentication, however -/// preferred can cause legitimate credentials to not prompt for UV correctly -/// due to browser perhipheral exchange leading Webauthn RS to deny them in what -/// should otherwise be legitimate operations. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum UserVerificationPolicy { - /// Require user verification bit to be set, and fail the registration or - /// authentication if false. If the authenticator is not able to perform - /// verification, it will not be usable with this policy. - /// - /// This policy is the default as it is the only secure and consistent user - /// verification option. - Required, - /// Prefer UV if possible, but ignore if not present. In other webauthn - /// deployments this is bypassable as it implies the library will not - /// check UV is set correctly for this credential. Webauthn-RS is not - /// vulnerable to this as we check the UV state always based on - /// it's presence at registration. - /// - /// 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, +impl TryFrom + for UserPasskeyRegistrationStartResponse +{ + type Error = WebauthnError; + fn try_from( + value: webauthn_rs::prelude::CreationChallengeResponse, + ) -> Result { + Ok(Self { + public_key: value.public_key.try_into()?, + }) + } } /// A client response to a registration challenge. This contains all required @@ -475,6 +139,36 @@ pub struct UserPasskeyRegistrationFinishRequest { pub extensions: RegistrationExtensionsClientOutputs, } +impl TryFrom + for webauthn_rs::prelude::RegisterPublicKeyCredential +{ + type Error = KeystoneApiError; + fn try_from(value: UserPasskeyRegistrationFinishRequest) -> Result { + Ok(Self { + id: value.id, + raw_id: URL_SAFE.decode(value.raw_id)?.into(), + type_: value.type_, + response: value.response.try_into()?, + extensions: value.extensions.into(), + }) + } +} + +impl From + for UserPasskeyRegistrationFinishRequest +{ + fn from(value: webauthn_rs::prelude::RegisterPublicKeyCredential) -> Self { + Self { + description: None, + id: value.id, + raw_id: URL_SAFE.encode(value.raw_id), + type_: value.type_, + response: value.response.into(), + extensions: value.extensions.into(), + } + } +} + /// #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthenticatorAttestationResponseRaw { @@ -490,6 +184,36 @@ pub struct AuthenticatorAttestationResponseRaw { pub transports: Option>, } +impl From + for AuthenticatorAttestationResponseRaw +{ + fn from(value: webauthn_rs_proto::attest::AuthenticatorAttestationResponseRaw) -> Self { + Self { + attestation_object: URL_SAFE.encode(value.attestation_object), + client_data_json: URL_SAFE.encode(value.client_data_json), + transports: value + .transports + .map(|i| i.into_iter().map(Into::into).collect::>()), + } + } +} + +impl TryFrom + for webauthn_rs_proto::attest::AuthenticatorAttestationResponseRaw +{ + type Error = WebauthnError; + + fn try_from(value: AuthenticatorAttestationResponseRaw) -> Result { + Ok(Self { + attestation_object: URL_SAFE.decode(value.attestation_object)?.into(), + client_data_json: URL_SAFE.decode(value.client_data_json)?.into(), + transports: value + .transports + .map(|i| i.into_iter().map(Into::into).collect::>()), + }) + } +} + /// The default /// option here for Options are None, so it can be derived #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] @@ -520,211 +244,54 @@ pub struct RegistrationExtensionsClientOutputs { pub min_pin_length: Option, } -/// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] -pub struct CredProps { - /// A user agent supplied hint that this credential may have created a - /// resident key. It is 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. - pub rk: bool, -} - -impl From for PasskeyResponse { - fn from(value: crate::webauthn::types::WebauthnCredential) -> Self { +impl From + for webauthn_rs_proto::extensions::RegistrationExtensionsClientOutputs +{ + fn from(value: RegistrationExtensionsClientOutputs) -> Self { Self { - passkey: value.into(), + appid: value.appid, + cred_props: value.cred_props.map(Into::into), + hmac_secret: value.hmac_secret, + cred_protect: value.cred_protect.map(Into::into), + min_pin_length: value.min_pin_length, } } } -impl From for Passkey { - fn from(value: crate::webauthn::types::WebauthnCredential) -> Self { +impl From + for RegistrationExtensionsClientOutputs +{ + fn from(value: webauthn_rs_proto::extensions::RegistrationExtensionsClientOutputs) -> Self { Self { - credential_id: value.credential_id, - description: value.description, + appid: value.appid, + cred_props: value.cred_props.map(Into::into), + hmac_secret: value.hmac_secret, + cred_protect: value.cred_protect.map(Into::into), + min_pin_length: value.min_pin_length, } } } -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::Ble => webauthn_rs_proto::options::AuthenticatorTransport::Ble, - AuthenticatorTransport::Hybrid => webauthn_rs_proto::options::AuthenticatorTransport::Hybrid, - AuthenticatorTransport::Internal => webauthn_rs_proto::options::AuthenticatorTransport::Internal, - AuthenticatorTransport::Nfc => webauthn_rs_proto::options::AuthenticatorTransport::Nfc, - AuthenticatorTransport::Test => webauthn_rs_proto::options::AuthenticatorTransport::Test, - AuthenticatorTransport::Unknown => webauthn_rs_proto::options::AuthenticatorTransport::Unknown, - AuthenticatorTransport::Usb => webauthn_rs_proto::options::AuthenticatorTransport::Usb, +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct CredProps { + /// A user agent supplied hint that this credential may have created a + /// resident key. It is 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. + pub rk: Option, +} - }) - .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::Optional => webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional, - CredentialProtectionPolicy::OptionalWithCredentialIDList => webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList, - CredentialProtectionPolicy::Required => webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired - } - }), - min_pin_length: val.extensions.min_pin_length, - }, - }) +impl From for webauthn_rs_proto::extensions::CredProps { + fn from(value: CredProps) -> Self { + Self { rk: value.rk } } } -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::Optional, - webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => CredentialProtectionPolicy::OptionalWithCredentialIDList, - webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired => CredentialProtectionPolicy::Required, - - }, - 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, - } - }, - }) +impl From for CredProps { + fn from(value: webauthn_rs_proto::extensions::CredProps) -> Self { + Self { rk: value.rk } } } diff --git a/src/webauthn/api/types/relying_party.rs b/src/webauthn/api/types/relying_party.rs new file mode 100644 index 00000000..473c00a9 --- /dev/null +++ b/src/webauthn/api/types/relying_party.rs @@ -0,0 +1,45 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +/// Relying Party Entity. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct RelyingParty { + /// The id of the relying party. + #[validate(length(max = 64))] + pub id: String, + /// The name of the relying party. + #[validate(length(max = 255))] + pub name: String, +} + +impl From for webauthn_rs_proto::options::RelyingParty { + fn from(value: RelyingParty) -> Self { + Self { + id: value.id, + name: value.name, + } + } +} + +impl From for RelyingParty { + fn from(value: webauthn_rs_proto::options::RelyingParty) -> Self { + Self { + id: value.id, + name: value.name, + } + } +} diff --git a/src/webauthn/api/types/request_authentication_extensions.rs b/src/webauthn/api/types/request_authentication_extensions.rs new file mode 100644 index 00000000..6df503cf --- /dev/null +++ b/src/webauthn/api/types/request_authentication_extensions.rs @@ -0,0 +1,66 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::hmac_get_secret_input::HmacGetSecretInput; +use crate::webauthn::WebauthnError; + +/// Extension option inputs for PublicKeyCredentialRequestOptions +/// +/// Implements AuthenticatorExtensionsClientInputs from the spec +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct RequestAuthenticationExtensions { + /// The appid extension options. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub appid: Option, + /// ⚠️ - Browsers do not support this! + /// Hmac get secret. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] + pub hmac_get_secret: Option, + /// ⚠️ - Browsers do not support this! Uvm. + #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] + pub uvm: Option, +} + +impl From + for RequestAuthenticationExtensions +{ + fn from(value: webauthn_rs_proto::extensions::RequestAuthenticationExtensions) -> Self { + Self { + appid: value.appid, + hmac_get_secret: value.hmac_get_secret.map(Into::into), + uvm: value.uvm, + } + } +} + +impl TryFrom + for webauthn_rs_proto::extensions::RequestAuthenticationExtensions +{ + type Error = WebauthnError; + + fn try_from(value: RequestAuthenticationExtensions) -> Result { + Ok(Self { + appid: value.appid, + hmac_get_secret: value.hmac_get_secret.map(TryInto::try_into).transpose()?, + uvm: value.uvm, + }) + } +} diff --git a/src/webauthn/api/types/request_registration_extension.rs b/src/webauthn/api/types/request_registration_extension.rs new file mode 100644 index 00000000..2675514c --- /dev/null +++ b/src/webauthn/api/types/request_registration_extension.rs @@ -0,0 +1,77 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use super::cred_protect::CredProtect; + +/// Extension option inputs for PublicKeyCredentialCreationOptions. +/// +/// Implements `AuthenticatorExtensionsClientInputs` from the spec. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +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")] + #[validate(nested)] + pub cred_protect: 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, +} + +impl From + for webauthn_rs_proto::extensions::RequestRegistrationExtensions +{ + fn from(value: RequestRegistrationExtensions) -> Self { + Self { + cred_props: value.cred_props, + cred_protect: value.cred_protect.map(Into::into), + hmac_create_secret: value.hmac_create_secret, + min_pin_length: value.min_pin_length, + uvm: value.uvm, + } + } +} + +impl From + for RequestRegistrationExtensions +{ + fn from(value: webauthn_rs_proto::extensions::RequestRegistrationExtensions) -> Self { + Self { + cred_props: value.cred_props, + cred_protect: value.cred_protect.map(Into::into), + hmac_create_secret: value.hmac_create_secret, + min_pin_length: value.min_pin_length, + uvm: value.uvm, + } + } +} diff --git a/src/webauthn/api/types/resident_key_requirement.rs b/src/webauthn/api/types/resident_key_requirement.rs new file mode 100644 index 00000000..92c65561 --- /dev/null +++ b/src/webauthn/api/types/resident_key_requirement.rs @@ -0,0 +1,61 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// The Relying Party's requirements for client-side discoverable credentials. +/// +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum ResidentKeyRequirement { + /// . + Discouraged, + /// ⚠️ In all major browsers preferred is identical in behaviour to + /// required. You should use required instead. . + Preferred, + /// . + Required, +} + +impl From for ResidentKeyRequirement { + fn from(value: webauthn_rs_proto::options::ResidentKeyRequirement) -> Self { + match value { + webauthn_rs_proto::options::ResidentKeyRequirement::Discouraged => { + ResidentKeyRequirement::Discouraged + } + webauthn_rs_proto::options::ResidentKeyRequirement::Preferred => { + ResidentKeyRequirement::Preferred + } + webauthn_rs_proto::options::ResidentKeyRequirement::Required => { + ResidentKeyRequirement::Required + } + } + } +} + +impl From for webauthn_rs_proto::options::ResidentKeyRequirement { + fn from(value: ResidentKeyRequirement) -> Self { + match value { + ResidentKeyRequirement::Discouraged => { + webauthn_rs_proto::options::ResidentKeyRequirement::Discouraged + } + ResidentKeyRequirement::Preferred => { + webauthn_rs_proto::options::ResidentKeyRequirement::Preferred + } + ResidentKeyRequirement::Required => { + webauthn_rs_proto::options::ResidentKeyRequirement::Required + } + } + } +} diff --git a/src/webauthn/api/types/user.rs b/src/webauthn/api/types/user.rs new file mode 100644 index 00000000..07cbe24b --- /dev/null +++ b/src/webauthn/api/types/user.rs @@ -0,0 +1,59 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +use crate::webauthn::WebauthnError; + +/// User Entity. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +#[schema(as = PasskeyUser)] +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: 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. + #[validate(length(max = 255))] + pub name: String, + /// The user's preferred name for display. This value can change, so must + /// not be used as a primary key. + #[validate(length(max = 255))] + pub display_name: String, +} + +impl TryFrom for webauthn_rs_proto::options::User { + type Error = WebauthnError; + fn try_from(value: User) -> Result { + Ok(webauthn_rs_proto::options::User { + id: URL_SAFE.decode(value.id)?.into(), + name: value.name, + display_name: value.display_name, + }) + } +} + +impl From for User { + fn from(value: webauthn_rs_proto::options::User) -> Self { + Self { + id: URL_SAFE.encode(&value.id), + name: value.name, + display_name: value.display_name, + } + } +} diff --git a/src/webauthn/api/types/user_verification_policy.rs b/src/webauthn/api/types/user_verification_policy.rs new file mode 100644 index 00000000..4b33f089 --- /dev/null +++ b/src/webauthn/api/types/user_verification_policy.rs @@ -0,0 +1,106 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// Defines the User Authenticator Verification policy. This is documented +/// , 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 a fingerprint to be verified, or a yubico +/// device requiring a pin in addition to a touch event. +/// +/// An example of a non-verified interaction is a yubico device with no pin +/// where touch is the only interaction - we only verify a user is present, but +/// we don't have extra details to the legitimacy of that user. +/// +/// As UserVerificationPolicy is only used in credential registration, this +/// stores the verification state of the credential in the persisted credential. +/// These persisted credentials define which UserVerificationPolicy is issued +/// during authentications. +/// +/// IMPORTANT - Due to limitations of the webauthn specification, CTAP devices, +/// and browser implementations, the only secure choice as an RP is required. +/// +/// ⚠️ WARNING - discouraged is marked with a warning, as some authenticators +/// will FORCE verification during registration but NOT during authentication. +/// This makes it impossible for a relying party to consistently enforce user +/// verification, which can confuse users and lead them to distrust user +/// verification is being enforced. +/// +/// ⚠️ WARNING - preferred can lead to authentication errors in some cases due +/// to browser peripheral exchange allowing authentication verification +/// bypass. Webauthn RS is not vulnerable to these bypasses due to our +/// tracking of UV during registration through authentication, however +/// preferred can cause legitimate credentials to not prompt for UV correctly +/// due to browser perhipheral exchange leading Webauthn RS to deny them in what +/// should otherwise be legitimate operations. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum UserVerificationPolicy { + /// Require user verification bit to be set, and fail the registration or + /// authentication if false. If the authenticator is not able to perform + /// verification, it will not be usable with this policy. + /// + /// This policy is the default as it is the only secure and consistent user + /// verification option. + Required, + /// Prefer UV if possible, but ignore if not present. In other webauthn + /// deployments this is bypassable as it implies the library will not + /// check UV is set correctly for this credential. Webauthn-RS is not + /// vulnerable to this as we check the UV state always based on + /// it's presence at registration. + /// + /// 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, +} + +impl From for webauthn_rs_proto::options::UserVerificationPolicy { + fn from(val: UserVerificationPolicy) -> Self { + match val { + UserVerificationPolicy::DiscouragedDoNotUse => { + webauthn_rs_proto::options::UserVerificationPolicy::Discouraged_DO_NOT_USE + } + UserVerificationPolicy::Preferred => { + webauthn_rs_proto::options::UserVerificationPolicy::Preferred + } + UserVerificationPolicy::Required => { + webauthn_rs_proto::options::UserVerificationPolicy::Required + } + } + } +} + +impl From for UserVerificationPolicy { + fn from(val: webauthn_rs_proto::options::UserVerificationPolicy) -> Self { + match val { + webauthn_rs_proto::options::UserVerificationPolicy::Discouraged_DO_NOT_USE => { + UserVerificationPolicy::DiscouragedDoNotUse + } + webauthn_rs_proto::options::UserVerificationPolicy::Preferred => { + UserVerificationPolicy::Preferred + } + webauthn_rs_proto::options::UserVerificationPolicy::Required => { + UserVerificationPolicy::Required + } + } + } +} diff --git a/src/webauthn/error.rs b/src/webauthn/error.rs index 93a6eadb..eb5fbad5 100644 --- a/src/webauthn/error.rs +++ b/src/webauthn/error.rs @@ -27,6 +27,10 @@ pub enum WebauthnError { source: crate::auth::AuthenticationError, }, + /// Base64 decode error + #[error("base64 decoding error")] + Base64Decode(#[from] base64::DecodeError), + /// Conflict. #[error("conflict: {0}")] Conflict(String), diff --git a/tests/api/main.rs b/tests/api/main.rs index 7abd6747..043d3eca 100644 --- a/tests/api/main.rs +++ b/tests/api/main.rs @@ -18,3 +18,4 @@ mod common; mod identity; mod resource; mod role; +mod webauthn; diff --git a/tests/api/webauthn.rs b/tests/api/webauthn.rs new file mode 100644 index 00000000..4dcb0853 --- /dev/null +++ b/tests/api/webauthn.rs @@ -0,0 +1,186 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use eyre::{Result, eyre}; +use reqwest::{ + ClientBuilder, StatusCode, + header::{HeaderMap, HeaderName, HeaderValue}, +}; +use secrecy::SecretString; +use tracing::info; +use url::Url; + +use webauthn_authenticator_rs::{AuthenticatorBackend, WebauthnAuthenticator}; + +use openstack_keystone::webauthn::api::types::auth::*; +use openstack_keystone::webauthn::api::types::register::*; + +use crate::common::*; + +mod register; +mod roundtrip; + +pub async fn start_registration>( + tc: &TestClient, + user_id: U, + req: PasskeyCreate, +) -> Result { + Ok(tc + .client + .post( + tc.base_url + .join(format!("v4/users/{}/passkeys/register_start", user_id.as_ref()).as_str())?, + ) + .json(&UserPasskeyRegistrationStartRequest { passkey: req }) + .send() + .await? + .json::() + .await?) +} + +pub async fn finish_registration>( + tc: &TestClient, + user_id: U, + req: UserPasskeyRegistrationFinishRequest, +) -> Result { + Ok(tc + .client + .post( + tc.base_url + .join(format!("v4/users/{}/passkeys/register_finish", user_id.as_ref()).as_str())?, + ) + .json(&req) + .send() + .await? + .json::() + .await?) +} + +pub async fn start_auth>( + tc: &TestClient, + user_id: U, +) -> Result { + Ok(tc + .client + .post(tc.base_url.join("v4/auth/passkey/start")?) + .json(&PasskeyAuthenticationStartRequest { + passkey: PasskeyUserAuthenticationRequest { + user_id: user_id.as_ref().into(), + }, + }) + .send() + .await? + .json::() + .await?) +} + +pub async fn finish_auth( + tc: &TestClient, + data: PasskeyAuthenticationFinishRequest, +) -> Result { + Ok(tc + .client + .post(tc.base_url.join("v4/auth/passkey/finish")?) + .json(&data) + .send() + .await?) +} + +pub async fn register_user_passkey, D: Into>( + tc: &TestClient, + user_id: U, + origin: Url, + authenticator: &mut WebauthnAuthenticator, + description: Option, +) -> Result<()> +where + B: AuthenticatorBackend, +{ + let reg_challenge = start_registration(tc, user_id.as_ref(), PasskeyCreate::default()).await?; + info!("registration challenge data: {:?}", reg_challenge); + + let reg_result = authenticator.do_registration( + origin.clone(), + webauthn_authenticator_rs::prelude::CreationChallengeResponse { + public_key: reg_challenge.public_key.try_into()?, + }, + )?; + info!("registration challenge response: {:?}", reg_result); + + let mut finish_req: UserPasskeyRegistrationFinishRequest = reg_result.into(); + if let Some(val) = description { + finish_req.description = Some(val.into()); + } + let reg_finish_response = finish_registration(tc, user_id.as_ref(), finish_req).await?; + info!("registration finish response: {:?}", reg_finish_response); + Ok(()) +} + +impl TestClient { + pub async fn auth_passkey>( + self, + user_id: U, + origin: Url, + authenticator: &mut WebauthnAuthenticator, + ) -> Result { + let mut new = self; + let auth_challenge = start_auth(&new, user_id.as_ref()).await?; + info!("start auth challenge: {:?}", auth_challenge); + + let auth_challenge_response = authenticator.do_authentication( + origin, + webauthn_authenticator_rs::prelude::RequestChallengeResponse { + public_key: auth_challenge.public_key.try_into()?, + mediation: auth_challenge.mediation.map(Into::into), + }, + )?; + + info!("auth challenge response: is {:?}", auth_challenge_response); + let rsp = finish_auth( + &new, + PasskeyAuthenticationFinishRequest { + id: auth_challenge_response.id, + extensions: auth_challenge_response.extensions.into(), + raw_id: URL_SAFE.encode(auth_challenge_response.raw_id), + response: auth_challenge_response.response.into(), + type_: auth_challenge_response.type_, + user_id: user_id.as_ref().to_string(), + }, + ) + .await?; + + if rsp.status() != StatusCode::OK { + return Err(eyre!("Authentication failed with {}", rsp.status())); + } + + let token = rsp + .headers() + .get("X-Subject-Token") + .ok_or_else(|| eyre!("Token is missing in the {:?}", rsp))? + .to_str()? + .to_string(); + + new.token = Some(SecretString::from(token.clone())); + new.auth = Some(rsp.json().await?); + let mut token = HeaderValue::from_str(&token)?; + token.set_sensitive(true); + new.client = ClientBuilder::new() + .default_headers(HeaderMap::from_iter([( + HeaderName::from_static("x-auth-token"), + token, + )])) + .build()?; + Ok(new) + } +} diff --git a/tests/api/webauthn/register.rs b/tests/api/webauthn/register.rs new file mode 100644 index 00000000..3be8a4cb --- /dev/null +++ b/tests/api/webauthn/register.rs @@ -0,0 +1,84 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use eyre::Result; +use tracing_test::traced_test; +use url::Url; +use uuid::Uuid; + +use webauthn_authenticator_rs::WebauthnAuthenticator; +use webauthn_authenticator_rs::softtoken::SoftToken; + +use openstack_keystone::api::v3::user::types::UserCreate; + +use super::*; +use crate::identity::user::*; + +#[tokio::test] +#[traced_test] +async fn test_register_empty_description() -> Result<()> { + let mut test_client = TestClient::default()?; + test_client.auth_admin().await?; + + let user_create = UserCreate { + name: Uuid::new_v4().to_string(), + domain_id: "default".into(), + ..Default::default() + }; + let user = create_user(&test_client, user_create).await?; + + let authenticator_backend = SoftToken::new(true)?.0; + let mut authenticator = WebauthnAuthenticator::new(authenticator_backend); + let origin = Url::parse("http://localhost:8080")?; + + register_user_passkey( + &test_client, + &user.id, + origin.clone(), + &mut authenticator, + None::, + ) + .await?; + + Ok(()) +} + +#[tokio::test] +#[traced_test] +async fn test_register_description() -> Result<()> { + let mut test_client = TestClient::default()?; + test_client.auth_admin().await?; + + let user_create = UserCreate { + name: Uuid::new_v4().to_string(), + domain_id: "default".into(), + ..Default::default() + }; + let user = create_user(&test_client, user_create).await?; + + let authenticator_backend = SoftToken::new(true)?.0; + let mut authenticator = WebauthnAuthenticator::new(authenticator_backend); + let origin = Url::parse("http://localhost:8080")?; + + register_user_passkey( + &test_client, + &user.id, + origin.clone(), + &mut authenticator, + Some("softkey"), + ) + .await?; + + Ok(()) +} diff --git a/tests/api/webauthn/roundtrip.rs b/tests/api/webauthn/roundtrip.rs new file mode 100644 index 00000000..36d8a34b --- /dev/null +++ b/tests/api/webauthn/roundtrip.rs @@ -0,0 +1,59 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use eyre::Result; +use tracing_test::traced_test; +use url::Url; +use uuid::Uuid; + +use webauthn_authenticator_rs::WebauthnAuthenticator; +use webauthn_authenticator_rs::softtoken::SoftToken; + +use openstack_keystone::api::v3::user::types::UserCreate; + +use super::*; +use crate::identity::user::*; + +#[tokio::test] +#[traced_test] +async fn test_register_auth() -> Result<()> { + let mut test_client = TestClient::default()?; + test_client.auth_admin().await?; + + let user_create = UserCreate { + name: Uuid::new_v4().to_string(), + domain_id: "default".into(), + ..Default::default() + }; + let user = create_user(&test_client, user_create).await?; + + let authenticator_backend = SoftToken::new(true)?.0; + let mut authenticator = WebauthnAuthenticator::new(authenticator_backend); + let origin = Url::parse("http://localhost:8080")?; + + register_user_passkey( + &test_client, + &user.id, + origin.clone(), + &mut authenticator, + Some("softkey"), + ) + .await?; + + let _new_auth = test_client + .auth_passkey(&user.id, origin.clone(), &mut authenticator) + .await?; + + Ok(()) +} diff --git a/tools/k8s/keystone/overlays/dev/conf/keystone.conf b/tools/k8s/keystone/overlays/dev/conf/keystone.conf index b758098a..8502e2a1 100644 --- a/tools/k8s/keystone/overlays/dev/conf/keystone.conf +++ b/tools/k8s/keystone/overlays/dev/conf/keystone.conf @@ -2,7 +2,7 @@ debug = true [auth] -methods = password,token,openid,application_credential +methods = password,token,openid,application_credential,x509 [api_policy] opa_base_url = http://localhost:8181