Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for WebAuthn PRF extension #337

Merged
merged 42 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ebe8793
Add support for WebAuthn PRF extension
emlun May 24, 2024
a701708
Send correct PIN protocol ID in hmac-secret
emlun May 24, 2024
5cc6c14
Extract function HmacSecretResponse::decrypt_secrets
emlun May 30, 2024
c4aa497
Clarify and correct hmac-secret and PRF client outputs in makeCredential
emlun Jun 4, 2024
1cac147
Delete unnecessary impl Default
emlun Jun 4, 2024
dee98a0
Rename HmacSecretFromHmacSecretOrPrf to HmacCreateSecretOrPrf
emlun Jun 4, 2024
501ad7f
Use HmacGetSecretOrPrf data model in getAssertion too
emlun Jun 4, 2024
ba1e548
Add examples/prf.rs
emlun Jun 4, 2024
8f63de6
Construct channels outside loop
emlun Jun 4, 2024
e1fce6e
Remove unused loop
emlun Jun 4, 2024
a74120a
Add tests for HmacSecretResponse::decrypt_secrets
emlun Jun 5, 2024
15b1f5b
Extract function AuthenticationExtensionsPRFInputs::eval_to_salt
emlun Jun 5, 2024
0df9cf2
Extract AuthenticationExtensionsPRFInputs::select_eval and ::select_c…
emlun Jun 5, 2024
7222884
Add doc comment to AuthenticationExtensionsPRFInputs::calculate
emlun Jun 5, 2024
25a535e
Fix clippy lint
emlun Jun 10, 2024
c6d0756
Return empty prf output if no eval or evalByCredential entry matched
emlun Jun 10, 2024
04191f9
Extract function HmacGetSecretOrPrf::calculate
emlun Jun 13, 2024
c6b4479
Add tests of calculating hmac-secret/PRF inputs
emlun Jun 13, 2024
269eb6a
Fix outdated error messages
emlun Jun 13, 2024
5fb07b1
Separate hmac_secret tests that require a crypto backend
emlun Jul 8, 2024
3c04dda
Add debug output to error paths of HmacSecretResponse::decrypt_secrets
emlun Jul 8, 2024
0f440aa
Fix a typo and a cryptic comment
emlun Jul 8, 2024
2a5baf5
Eliminate unnecessary sha256 function
emlun Jul 8, 2024
486a7e9
Simplify to Sha256::digest where possible
emlun Jul 8, 2024
093957b
Derive PartialEq always, not just in cfg(test)
emlun Jul 8, 2024
01cb0d3
Document generation of hmac_secret test data
emlun Jul 8, 2024
beda054
Remove unnecessary comma
emlun Jul 8, 2024
5035146
Tweak imports per review
emlun Jul 8, 2024
88b8db7
Take PinUvAuthToken as reference in HmacSecretExtension::calculate
emlun Jul 8, 2024
80d0f35
Deduplicate decrypt_pin_token code in tests
emlun Jul 8, 2024
eede8c7
Extract function GetAssertion::process_hmac_secret_and_prf_extension
emlun Jul 10, 2024
fd8e062
Move allow_list assignment to top level scope
emlun Jul 10, 2024
06e74c4
Add tests of hmac-secret and prf processing in GetAssertion::finalize…
emlun Jul 10, 2024
5cebc5b
Fail hmac-secret salt calculation if input salts are too long
emlun Jul 8, 2024
b959eeb
Add tests of GetAssertion::process_hmac_secret_and_prf_extension
emlun Jul 10, 2024
9036264
Propagate WrongSaltLength as InvalidRelyingPartyInput in GetAssertion…
emlun Jul 10, 2024
978725b
Return PrfUnmatched instead of None when shared secret is not available
emlun Jul 10, 2024
b833a60
Add debug logging when no shared secret is available
emlun Jul 10, 2024
a02ed2b
Add debug logging when hmac-secret output decryption fails
emlun Jul 10, 2024
f4fda67
Merge remote-tracking branch 'origin/ctap2-2021' into prf-extension
emlun Jul 19, 2024
4ae6410
Add test of serializing uninitialized and unmatched PRF inputs
emlun Jul 19, 2024
cf92871
Add missing test of serializing hmac-secret with PIN protocol 2
emlun Jul 19, 2024
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
307 changes: 307 additions & 0 deletions examples/prf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
/* 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, AuthenticationExtensionsPRFInputs,
AuthenticationExtensionsPRFValues, HMACGetSecretInput, PublicKeyCredentialDescriptor,
PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty,
ResidentKeyRequirement, Transport, UserVerificationRequirement,
},
statecallback::StateCallback,
Pin, StatusPinUv, StatusUpdate,
};
use getopts::Options;
use rand::{thread_rng, RngCore};
use std::sync::mpsc::{channel, RecvError};
use std::{env, thread};

fn print_usage(program: &str, opts: Options) {
let brief = format!("Usage: {program} [options]");
print!("{}", opts.usage(&brief));
}

fn main() {
env_logger::init();

let args: Vec<String> = env::args().collect();
let program = args[0].clone();

let rp_id = "example.com".to_string();

let mut opts = Options::new();
opts.optflag("h", "help", "print this help menu").optopt(
"t",
"timeout",
"timeout in seconds",
"SEC",
);
opts.optflag("h", "help", "print this help menu");
opts.optflag(
"",
"hmac-secret",
"Return hmac-secret outputs instead of prf outputs (i.e., do not prefix and hash the inputs)",
);
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", 25) {
Ok(timeout_s) => {
println!("Using {}s as the timeout", &timeout_s);
timeout_s * 1_000
}
Err(e) => {
println!("{e}");
print_usage(&program, opts);
return;
}
};

let (register_hmac_secret, sign_hmac_secret, register_prf, sign_prf) =
if matches.opt_present("hmac-secret") {
let register_hmac_secret = Some(true);
let sign_hmac_secret = Some(HMACGetSecretInput {
salt1: [0x07; 32],
salt2: Some([0x07; 32]),
});
(register_hmac_secret, sign_hmac_secret, None, None)
} else {
let register_prf = Some(AuthenticationExtensionsPRFInputs::default());
let sign_prf = Some(AuthenticationExtensionsPRFInputs {
eval: Some(AuthenticationExtensionsPRFValues {
first: vec![1, 2, 3, 4],
second: Some(vec![1, 2, 3, 4]),
}),
eval_by_credential: None,
});
(None, None, register_prf, sign_prf)
};

println!("Asking a security key to register now...");
let mut chall_bytes = [0u8; 32];
thread_rng().fill_bytes(&mut chall_bytes);

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 device notice")
}
Err(RecvError) => {
println!("STATUS: end");
return;
}
}
});

let user = PublicKeyCredentialUserEntity {
id: "user_id".as_bytes().to_vec(),
name: Some("A. User".to_string()),
display_name: None,
};
let relying_party = RelyingParty {
id: rp_id.clone(),
name: None,
};
let ctap_args = RegisterArgs {
client_data_hash: chall_bytes,
relying_party,
origin: format!("https://{rp_id}"),
user,
pub_cred_params: vec![
PublicKeyCredentialParameters {
alg: COSEAlgorithm::ES256,
},
PublicKeyCredentialParameters {
alg: COSEAlgorithm::RS256,
},
],
exclude_list: vec![],
user_verification_req: UserVerificationRequirement::Required,
resident_key_req: ResidentKeyRequirement::Discouraged,
extensions: AuthenticationExtensionsClientInputs {
hmac_create_secret: register_hmac_secret,
prf: register_prf,
..Default::default()
},
pin: None,
use_ctap1_fallback: false,
};

let attestation_object;
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.clone(), 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;
}
Err(e) => panic!("Registration failed: {:?}", e),
};

println!("Register result: {:?}", &attestation_object);

println!();
println!("*********************************************************************");
println!("Asking a security key to sign now, with the data from the register...");
println!("*********************************************************************");

let allow_list;
if let Some(cred_data) = attestation_object.att_obj.auth_data.credential_data {
allow_list = vec![PublicKeyCredentialDescriptor {
id: cred_data.credential_id,
transports: vec![Transport::USB],
}];
} else {
allow_list = Vec::new();
}

let ctap_args = SignArgs {
client_data_hash: chall_bytes,
origin: format!("https://{rp_id}"),
relying_party_id: rp_id,
allow_list,
user_verification_req: UserVerificationRequirement::Required,
user_presence_req: true,
extensions: AuthenticationExtensionsClientInputs {
hmac_get_secret: sign_hmac_secret.clone(),
prf: sign_prf.clone(),
..Default::default()
},
pin: None,
use_ctap1_fallback: false,
};

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!("Done.");

if sign_hmac_secret.is_some() {
let hmac_secret_outputs = assertion_object
.extensions
.hmac_get_secret
.as_ref()
.expect("Expected hmac-secret output");

assert_eq!(
Some(hmac_secret_outputs.output1),
hmac_secret_outputs.output2,
"Expected hmac-secret outputs to be equal for equal input"
);

assert_eq!(
assertion_object.extensions.prf, None,
"Expected no PRF outputs when hmacGetSecret input was present"
);
}

if sign_prf.is_some() {
let prf_results = assertion_object
.extensions
.prf
.expect("Expected PRF output")
.results
.expect("Expected PRF output to contain results");

assert_eq!(
Some(prf_results.first),
prf_results.second,
"Expected PRF results to be equal for equal input"
);

assert_eq!(
assertion_object.extensions.hmac_get_secret, None,
"Expected no hmacGetSecret output when PRF input was present"
);
}
}

Err(e) => panic!("Signing failed: {:?}", e),
}
}
18 changes: 18 additions & 0 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,23 @@ impl SharedSecret {
pub fn peer_input(&self) -> &COSEKey {
&self.inputs.peer
}

#[cfg(test)]
pub fn new_test(
pin_protocol: PinUvAuthProtocol,
key: Vec<u8>,
client_input: COSEKey,
peer_input: COSEKey,
) -> Self {
Self {
pin_protocol,
key,
inputs: PublicInputs {
client: client_input,
peer: peer_input,
},
}
}
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -1056,6 +1073,7 @@ impl Serialize for COSEKey {

/// Errors that can be returned from COSE functions.
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(test, derive(PartialEq))]
jschanck marked this conversation as resolved.
Show resolved Hide resolved
pub enum CryptoError {
// DecodingFailure,
LibraryFailure,
Expand Down
Loading
Loading