use std::collections::HashMap;
use std::convert::TryInto;
use std::error::Error;
use std::io::{self, Write};
use std::time::Duration;
use libwebauthn::transport::hid::channel::HidChannel;
use libwebauthn::UvUpdate;
use rand::{thread_rng, Rng};
use serde_bytes::ByteBuf;
use text_io::read;
use tokio::sync::broadcast::Receiver;
use tracing_subscriber::{self, EnvFilter};
use libwebauthn::ops::webauthn::{
GetAssertionRequest, GetAssertionRequestExtensions, PRFValue, PrfInput,
UserVerificationRequirement,
};
use libwebauthn::pin::PinRequestReason;
use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType};
use libwebauthn::transport::hid::list_devices;
use libwebauthn::transport::{Channel as _, Device};
use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn};
const TIMEOUT: Duration = Duration::from_secs(10);
fn setup_logging() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.without_time()
.init();
}
async fn handle_updates(mut state_recv: Receiver<UvUpdate>) {
while let Ok(update) = state_recv.recv().await {
match update {
UvUpdate::PresenceRequired => println!("Please touch your device!"),
UvUpdate::PinNotSet(_) => println!("Pin not set for your device!"),
UvUpdate::UvRetry { attempts_left } => {
print!("UV failed.");
if let Some(attempts_left) = attempts_left {
print!(" You have {attempts_left} attempts left.");
}
}
UvUpdate::PinRequired(update) => {
let mut attempts_str = String::new();
if let Some(attempts) = update.attempts_left {
attempts_str = format!(". You have {attempts} attempts left!");
};
match update.reason {
PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."),
PinRequestReason::AuthenticatorPolicy => {
println!("Your device requires a PIN.")
}
PinRequestReason::FallbackFromUV => {
println!("UV failed too often and is blocked. Falling back to PIN.")
}
}
print!("PIN: Please enter the PIN for your authenticator{attempts_str}: ");
io::stdout().flush().unwrap();
let pin_raw: String = read!("{}\n");
if pin_raw.is_empty() {
println!("PIN: No PIN provided, cancelling operation.");
update.cancel();
} else {
let _ = update.send_pin(&pin_raw);
}
}
}
}
}
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn Error>> {
setup_logging();
let argv: Vec<_> = std::env::args().collect();
if argv.len() != 3 {
println!("Usage: cargo run --example prf_test -- CREDENTIAL_ID FIRST_PRF_INPUT");
println!();
println!("CREDENTIAL_ID: Credential ID to be used to sign against, as a hexstring (like 5830c80ae90f7865c631626573f1fdc7..)");
println!(
"FIRST_PRF_INPUT: PRF input to be used as a hexstring. Needs to be 32 bytes long!"
);
println!();
println!("How to use:");
println!("1. Go to https://demo.yubico.com/webauthn-developers");
println!("2. Register there with PRF extension enabled, using your favorite browser");
println!("3. Sign in, with FIRST_PRF_INPUT set");
println!("4. Copy out the used hexstrings for credential_id and PRF input, and use them with this example");
println!("5. Hope the outputs match");
return Ok(());
}
let credential_id =
hex::decode(argv[1].clone()).expect("CREDENTIAL_ID is not a valid hex code");
let first_prf_input = hex::decode(argv[2].clone())
.expect("FIRST_PRF_INPUT is not a valid hex code")
.try_into()
.expect("FIRST_PRF_INPUT is not exactly 32 bytes long");
let devices = list_devices().await.unwrap();
println!("Devices found: {:?}", devices);
let challenge: [u8; 32] = thread_rng().gen();
for mut device in devices {
println!("Selected HID authenticator: {}", &device);
let mut channel = device.channel().await?;
channel.wink(TIMEOUT).await?;
let state_recv = channel.get_ux_update_receiver();
tokio::spawn(handle_updates(state_recv));
let credential = Ctap2PublicKeyCredentialDescriptor {
r#type: Ctap2PublicKeyCredentialType::PublicKey,
id: ByteBuf::from(credential_id.as_slice()),
transports: None,
};
let eval = Some(PRFValue {
first: first_prf_input,
second: None,
});
let eval_by_credential = HashMap::new();
let prf = PrfInput {
eval,
eval_by_credential,
};
run_success_test(&mut channel, &credential, &challenge, prf, "PRF output: ").await;
}
Ok(())
}
async fn run_success_test(
channel: &mut HidChannel<'_>,
credential: &Ctap2PublicKeyCredentialDescriptor,
challenge: &[u8; 32],
prf: PrfInput,
printoutput: &str,
) {
let get_assertion = GetAssertionRequest {
relying_party_id: "demo.yubico.com".to_owned(),
challenge: Vec::from(challenge),
origin: "demo.yubico.com".to_string(),
cross_origin: None,
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Preferred,
extensions: Some(GetAssertionRequestExtensions {
prf: Some(prf),
..Default::default()
}),
timeout: TIMEOUT,
};
let response = loop {
match channel.webauthn_get_assertion(&get_assertion).await {
Ok(response) => break Ok(response),
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();
for (num, assertion) in response.assertions.iter().enumerate() {
println!(
"{num}. result of {printoutput}: {:?}",
assertion
.unsigned_extensions_output
.as_ref()
.map(|e| if let Some(prf) = &e.prf {
let results = prf.results.as_ref().map(|r| hex::encode(r.first)).unwrap();
format!("Found PRF results: {}", results)
} else if e.hmac_get_secret.is_some() {
String::from("ERROR: Got HMAC instead of PRF output")
} else {
String::from("ERROR: No PRF output")
})
.unwrap_or(String::from("ERROR: No extensions returned"))
);
}
}