Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions libwebauthn/examples/authenticator_config_hid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use std::error::Error;
use std::fmt::Display;
use std::time::Duration;

use libwebauthn::management::AuthenticatorConfig;
use libwebauthn::pin::{PinProvider, StdinPromptPinProvider};
use libwebauthn::proto::ctap2::{Ctap2, Ctap2GetInfoResponse};
use libwebauthn::transport::hid::list_devices;
use libwebauthn::transport::Device;
use libwebauthn::webauthn::Error as WebAuthnError;
use std::io::{self, Write};
use text_io::read;
use tracing_subscriber::{self, EnvFilter};

const TIMEOUT: Duration = Duration::from_secs(10);

fn setup_logging() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.without_time()
.init();
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Operation {
ToggleAlwaysUv,
EnableForceChangePin,
DisableForceChangePin,
SetMinPinLength(Option<u32>),
SetMinPinLengthRpids,
EnableEnterpriseAttestation,
}

impl Display for Operation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Operation::ToggleAlwaysUv => f.write_str("Toggle AlwaysUV"),
Operation::EnableForceChangePin => f.write_str("Enable force change pin"),
Operation::DisableForceChangePin => f.write_str("Disable force change pin"),
Operation::SetMinPinLength(l) => {
if let Some(length) = l {
f.write_fmt(format_args!("Set min PIN length. Current length: {length}"))
} else {
f.write_str("Set min PIN length.")
}
}
Operation::SetMinPinLengthRpids => f.write_str("Set min PIN length RPIDs"),
Operation::EnableEnterpriseAttestation => f.write_str("Enable enterprise attestation"),
}
}
}

fn get_supported_options(info: &Ctap2GetInfoResponse) -> Vec<Operation> {
let mut configure_ops = vec![];
if let Some(options) = &info.options {
if options.get("authnrCfg") == Some(&true) && options.get("alwaysUv").is_some() {
configure_ops.push(Operation::ToggleAlwaysUv);
}
if options.get("authnrCfg") == Some(&true) && options.get("setMinPINLength").is_some() {
if info.force_pin_change == Some(true) {
configure_ops.push(Operation::DisableForceChangePin);
} else {
configure_ops.push(Operation::EnableForceChangePin);
}
configure_ops.push(Operation::SetMinPinLength(info.min_pin_length));
configure_ops.push(Operation::SetMinPinLengthRpids);
}
if options.get("ep").is_some() {
configure_ops.push(Operation::EnableEnterpriseAttestation);
}
}
configure_ops
}

#[tokio::main]
pub async fn main() -> Result<(), Box<dyn Error>> {
setup_logging();

let devices = list_devices().await.unwrap();
println!("Devices found: {:?}", devices);
let pin_provider: Box<dyn PinProvider> = Box::new(StdinPromptPinProvider::new());

for mut device in devices {
println!("Selected HID authenticator: {}", &device);
device.wink(TIMEOUT).await?;

let mut channel = device.channel().await?;
let info = channel.ctap2_get_info().await?;
let options = get_supported_options(&info);

println!("What do you want to do?");
println!();
for (idx, op) in options.iter().enumerate() {
println!("({idx}) {op}");
}

let idx = loop {
print!("Your choice: ");
io::stdout().flush().expect("Failed to flush stdout!");
let input: String = read!("{}\n");
if let Ok(idx) = input.trim().parse::<usize>() {
if idx < options.len() {
println!();
break idx;
}
}
};

let mut min_pin_length_rpids = Vec::new();
if options[idx] == Operation::SetMinPinLengthRpids {
loop {
print!("Add RPIDs to list (enter empty string once finished): ");
io::stdout().flush().expect("Failed to flush stdout!");
let input: String = read!("{}\n");
let trimmed = input.trim().to_string();
if trimmed.is_empty() {
break;
} else {
min_pin_length_rpids.push(trimmed);
}
}
};

let new_pin_length = if matches!(options[idx], Operation::SetMinPinLength(..)) {
loop {
print!("New minimum PIN length: ");
io::stdout().flush().expect("Failed to flush stdout!");
let input: String = read!("{}\n");
match input.trim().parse::<u64>() {
Ok(l) => {
break l;
}
Err(_) => continue,
}
}
} else {
0
};

loop {
let action = match options[idx] {
Operation::ToggleAlwaysUv => channel.toggle_always_uv(&pin_provider, TIMEOUT),
Operation::SetMinPinLengthRpids => channel.set_min_pin_length_rpids(
min_pin_length_rpids.clone(),
&pin_provider,
TIMEOUT,
),
Operation::SetMinPinLength(..) => {
channel.set_min_pin_length(new_pin_length, &pin_provider, TIMEOUT)
}
Operation::EnableEnterpriseAttestation => {
channel.enable_enterprise_attestation(&pin_provider, TIMEOUT)
}
Operation::EnableForceChangePin => {
channel.force_change_pin(true, &pin_provider, TIMEOUT)
}
Operation::DisableForceChangePin => {
channel.force_change_pin(false, &pin_provider, TIMEOUT)
}
};
match action.await {
Ok(_) => break Ok(()),
Err(WebAuthnError::Ctap(ctap_error)) => {
if ctap_error.is_retryable_user_error() {
println!("Oops, try again! Error: {}", ctap_error);
continue;
}
break Err(WebAuthnError::Ctap(ctap_error));
}
Err(err) => break Err(err),
};
}
.unwrap();
println!("Authenticator config done!");
}

Ok(())
}
1 change: 1 addition & 0 deletions libwebauthn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod fido;
pub mod management;
pub mod ops;
pub mod pin;
pub mod proto;
Expand Down
198 changes: 198 additions & 0 deletions libwebauthn/src/management.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use serde_cbor::ser::to_vec;
use std::time::Duration;

use crate::pin::{PinProvider, PinUvAuthProtocol};
use crate::proto::ctap2::Ctap2AuthenticatorConfigCommand;
pub use crate::transport::error::{CtapError, Error, TransportError};
use crate::transport::Channel;
use crate::webauthn::user_verification;
use crate::{
ops::webauthn::UserVerificationRequirement,
proto::ctap2::{
ClientPinRequestPermissions, Ctap2, Ctap2AuthenticatorConfigRequest,
Ctap2UserVerifiableRequest,
},
};
use async_trait::async_trait;
use serde_bytes::ByteBuf;

#[async_trait]
pub trait AuthenticatorConfig {
async fn toggle_always_uv(
&mut self,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error>;

async fn enable_enterprise_attestation(
&mut self,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error>;

async fn set_min_pin_length(
&mut self,
new_pin_length: u64,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error>;

async fn force_change_pin(
&mut self,
force: bool,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error>;

async fn set_min_pin_length_rpids(
&mut self,
rpids: Vec<String>,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error>;
}

#[async_trait]
impl<C> AuthenticatorConfig for C
where
C: Channel,
{
async fn toggle_always_uv(
&mut self,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error> {
let mut req = Ctap2AuthenticatorConfigRequest::new_toggle_always_uv();

user_verification(
self,
UserVerificationRequirement::Required,
&mut req,
pin_provider,
timeout,
)
.await?;

// On success, this is an all-empty Ctap2ClientPinResponse
self.ctap2_authenticator_config(&req, timeout).await
}

async fn enable_enterprise_attestation(
&mut self,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error> {
let mut req = Ctap2AuthenticatorConfigRequest::new_enable_enterprise_attestation();

user_verification(
self,
UserVerificationRequirement::Required,
&mut req,
pin_provider,
timeout,
)
.await?;

// On success, this is an all-empty Ctap2ClientPinResponse
self.ctap2_authenticator_config(&req, timeout).await
}

async fn set_min_pin_length(
&mut self,
new_pin_length: u64,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error> {
let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length(new_pin_length);

user_verification(
self,
UserVerificationRequirement::Required,
&mut req,
pin_provider,
timeout,
)
.await?;

// On success, this is an all-empty Ctap2ClientPinResponse
self.ctap2_authenticator_config(&req, timeout).await
}

async fn force_change_pin(
&mut self,
force: bool,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error> {
let mut req = Ctap2AuthenticatorConfigRequest::new_force_change_pin(force);

user_verification(
self,
UserVerificationRequirement::Required,
&mut req,
pin_provider,
timeout,
)
.await?;

// On success, this is an all-empty Ctap2ClientPinResponse
self.ctap2_authenticator_config(&req, timeout).await
}

async fn set_min_pin_length_rpids(
&mut self,
rpids: Vec<String>,
pin_provider: &Box<dyn PinProvider>,
timeout: Duration,
) -> Result<(), Error> {
let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length_rpids(rpids);

user_verification(
self,
UserVerificationRequirement::Required,
&mut req,
pin_provider,
timeout,
)
.await?;

// On success, this is an all-empty Ctap2ClientPinResponse
self.ctap2_authenticator_config(&req, timeout).await
}
}

impl Ctap2UserVerifiableRequest for Ctap2AuthenticatorConfigRequest {
fn ensure_uv_set(&mut self) {
// No-op
}

fn calculate_and_set_uv_auth(
&mut self,
uv_proto: &Box<dyn PinUvAuthProtocol>,
uv_auth_token: &[u8],
) {
// pinUvAuthParam (0x04): the result of calling
// authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams).
let mut data = vec![0xff; 32];
data.push(0x0D);
data.push(self.subcommand as u8);
if self.subcommand == Ctap2AuthenticatorConfigCommand::SetMinPINLength {
data.extend(to_vec(&self.subcommand_params).unwrap());
}
let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data);
self.protocol = Some(uv_proto.version());
self.uv_auth_param = Some(ByteBuf::from(uv_auth_param));
}

fn client_data_hash(&self) -> &[u8] {
unreachable!()
}

fn permissions(&self) -> ClientPinRequestPermissions {
return ClientPinRequestPermissions::AUTHENTICATOR_CONFIGURATION;
}

fn permissions_rpid(&self) -> Option<&str> {
None
}
}
Loading
Loading