use base64::{Engine as _, engine::general_purpose::URL_SAFE};
use clap::Args;
use eyre::{OptionExt, WrapErr, eyre};
use tracing::{error, info, warn};
use webauthn_authenticator_rs::WebauthnAuthenticator;
use webauthn_authenticator_rs::prelude::Url;
use openstack_sdk::AsyncOpenStack;
use crate::Cli;
use crate::OpenStackCliError;
use crate::output::OutputProcessor;
use openstack_sdk::api::QueryAsync;
use openstack_sdk::api::find_by_name;
use openstack_sdk::api::identity::v3::user::find as find_user;
use openstack_sdk::api::identity::v4::user::passkey::{register_finish, register_start};
use openstack_types::identity::v4::user::passkey::response::{
register_finish as passkey, register_start as register,
};
#[derive(Args)]
#[command(about = "Create the identity provider.")]
pub struct PasskeyCommand {
#[command(flatten)]
path: PathParameters,
#[command(flatten)]
passkey: Passkey,
}
#[derive(Args)]
struct PathParameters {
#[command(flatten)]
user: UserInput,
}
#[derive(Args)]
#[group(required = false, multiple = false)]
struct UserInput {
#[arg(long, help_heading = "Path parameters", value_name = "USER_NAME")]
user_name: Option<String>,
#[arg(long, help_heading = "Path parameters", value_name = "USER_ID")]
user_id: Option<String>,
#[arg(long, help_heading = "Path parameters", action = clap::ArgAction::SetTrue)]
current_user: bool,
}
#[derive(Args, Clone)]
struct Passkey {
#[arg(help_heading = "Body parameters", long)]
description: Option<String>,
}
#[inline(always)]
fn convert_api_parameters_to_webauthn(
val: register::RegisterStartResponse,
) -> Result<webauthn_authenticator_rs::prelude::CreationChallengeResponse, OpenStackCliError> {
Ok(
webauthn_authenticator_rs::prelude::CreationChallengeResponse {
public_key: convert_api_response_to_public_key_credential_options(val)?,
},
)
}
#[inline(always)]
fn convert_api_response_to_public_key_credential_options(
val: register::RegisterStartResponse,
) -> Result<webauthn_rs_proto::attest::PublicKeyCredentialCreationOptions, OpenStackCliError> {
Ok(
webauthn_rs_proto::attest::PublicKeyCredentialCreationOptions {
attestation: val.attestation.map(convert_attestation),
attestation_formats: val.attestation_formats.map(|ats| {
ats.into_iter()
.map(convert_attestation_format)
.collect::<Vec<_>>()
}),
authenticator_selection: val
.authenticator_selection
.map(convert_authenticator_selection),
challenge: URL_SAFE.decode(val.challenge)?.into(),
exclude_credentials: val
.exclude_credentials
.map(|ecs| {
ecs.into_iter()
.map(convert_exclude_credential)
.collect::<Result<Vec<_>, _>>()
})
.transpose()?,
extensions: val.extensions.map(convert_extension),
hints: val
.hints
.map(|hints| hints.into_iter().map(convert_hint).collect::<Vec<_>>()),
pub_key_cred_params: val
.pub_key_cred_params
.into_iter()
.map(convert_pub_key_cred_params)
.collect::<Vec<_>>(),
rp: convert_rp(val.rp),
timeout: val.timeout,
user: convert_user(val.user)?,
},
)
}
#[inline(always)]
fn convert_attestation(
val: register::Attestation,
) -> webauthn_rs_proto::options::AttestationConveyancePreference {
match val {
register::Attestation::Direct => {
webauthn_rs_proto::options::AttestationConveyancePreference::Direct
}
register::Attestation::Indirect => {
webauthn_rs_proto::options::AttestationConveyancePreference::Indirect
}
register::Attestation::None => {
webauthn_rs_proto::options::AttestationConveyancePreference::None
}
}
}
#[inline(always)]
fn convert_attestation_format(
val: register::AttestationFormats,
) -> webauthn_rs_proto::options::AttestationFormat {
match val {
register::AttestationFormats::Androidkey => {
webauthn_rs_proto::options::AttestationFormat::AndroidKey
}
register::AttestationFormats::Androidsafetynet => {
webauthn_rs_proto::options::AttestationFormat::AndroidSafetyNet
}
register::AttestationFormats::Appleanonymous => {
webauthn_rs_proto::options::AttestationFormat::AppleAnonymous
}
register::AttestationFormats::Fidou2f => {
webauthn_rs_proto::options::AttestationFormat::FIDOU2F
}
register::AttestationFormats::None => webauthn_rs_proto::options::AttestationFormat::None,
register::AttestationFormats::Packed => {
webauthn_rs_proto::options::AttestationFormat::Packed
}
register::AttestationFormats::Tpm => webauthn_rs_proto::options::AttestationFormat::Tpm,
}
}
#[inline(always)]
fn convert_authenticator_selection(
val: register::AuthenticatorSelection,
) -> webauthn_rs_proto::options::AuthenticatorSelectionCriteria {
webauthn_rs_proto::options::AuthenticatorSelectionCriteria {
authenticator_attachment: val
.authenticator_attachment
.map(convert_authenticator_attachment),
resident_key: val.resident_key.map(convert_resident_key),
require_resident_key: val.require_resident_key,
user_verification: convert_user_verification(val.user_verification),
}
}
#[inline(always)]
fn convert_authenticator_attachment(
val: register::AuthenticatorAttachment,
) -> webauthn_rs_proto::options::AuthenticatorAttachment {
match val {
register::AuthenticatorAttachment::Crossplatform => {
webauthn_rs_proto::options::AuthenticatorAttachment::CrossPlatform
}
register::AuthenticatorAttachment::Platform => {
webauthn_rs_proto::options::AuthenticatorAttachment::Platform
}
}
}
#[inline(always)]
fn convert_resident_key(
val: register::ResidentKey,
) -> webauthn_rs_proto::options::ResidentKeyRequirement {
match val {
register::ResidentKey::Discouraged => {
webauthn_rs_proto::options::ResidentKeyRequirement::Discouraged
}
register::ResidentKey::Preferred => {
webauthn_rs_proto::options::ResidentKeyRequirement::Preferred
}
register::ResidentKey::Required => {
webauthn_rs_proto::options::ResidentKeyRequirement::Required
}
}
}
#[inline(always)]
fn convert_user_verification(
val: register::UserVerification,
) -> webauthn_rs_proto::options::UserVerificationPolicy {
match val {
register::UserVerification::Discourageddonotuse => {
webauthn_rs_proto::options::UserVerificationPolicy::Discouraged_DO_NOT_USE
}
register::UserVerification::Preferred => {
webauthn_rs_proto::options::UserVerificationPolicy::Preferred
}
register::UserVerification::Required => {
webauthn_rs_proto::options::UserVerificationPolicy::Required
}
}
}
#[inline(always)]
fn convert_exclude_credential(
val: register::ExcludeCredentials,
) -> Result<webauthn_rs_proto::options::PublicKeyCredentialDescriptor, OpenStackCliError> {
Ok(webauthn_rs_proto::options::PublicKeyCredentialDescriptor {
id: URL_SAFE.decode(val.id)?.into(),
type_: val.type_,
transports: val
.transports
.map(|trs| trs.into_iter().map(convert_transport).collect::<Vec<_>>()),
})
}
#[inline(always)]
fn convert_transport(
val: register::Transports,
) -> webauthn_rs_proto::options::AuthenticatorTransport {
match val {
register::Transports::Ble => webauthn_rs_proto::options::AuthenticatorTransport::Ble,
register::Transports::Hybrid => webauthn_rs_proto::options::AuthenticatorTransport::Hybrid,
register::Transports::Internal => {
webauthn_rs_proto::options::AuthenticatorTransport::Internal
}
register::Transports::Nfc => webauthn_rs_proto::options::AuthenticatorTransport::Nfc,
register::Transports::Test => webauthn_rs_proto::options::AuthenticatorTransport::Test,
register::Transports::Unknown => {
webauthn_rs_proto::options::AuthenticatorTransport::Unknown
}
register::Transports::Usb => webauthn_rs_proto::options::AuthenticatorTransport::Usb,
}
}
#[inline(always)]
fn convert_transport_webauthn_to_keystone(
val: webauthn_rs_proto::options::AuthenticatorTransport,
) -> register_finish::Transports {
match val {
webauthn_rs_proto::options::AuthenticatorTransport::Ble => register_finish::Transports::Ble,
webauthn_rs_proto::options::AuthenticatorTransport::Hybrid => {
register_finish::Transports::Hybrid
}
webauthn_rs_proto::options::AuthenticatorTransport::Internal => {
register_finish::Transports::Internal
}
webauthn_rs_proto::options::AuthenticatorTransport::Nfc => register_finish::Transports::Nfc,
webauthn_rs_proto::options::AuthenticatorTransport::Test => {
register_finish::Transports::Test
}
webauthn_rs_proto::options::AuthenticatorTransport::Unknown => {
register_finish::Transports::Unknown
}
webauthn_rs_proto::options::AuthenticatorTransport::Usb => register_finish::Transports::Usb,
}
}
#[inline(always)]
fn convert_hint(val: register::Hints) -> webauthn_rs_proto::options::PublicKeyCredentialHints {
match val {
register::Hints::Clientdevice => {
webauthn_rs_proto::options::PublicKeyCredentialHints::ClientDevice
}
register::Hints::Hybrid => webauthn_rs_proto::options::PublicKeyCredentialHints::Hybrid,
register::Hints::Securitykey => {
webauthn_rs_proto::options::PublicKeyCredentialHints::SecurityKey
}
}
}
#[inline(always)]
fn convert_extension(
val: register::Extensions,
) -> webauthn_rs_proto::extensions::RequestRegistrationExtensions {
webauthn_rs_proto::extensions::RequestRegistrationExtensions {
cred_props: val.cred_props,
cred_protect: val.cred_protect.map(convert_cred_protect),
hmac_create_secret: val.hmac_create_secret,
min_pin_length: val.min_pin_length,
uvm: val.uvm,
}
}
#[inline(always)]
fn convert_cred_protect(val: register::CredProtect) -> webauthn_rs_proto::extensions::CredProtect {
webauthn_rs_proto::extensions::CredProtect {
credential_protection_policy: convert_credential_protection_policy(
val.credential_protection_policy,
),
enforce_credential_protection_policy: val.enforce_credential_protection_policy,
}
}
#[inline(always)]
fn convert_credential_protection_policy(
val: register::CredentialProtectionPolicy,
) -> webauthn_rs_proto::extensions::CredentialProtectionPolicy {
match val {
register::CredentialProtectionPolicy::Optional => {
webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional
}
register::CredentialProtectionPolicy::Optionalwithcredentialidlist => {
webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList
}
register::CredentialProtectionPolicy::Required => {
webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired
}
}
}
#[inline(always)]
fn convert_pub_key_cred_params(
val: register::PubKeyCredParams,
) -> webauthn_rs_proto::options::PubKeyCredParams {
webauthn_rs_proto::options::PubKeyCredParams {
alg: val.alg,
type_: val.type_,
}
}
#[inline(always)]
fn convert_rp(val: register::Rp) -> webauthn_rs_proto::options::RelyingParty {
webauthn_rs_proto::options::RelyingParty {
id: val.id,
name: val.name,
}
}
#[inline(always)]
fn convert_user(
val: register::User,
) -> Result<webauthn_rs_proto::options::User, OpenStackCliError> {
Ok(webauthn_rs_proto::options::User {
id: URL_SAFE.decode(val.id)?.into(),
name: val.name,
display_name: val.display_name,
})
}
fn get_finish_registration_endpoint(
user_id: String,
register_cred: webauthn_authenticator_rs::prelude::RegisterPublicKeyCredential,
description: Option<String>,
) -> Result<register_finish::Request<'static>, OpenStackCliError> {
let mut builder = register_finish::Request::builder();
if let Some(description) = description {
builder.description(description);
}
builder.id(register_cred.id);
builder.raw_id(URL_SAFE.encode(register_cred.raw_id));
builder.type_(register_cred.type_);
builder.user_id(user_id);
let mut rsp = register_finish::ResponseBuilder::default();
rsp.attestation_object(URL_SAFE.encode(register_cred.response.attestation_object));
rsp.client_data_json(URL_SAFE.encode(register_cred.response.client_data_json));
if let Some(transports) = register_cred.response.transports {
rsp.transports(
transports
.into_iter()
.map(convert_transport_webauthn_to_keystone)
.collect::<Vec<_>>(),
);
}
builder.response(
rsp.build()
.wrap_err_with(|| eyre!("cannot build passkey `response` structure"))?,
);
let mut extensions = register_finish::ExtensionsBuilder::default();
if let Some(val) = register_cred.extensions.appid {
extensions.appid(val);
}
if let Some(val) = register_cred.extensions.cred_props
&& let Some(flag) = val.rk
{
extensions.cred_props(
register_finish::CredPropsBuilder::default()
.rk(flag)
.build()
.wrap_err_with(|| eyre!("cannot build passkey `cred_props` structure"))?,
);
}
if let Some(val) = register_cred.extensions.cred_protect {
extensions.cred_protect(match val {
webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional => register_finish::CredProtect::Optional,
webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => register_finish::CredProtect::Optionalwithcredentialidlist,
webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired => register_finish::CredProtect::Required,
});
}
if let Some(val) = register_cred.extensions.hmac_secret {
extensions.hmac_secret(val);
}
if let Some(val) = register_cred.extensions.min_pin_length {
extensions.min_pin_length(val);
}
builder.extensions(
extensions
.build()
.wrap_err_with(|| eyre!("cannot build passkey `extensions` structure"))?,
);
builder
.build()
.map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))
}
impl PasskeyCommand {
pub async fn take_action(
&self,
parsed_args: &Cli,
client: &mut AsyncOpenStack,
) -> Result<(), OpenStackCliError> {
info!("Create Passkey");
let op = OutputProcessor::from_args(
parsed_args,
Some("identity.user/passkey"),
Some("register"),
);
op.validate_args(parsed_args)?;
let mut ep_builder = register_start::Request::builder();
let user_id = if let Some(id) = &self.path.user.user_id {
id.clone()
} else if let Some(name) = &self.path.user.user_name {
let mut sub_find_builder = find_user::Request::builder();
warn!(
"Querying user by name (because of `--user-name` parameter passed) may not be definite. This may fail in which case parameter `--user-id` should be used instead."
);
sub_find_builder.id(name);
let find_ep = sub_find_builder
.build()
.map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?;
let find_data: serde_json::Value = find_by_name(find_ep).query_async(client).await?;
match find_data.get("id") {
Some(val) => match val.as_str() {
Some(id_str) => id_str.to_owned(),
None => {
return Err(OpenStackCliError::ResourceAttributeNotString(
serde_json::to_string(&val)?,
));
}
},
None => {
return Err(OpenStackCliError::ResourceAttributeMissing(
"id".to_string(),
));
}
}
} else if self.path.user.current_user {
client
.get_auth_info()
.ok_or_eyre("Cannot determine current authentication information")?
.token
.user
.id
} else {
return Err(eyre!("cannot determine the user").into());
};
ep_builder.user_id(user_id.clone());
let mut passkey = register_start::PasskeyBuilder::default();
if let Some(description) = &self.passkey.description {
passkey.description(description);
}
ep_builder.passkey(
passkey
.build()
.wrap_err_with(|| eyre!("cannot build `passkey` structure"))?,
);
let ep = ep_builder
.build()
.map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?;
let pk_request: register::RegisterStartResponse = ep.query_async(client).await?;
let mut auth = WebauthnAuthenticator::new(
webauthn_authenticator_rs::mozilla::MozillaAuthenticator::new(),
);
match auth.do_registration(
Url::parse("http://localhost:8080")?,
convert_api_parameters_to_webauthn(pk_request)?,
) {
Ok(rsp) => {
let ep = get_finish_registration_endpoint(
user_id,
rsp,
self.passkey.description.clone(),
)?;
let data = ep.query_async(client).await?;
op.output_single::<passkey::RegisterFinishResponse>(data)?;
}
Err(e) => {
error!("Error -> {:x?}", e);
return Err(eyre!("Registration failed").into());
}
}
op.show_command_hint()?;
Ok(())
}
}