/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use authenticator::{ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, crypto::COSEAlgorithm, ctap2::server::{ AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, UserVerificationRequirement, }, statecallback::StateCallback, Pin, StatusPinUv, StatusUpdate, }; use getopts::Options; use sha2::{Digest, Sha256}; use std::sync::mpsc::{channel, RecvError}; use std::{env, io, thread}; use std::io::Write; fn print_usage(program: &str, opts: Options) { println!("------------------------------------------------------------------------"); println!("This program registers 3x the same origin with different users and"); println!("requests 'discoverable credentials' for them."); println!("After that, we try to log in to that origin and list all credentials found."); println!("------------------------------------------------------------------------"); let brief = format!("Usage: {program} [options]"); print!("{}", opts.usage(&brief)); } fn ask_user_choice(choices: &[PublicKeyCredentialUserEntity]) -> Option { for (idx, op) in choices.iter().enumerate() { println!("({idx}) \"{}\"", op.name.as_ref().unwrap()); } println!("({}) Cancel", choices.len()); let mut input = String::new(); loop { input.clear(); print!("Your choice: "); io::stdout() .lock() .flush() .expect("Failed to flush stdout!"); io::stdin() .read_line(&mut input) .expect("error: unable to read user input"); if let Ok(idx) = input.trim().parse::() { if idx < choices.len() { // Add a newline in case of success for better separation of in/output println!(); return Some(idx); } else if idx == choices.len() { println!(); return None; } println!("invalid input"); } } } fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) { println!(); println!("*********************************************************************"); println!("Asking a security key to register now with user: {username}"); println!("*********************************************************************"); println!("Asking a security key to register now..."); let challenge_str = format!( "{}{}{}{}", r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#, r#" "version": "U2F_V2", "appId": "http://example.com", "username": ""#, username, r#""}"# ); let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { match status_rx.recv() { Ok(StatusUpdate::InteractiveManagement(..)) => { panic!("STATUS: This can't happen when doing non-interactive usage"); } Ok(StatusUpdate::SelectDeviceNotice) => { println!("STATUS: Please select a device by touching one of them."); } Ok(StatusUpdate::PresenceRequired) => { println!("STATUS: waiting for user presence"); } Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); continue; } Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { println!( "Wrong PIN! {}", attempts.map_or("Try again.".to_string(), |a| format!( "You have {a} attempts left." )) ); let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); continue; } Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") } Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { panic!("Too many failed attempts. Your device has been blocked. Reset it.") } Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { println!( "Wrong UV! {}", attempts.map_or("Try again.".to_string(), |a| format!( "You have {a} attempts left." )) ); continue; } Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { println!("Too many failed UV-attempts."); continue; } Ok(StatusUpdate::PinUvError(e)) => { panic!("Unexpected error: {:?}", e) } Ok(StatusUpdate::SelectResultNotice(_, _)) => { panic!("Unexpected select result notice") } Err(RecvError) => { println!("STATUS: end"); return; } } }); let user = PublicKeyCredentialUserEntity { id: username.as_bytes().to_vec(), name: Some(username.to_string()), display_name: None, }; let origin = "https://example.com".to_string(); let ctap_args = RegisterArgs { client_data_hash: chall_bytes, relying_party: RelyingParty { id: "example.com".to_string(), name: None, }, origin, user, pub_cred_params: vec![ PublicKeyCredentialParameters { alg: COSEAlgorithm::ES256, }, PublicKeyCredentialParameters { alg: COSEAlgorithm::RS256, }, ], exclude_list: vec![PublicKeyCredentialDescriptor { id: vec![], transports: vec![Transport::USB, Transport::NFC], }], user_verification_req: UserVerificationRequirement::Required, resident_key_req: ResidentKeyRequirement::Required, extensions: AuthenticationExtensionsClientInputs { cred_props: Some(true), ..Default::default() }, pin: None, use_ctap1_fallback: false, }; let attestation_object; loop { let (register_tx, register_rx) = channel(); let callback = StateCallback::new(Box::new(move |rv| { register_tx.send(rv).unwrap(); })); if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx, callback) { panic!("Couldn't register: {:?}", e); }; let register_result = register_rx .recv() .expect("Problem receiving, unable to continue"); match register_result { Ok(a) => { println!("Ok!"); attestation_object = a; break; } Err(e) => panic!("Registration failed: {:?}", e), }; } println!("Register result: {:?}", &attestation_object); } fn main() { env_logger::init(); let args: Vec = env::args().collect(); let program = args[0].clone(); let mut opts = Options::new(); opts.optflag("h", "help", "print this help menu").optopt( "t", "timeout", "timeout in seconds", "SEC", ); opts.optflag( "s", "skip_reg", "Skip registration"); opts.optflag("h", "help", "print this help menu"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), }; if matches.opt_present("help") { print_usage(&program, opts); return; } let mut manager = AuthenticatorService::new().expect("The auth service should initialize safely"); manager.add_u2f_usb_hid_platform_transports(); let timeout_ms = match matches.opt_get_default::("timeout", 15) { Ok(timeout_s) => { println!("Using {}s as the timeout", &timeout_s); timeout_s * 1_000 } Err(e) => { println!("{e}"); print_usage(&program, opts); return; } }; if !matches.opt_present("skip_reg") { for username in &["A. User", "A. Nother", "Dr. Who"] { register_user(&mut manager, username, timeout_ms) } } println!(); println!("*********************************************************************"); println!("Asking a security key to sign now, with the data from the register..."); println!("*********************************************************************"); // Discovering creds: let allow_list = Vec::new(); let origin = "https://example.com".to_string(); let challenge_str = format!( "{}{}", r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#, r#" "version": "U2F_V2", "appId": "http://example.com" "#, ); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { match status_rx.recv() { Ok(StatusUpdate::InteractiveManagement(..)) => { panic!("STATUS: This can't happen when doing non-interactive usage"); } Ok(StatusUpdate::SelectDeviceNotice) => { println!("STATUS: Please select a device by touching one of them."); } Ok(StatusUpdate::PresenceRequired) => { println!("STATUS: waiting for user presence"); } Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); continue; } Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { println!( "Wrong PIN! {}", attempts.map_or("Try again.".to_string(), |a| format!( "You have {a} attempts left." )) ); let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); continue; } Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") } Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { panic!("Too many failed attempts. Your device has been blocked. Reset it.") } Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { println!( "Wrong UV! {}", attempts.map_or("Try again.".to_string(), |a| format!( "You have {a} attempts left." )) ); continue; } Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { println!("Too many failed UV-attempts."); continue; } Ok(StatusUpdate::PinUvError(e)) => { panic!("Unexpected error: {:?}", e) } Ok(StatusUpdate::SelectResultNotice(index_sender, users)) => { println!("Multiple signatures returned. Select one or cancel."); let idx = ask_user_choice(&users); index_sender.send(idx).expect("Failed to send choice"); } Err(RecvError) => { println!("STATUS: end"); return; } } }); let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let ctap_args = SignArgs { client_data_hash: chall_bytes, origin, relying_party_id: "example.com".to_string(), allow_list, user_verification_req: UserVerificationRequirement::Required, user_presence_req: true, extensions: Default::default(), pin: None, use_ctap1_fallback: false, }; loop { let (sign_tx, sign_rx) = channel(); let callback = StateCallback::new(Box::new(move |rv| { sign_tx.send(rv).unwrap(); })); if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) { panic!("Couldn't sign: {:?}", e); } let sign_result = sign_rx .recv() .expect("Problem receiving, unable to continue"); match sign_result { Ok(assertion_object) => { println!("Assertion Object: {assertion_object:?}"); println!("-----------------------------------------------------------------"); println!("Found credentials:"); println!( "{:?}", assertion_object.assertion.user.clone().unwrap().name.unwrap() // Unwrapping here, as these shouldn't fail ); println!("-----------------------------------------------------------------"); println!("Done."); break; } Err(e) => panic!("Signing failed: {:?}", e), } } }