Skip to content

Commit

Permalink
Add StatusUpdate::SelectResultNotice
Browse files Browse the repository at this point in the history
  • Loading branch information
jschanck committed Sep 19, 2023
1 parent 5d20800 commit d3a0d09
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 73 deletions.
3 changes: 3 additions & 0 deletions examples/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ fn main() {
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
58 changes: 50 additions & 8 deletions examples/ctap2_discoverable_creds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ use authenticator::{
use getopts::Options;
use sha2::{Digest, Sha256};
use std::sync::mpsc::{channel, RecvError};
use std::{env, thread};
use std::{env, io, thread};
use std::io::Write;

fn print_usage(program: &str, opts: Options) {
println!("------------------------------------------------------------------------");
Expand All @@ -28,6 +29,37 @@ fn print_usage(program: &str, opts: 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!("*********************************************************************");
Expand Down Expand Up @@ -98,6 +130,9 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select result notice")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down Expand Up @@ -181,6 +216,10 @@ fn main() {
"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..]) {
Expand Down Expand Up @@ -208,8 +247,10 @@ fn main() {
}
};

for username in &["A. User", "A. Nother", "Dr. Who"] {
register_user(&mut manager, username, timeout_ms)
if !matches.opt_present("skip_reg") {
for username in &["A. User", "A. Nother", "Dr. Who"] {
register_user(&mut manager, username, timeout_ms)
}
}

println!();
Expand Down Expand Up @@ -278,6 +319,11 @@ fn main() {
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;
Expand Down Expand Up @@ -322,11 +368,7 @@ fn main() {
println!("Found credentials:");
println!(
"{:?}",
assertion_object
.assertions
.iter()
.map(|x| x.user.clone().unwrap().name.unwrap()) // Unwrapping here, as these shouldn't fail
.collect::<Vec<_>>()
assertion_object.assertion.user.clone().unwrap().name.unwrap() // Unwrapping here, as these shouldn't fail
);
println!("-----------------------------------------------------------------");
println!("Done.");
Expand Down
3 changes: 3 additions & 0 deletions examples/interactive_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,9 @@ fn interactive_status_callback(status_rx: Receiver<StatusUpdate>) {
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
3 changes: 3 additions & 0 deletions examples/set_pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ fn main() {
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
3 changes: 3 additions & 0 deletions examples/test_exclude_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ fn main() {
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
101 changes: 47 additions & 54 deletions src/ctap2/commands/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,10 @@ impl GetAssertion {
// Handle extensions whose outputs are not encoded in the authenticator data.
// 1. appId
if let Some(app_id) = &self.extensions.app_id {
result.extensions.app_id = result
.assertions
.first()
.map(|assertion| {
assertion.auth_data.rp_id_hash
== RelyingPartyWrapper::from(app_id.as_str()).hash()
})
.or(Some(false));
result.extensions.app_id = Some(
result.assertion.auth_data.rp_id_hash
== RelyingPartyWrapper::from(app_id.as_str()).hash(),
);
}
}
}
Expand Down Expand Up @@ -307,7 +303,7 @@ impl Serialize for GetAssertion {
}

impl RequestCtap1 for GetAssertion {
type Output = GetAssertionResult;
type Output = Vec<GetAssertionResult>;
type AdditionalInfo = PublicKeyCredentialDescriptor;

fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError> {
Expand Down Expand Up @@ -358,24 +354,27 @@ impl RequestCtap1 for GetAssertion {
return Err(Retryable::Error(HIDError::ApduStatus(err)));
}

let mut output = GetAssertionResult::from_ctap1(input, &self.rp.hash(), add_info)
let mut result = GetAssertionResult::from_ctap1(input, &self.rp.hash(), add_info)
.map_err(|e| Retryable::Error(HIDError::Command(e)))?;
self.finalize_result(&mut output);
Ok(output)
self.finalize_result(&mut result);
// Although there's only one result, we return a vector for consistency with CTAP2.
Ok(vec![result])
}

fn send_to_virtual_device<Dev: VirtualFidoDevice>(
&self,
dev: &mut Dev,
) -> Result<Self::Output, HIDError> {
let mut output = dev.get_assertion(self)?;
self.finalize_result(&mut output);
Ok(output)
let mut results = dev.get_assertion(self)?;
for result in results.iter_mut() {
self.finalize_result(result);
}
Ok(results)
}
}

impl RequestCtap2 for GetAssertion {
type Output = GetAssertionResult;
type Output = Vec<GetAssertionResult>;

fn command(&self) -> Command {
Command::GetAssertion
Expand Down Expand Up @@ -411,22 +410,27 @@ impl RequestCtap2 for GetAssertion {
let assertion: GetAssertionResponse =
from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
let number_of_credentials = assertion.number_of_credentials.unwrap_or(1);
let mut assertions = Vec::with_capacity(number_of_credentials);
assertions.push(assertion.into());

let mut results = Vec::with_capacity(number_of_credentials);
results.push(GetAssertionResult {
assertion: assertion.into(),
extensions: Default::default(),
});

let msg = GetNextAssertion;
// We already have one, so skipping 0
for _ in 1..number_of_credentials {
let new_cred = dev.send_cbor(&msg)?;
assertions.push(new_cred.into());
let assertion = dev.send_cbor(&msg)?;
results.push(GetAssertionResult {
assertion: assertion.into(),
extensions: Default::default(),
});
}

let mut output = GetAssertionResult {
assertions,
extensions: Default::default(),
};
self.finalize_result(&mut output);
Ok(output)
for result in results.iter_mut() {
self.finalize_result(result);
}
Ok(results)
} else {
let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
Err(CommandError::StatusCode(status, Some(data)).into())
Expand All @@ -437,9 +441,11 @@ impl RequestCtap2 for GetAssertion {
&self,
dev: &mut Dev,
) -> Result<Self::Output, HIDError> {
let mut output = dev.get_assertion(self)?;
self.finalize_result(&mut output);
Ok(output)
let mut results = dev.get_assertion(self)?;
for result in results.iter_mut() {
self.finalize_result(result);
}
Ok(results)
}
}

Expand All @@ -465,7 +471,7 @@ impl From<GetAssertionResponse> for Assertion {

#[derive(Debug, PartialEq, Eq)]
pub struct GetAssertionResult {
pub assertions: Vec<Assertion>,
pub assertion: Assertion,
pub extensions: AuthenticationExtensionsClientOutputs,
}

Expand Down Expand Up @@ -501,23 +507,10 @@ impl GetAssertionResult {
};

Ok(GetAssertionResult {
assertions: vec![assertion],
assertion,
extensions: Default::default(),
})
}

pub fn u2f_sign_data(&self) -> Vec<u8> {
if let Some(first) = self.assertions.first() {
let mut res = Vec::new();
res.push(first.auth_data.flags.bits());
res.extend(first.auth_data.counter.to_be_bytes());
res.extend(&first.signature);
res
// first.signature.clone()
} else {
Vec::new()
}
}
}

pub struct GetAssertionResponse {
Expand Down Expand Up @@ -791,10 +784,10 @@ pub mod test {
auth_data: expected_auth_data,
};

let expected = GetAssertionResult {
assertions: vec![expected_assertion],
let expected = vec![GetAssertionResult {
assertion: expected_assertion,
extensions: Default::default(),
};
}];
let response = device.send_cbor(&assertion).unwrap();
assert_eq!(response, expected);
}
Expand Down Expand Up @@ -926,10 +919,10 @@ pub mod test {
auth_data: expected_auth_data,
};

let expected = GetAssertionResult {
assertions: vec![expected_assertion],
let expected = vec![GetAssertionResult {
assertion: expected_assertion,
extensions: Default::default(),
};
}];
assert_eq!(response, expected);
}

Expand Down Expand Up @@ -1070,10 +1063,10 @@ pub mod test {
auth_data: expected_auth_data,
};

let expected = GetAssertionResult {
assertions: vec![expected_assertion],
let expected = vec![GetAssertionResult {
assertion: expected_assertion,
extensions: Default::default(),
};
}];
assert_eq!(response, expected);
}

Expand Down Expand Up @@ -1338,7 +1331,7 @@ pub mod test {
let resp = GetAssertionResult::from_ctap1(&sample, &rp_hash, &add_info)
.expect("could not handle response");
assert_eq!(
resp.assertions[0].auth_data.flags,
resp.assertion.auth_data.flags,
AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::RESERVED_1
);
}
Expand Down
31 changes: 25 additions & 6 deletions src/ctap2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,15 +680,34 @@ pub fn sign<Dev: FidoDevice>(
debug!("{get_assertion:?} using {pin_uv_auth_result:?}");
debug!("------------------------------------------------------------------");
send_status(&status, crate::StatusUpdate::PresenceRequired);
let resp = dev.send_msg_cancellable(&get_assertion, alive);
match resp {
Ok(result) => {
callback.call(Ok(result));
return true;
}
let mut results = match dev.send_msg_cancellable(&get_assertion, alive) {
Ok(results) => results,
Err(e) => {
handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv);
}
};
if results.len() == 1 {
callback.call(Ok(results.swap_remove(0)));
return true;
}
let (tx, rx) = channel();
let user_entities = results
.iter()
.filter_map(|x| x.assertion.user.clone())
.collect();
send_status(
&status,
crate::StatusUpdate::SelectResultNotice(tx, user_entities),
);
match rx.recv() {
Ok(Some(index)) if index < results.len() => {
callback.call(Ok(results.swap_remove(index)));
return true;
}
_ => {
callback.call(Err(AuthenticatorError::CancelledByUser));
return true;
}
}
}
false
Expand Down
Loading

0 comments on commit d3a0d09

Please sign in to comment.