377 строки
14 KiB
Rust
377 строки
14 KiB
Rust
/* 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<usize> {
|
|
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::<usize>() {
|
|
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::<StatusUpdate>();
|
|
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<String> = 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::<u64>("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::<StatusUpdate>();
|
|
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),
|
|
}
|
|
}
|
|
}
|