diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index d19ccc6f..4667dcf5 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -74,9 +74,7 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: username, r#""}"# ); - let mut challenge = Sha256::new(); - challenge.update(challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().into(); + let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { @@ -331,9 +329,7 @@ fn main() { } }); - let mut challenge = Sha256::new(); - challenge.update(challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().into(); + let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let ctap_args = SignArgs { client_data_hash: chall_bytes, origin, diff --git a/examples/prf.rs b/examples/prf.rs new file mode 100644 index 00000000..58c89efd --- /dev/null +++ b/examples/prf.rs @@ -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 = 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::("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::(); + 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), + } +} diff --git a/examples/test_exclude_list.rs b/examples/test_exclude_list.rs index e24c49d0..958d66b1 100644 --- a/examples/test_exclude_list.rs +++ b/examples/test_exclude_list.rs @@ -72,9 +72,7 @@ fn main() { r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#, r#" "version": "U2F_V2", "appId": "http://example.com"}"# ); - let mut challenge = Sha256::new(); - challenge.update(challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().into(); + let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 72fece4f..a86dac82 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -341,6 +341,23 @@ impl SharedSecret { pub fn peer_input(&self) -> &COSEKey { &self.inputs.peer } + + #[cfg(test)] + pub fn new_test( + pin_protocol: PinUvAuthProtocol, + key: Vec, + client_input: COSEKey, + peer_input: COSEKey, + ) -> Self { + Self { + pin_protocol, + key, + inputs: PublicInputs { + client: client_input, + peer: peer_input, + }, + } + } } #[derive(Clone, Debug)] @@ -1073,7 +1090,7 @@ impl Serialize for COSEKey { } /// Errors that can be returned from COSE functions. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum CryptoError { // DecodingFailure, LibraryFailure, diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index ae592cea..ad5da1b0 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1,6 +1,6 @@ use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte}; -use crate::crypto::COSEAlgorithm; -use crate::ctap2::server::{CredentialProtectionPolicy, RpIdHash}; +use crate::crypto::{COSEAlgorithm, CryptoError, SharedSecret}; +use crate::ctap2::server::{CredentialProtectionPolicy, HMACGetSecretOutput, RpIdHash}; use crate::ctap2::utils::serde_parse_err; use crate::{crypto::COSEKey, errors::AuthenticatorError}; use base64::Engine; @@ -10,6 +10,7 @@ use serde::{ Deserialize, Deserializer, Serialize, }; use serde_cbor; +use std::convert::TryInto; use std::fmt; use std::io::{Cursor, Read}; @@ -23,6 +24,50 @@ pub enum HmacSecretResponse { Secret(Vec), } +impl HmacSecretResponse { + /// Return the decrypted HMAC outputs, if this is an instance of [HmacSecretResponse::Secret]. + pub fn decrypt_secrets( + &self, + shared_secret: &SharedSecret, + ) -> Option> { + if let HmacSecretResponse::Secret(hmac_outputs) = self { + Some(Self::decrypt_secrets_internal(shared_secret, hmac_outputs)) + } else { + None + } + } + + fn decrypt_secrets_internal( + shared_secret: &SharedSecret, + hmac_outputs: &[u8], + ) -> Result { + let output_secrets = shared_secret.decrypt(hmac_outputs)?; + match if output_secrets.len() < 32 { + Err(CryptoError::WrongSaltLength) + } else { + let (output1, output2) = output_secrets.split_at(32); + Ok(HMACGetSecretOutput { + output1: output1 + .try_into() + .map_err(|_| CryptoError::WrongSaltLength)?, + output2: (!output2.is_empty()) + .then(|| output2.try_into().map_err(|_| CryptoError::WrongSaltLength)) + .transpose()?, + }) + } { + err @ Err(CryptoError::WrongSaltLength) => { + // TODO: Use Result::inspect_err when stable + debug!( + "Bad hmac-secret output length: {} bytes (expected exactly 32 or 64)", + output_secrets.len() + ); + err + } + other => other, + } + } +} + impl Serialize for HmacSecretResponse { fn serialize(&self, serializer: S) -> Result where @@ -1512,4 +1557,171 @@ pub mod test { Ok(()) } + + mod hmac_secret { + use std::convert::TryFrom; + + use crate::{ + crypto::{ + COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthProtocol, + SharedSecret, + }, + ctap2::{attestation::HmacSecretResponse, commands::CommandError}, + AuthenticatorInfo, + }; + + fn make_test_secret(pin_protocol: u64) -> Result { + let fake_unused_key = COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![], + y: vec![], + }), + }; + + let pin_protocol = PinUvAuthProtocol::try_from(&AuthenticatorInfo { + pin_protocols: Some(vec![pin_protocol]), + ..Default::default() + })?; + + let key = { + let aes_key = 0..32; + let hmac_key = 32..64; + match pin_protocol.id() { + 1 => aes_key.collect(), + 2 => hmac_key.chain(aes_key).collect(), + _ => unimplemented!(), + } + }; + + Ok(SharedSecret::new_test( + pin_protocol, + key, + fake_unused_key.clone(), + fake_unused_key, + )) + } + + #[test] + fn decrypt_confirmed_returns_none() -> Result<(), CommandError> { + let shared_secret = make_test_secret(2)?; + for flag in [true, false] { + let resp = HmacSecretResponse::Confirmed(flag); + let hmac_output = resp.decrypt_secrets(&shared_secret); + assert_eq!(hmac_output, None, "Failed for confirmed flag: {:?}", flag); + } + Ok(()) + } + + #[cfg(not(feature = "crypto_dummy"))] + mod requires_crypto { + use super::*; + + use crate::{ + crypto::CryptoError, + ctap2::{attestation::HmacSecretResponse, commands::CommandError}, + }; + + const PIN_PROTOCOL_2_IV: [u8; 16] = [0; 16]; // PIN protocol 1 uses a hard-coded all-zero IV + + /// Generated using AES key 0..32 and ciphertext 0..64: + /// ``` + /// #!/usr/bin/env python3 + /// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + /// + /// key = bytes(range(32)) + /// iv = bytes([0] * 16) + /// ciphertext = bytes(range(64)) + /// + /// cipher = Cipher(algorithms.AES256(key), modes.CBC(iv)) + /// decryptor = cipher.decryptor() + /// outputs = list(decryptor.update(ciphertext) + decryptor.finalize()) + /// EXPECTED_OUTPUT1 = outputs[0:32] + /// EXPECTED_OUTPUT2 = outputs[32:64] + /// print(EXPECTED_OUTPUT1) + /// print(EXPECTED_OUTPUT2) + /// ``` + /// Note: Using WebCrypto to generate these is impractical since they MUST NOT be padded, but WebCrypto inserts PKCS#7 padding. + const EXPECTED_OUTPUT1: [u8; 32] = [ + 145, 61, 188, 229, 73, 58, 253, 192, 87, 114, 133, 138, 173, 74, 68, 50, 105, 3, + 44, 7, 205, 92, 54, 139, 137, 207, 7, 105, 89, 85, 211, 130, + ]; + + /// See [EXPECTED_OUTPUT1] for generation instructions + const EXPECTED_OUTPUT2: Option<[u8; 32]> = Some([ + 155, 19, 88, 255, 192, 226, 50, 42, 243, 22, 42, 12, 146, 77, 108, 29, 71, 72, 149, + 153, 183, 65, 182, 149, 71, 202, 57, 123, 239, 79, 94, 230, + ]); + + #[test] + fn decrypt_one_secret_pin_protocol_1() -> Result<(), CommandError> { + const CT_LEN: u8 = 32; + let shared_secret = make_test_secret(1)?; + let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, None, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_two_secrets_pin_protocol_1() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 2; + let shared_secret = make_test_secret(1)?; + let resp = HmacSecretResponse::Secret((0..CT_LEN).collect()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_one_secret_pin_protocol_2() -> Result<(), CommandError> { + const CT_LEN: u8 = 32; + let shared_secret = make_test_secret(2)?; + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, None, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_two_secrets_pin_protocol_2() -> Result<(), CommandError> { + const CT_LEN: u8 = 32 * 2; + let shared_secret = make_test_secret(2)?; + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..CT_LEN).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap()?; + assert_eq!(hmac_output.output1, EXPECTED_OUTPUT1, "Incorrect output1"); + assert_eq!(hmac_output.output2, EXPECTED_OUTPUT2, "Incorrect output2"); + Ok(()) + } + + #[test] + fn decrypt_wrong_length_pin_protocol_2() -> Result<(), CommandError> { + // hmac-secret output can only be multiples of 32 bytes since it operates on whole AES cipher blocks + let shared_secret = make_test_secret(2)?; + { + // Empty cleartext + let resp = HmacSecretResponse::Secret(PIN_PROTOCOL_2_IV.to_vec()); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); + assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + } + { + // Too long cleartext + let resp = HmacSecretResponse::Secret( + PIN_PROTOCOL_2_IV.iter().copied().chain(0..96).collect(), + ); + let hmac_output = resp.decrypt_secrets(&shared_secret).unwrap(); + assert_eq!(hmac_output, Err(CryptoError::WrongSaltLength)); + } + Ok(()) + } + } + } } diff --git a/src/ctap2/commands/client_pin.rs b/src/ctap2/commands/client_pin.rs index 15107b8c..3569b7ec 100644 --- a/src/ctap2/commands/client_pin.rs +++ b/src/ctap2/commands/client_pin.rs @@ -594,14 +594,7 @@ impl Pin { } pub fn for_pin_token(&self) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(self.0.as_bytes()); - - let mut output = [0u8; 16]; - let len = output.len(); - output.copy_from_slice(&hasher.finalize().as_slice()[..len]); - - output.to_vec() + Sha256::digest(self.as_bytes())[..16].into() } pub fn padded(&self) -> Vec { diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 0e9ef0a2..890febae 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -1,21 +1,22 @@ use super::get_info::AuthenticatorInfo; use super::{ - Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, - StatusCode, + Command, CommandError, CtapResponse, PinUvAuthCommand, PinUvAuthResult, RequestCtap1, + RequestCtap2, Retryable, StatusCode, }; use crate::consts::{ PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN, U2F_REQUEST_USER_PRESENCE, }; use crate::crypto::{COSEKey, CryptoError, PinUvAuthParam, PinUvAuthToken, SharedSecret}; -use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; +use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags, HmacSecretResponse}; use crate::ctap2::client_data::ClientDataHash; use crate::ctap2::commands::get_next_assertion::GetNextAssertion; use crate::ctap2::commands::make_credentials::UserVerification; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, - AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, - RelyingParty, RpIdHash, UserVerificationRequirement, + AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, + PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, + UserVerificationRequirement, }; use crate::ctap2::utils::{read_be_u32, read_byte}; use crate::errors::AuthenticatorError; @@ -29,6 +30,7 @@ use serde::{ }; use serde_bytes::ByteBuf; use serde_cbor::{de::from_slice, ser, Value}; +use std::convert::TryFrom; use std::fmt; use std::io::Cursor; @@ -66,18 +68,97 @@ impl UserVerification for GetAssertionOptions { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct CalculatedHmacSecretExtension { pub public_key: COSEKey, pub salt_enc: Vec, pub salt_auth: Vec, } -#[derive(Debug, Clone, Default)] +/// Wrapper type recording whether the hmac-secret input originally came from the hmacGetSecret or the prf client extension input. +#[derive(Debug, Clone, PartialEq)] +pub enum HmacGetSecretOrPrf { + /// hmac-secret inputs set by the hmacGetSecret client extension input. + HmacGetSecret(HmacSecretExtension), + /// hmac-secret input is to be calculated from PRF inputs, but we haven't yet identified which eval or evalByCredential entry to use. + PrfUninitialized(AuthenticationExtensionsPRFInputs), + /// prf client input with no eval or matching evalByCredential entry. + PrfUnmatched, + /// hmac-secret inputs set by the prf client extension input. + Prf(HmacSecretExtension), +} + +impl HmacGetSecretOrPrf { + fn skip_serializing(value: &Option) -> bool { + matches!(value, None | Some(Self::PrfUnmatched)) + } + + /// Calculate the appropriate hmac-secret or PRF salt inputs from the given inputs. + /// + /// - If this is a `HmacGetSecret` instance, + /// this returns a new `HmacGetSecret` instance with `calculated_hmac` set, paired with [None]. + /// - If this is a `PrfUninitialized` instance, + /// this attempts to select a PRF input to calculate salts from. + /// If an input is found, this returns a `Prf` instance with `calculated_hmac` set. + /// If the selected input came from `eval_by_credential`, + /// then this is paired with a [Some] referencing the matching element of `allow_credentials`. + /// If the selected input was `eval`, then this is paired with [None]. + /// If no input is found, this returns `PrfUnmatched` and [None]. + /// - If this is a `Prf` or `PrfUnmatched` instance, this panics. + /// + /// If the [Option] return value is [Some], the caller SHOULD set `allowCredentials` + /// to contain only that [PublicKeyCredentialDescriptor] value. + /// + /// # Panics + /// If this is a `Prf` or `PrfUnmatched` instance. + pub fn calculate<'allow_cred>( + self, + secret: &SharedSecret, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + puat: Option<&PinUvAuthToken>, + ) -> Result<(Self, Option<&'allow_cred PublicKeyCredentialDescriptor>), CryptoError> { + Ok(match self { + Self::HmacGetSecret(mut extension) => { + extension.calculate(secret, puat)?; + (Self::HmacGetSecret(extension), None) + } + + Self::PrfUninitialized(prf) => match prf.calculate(secret, allow_credentials, puat)? { + Some((hmac_secret, selected_credential)) => { + (Self::Prf(hmac_secret), selected_credential) + } + None => (Self::PrfUnmatched, None), + }, + + Self::Prf(_) | Self::PrfUnmatched => { + unreachable!("hmac-secret inputs from PRF already initialized") + } + }) + } +} + +impl Serialize for HmacGetSecretOrPrf { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + match self { + Self::HmacGetSecret(ext) => ext.serialize(s), + Self::PrfUninitialized(_) => Err(serde::ser::Error::custom( + "PrfUninitialized must be replaced with Prf or PrfUnmatched before serializing", + )), + Self::PrfUnmatched => unreachable!("PrfUnmatched serialization should be skipped"), + Self::Prf(ext) => ext.serialize(s), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq)] pub struct HmacSecretExtension { pub salt1: Vec, pub salt2: Option>, calculated_hmac: Option, + pin_protocol: Option, } impl HmacSecretExtension { @@ -86,6 +167,7 @@ impl HmacSecretExtension { salt1, salt2, calculated_hmac: None, + pin_protocol: None, } } @@ -94,27 +176,35 @@ impl HmacSecretExtension { salt1: Vec, salt2: Option>, calculated_hmac: CalculatedHmacSecretExtension, + pin_protocol: Option, ) -> Self { HmacSecretExtension { salt1, salt2, calculated_hmac: Some(calculated_hmac), + pin_protocol, } } - pub fn calculate(&mut self, secret: &SharedSecret) -> Result<(), AuthenticatorError> { - if self.salt1.len() < 32 { - return Err(CryptoError::WrongSaltLength.into()); - } - let salt_enc = match &self.salt2 { - Some(salt2) => { - if salt2.len() < 32 { - return Err(CryptoError::WrongSaltLength.into()); - } - let salts = [&self.salt1[..32], &salt2[..32]].concat(); // salt1 || salt2 - secret.encrypt(&salts) + /// Calculate inputs for the `hmac-secret` extension. + /// See "authenticatorGetAssertion additional behaviors" + /// in https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-hmac-secret-extension + pub fn calculate( + &mut self, + secret: &SharedSecret, + puat: Option<&PinUvAuthToken>, + ) -> Result<(), CryptoError> { + let salt_enc = match ( + <[u8; 32]>::try_from(self.salt1.as_slice()), + self.salt2.as_deref().map(<[u8; 32]>::try_from), + ) { + (Ok(salt1), None) => secret.encrypt(&salt1), + (Ok(salt1), Some(Ok(salt2))) => secret.encrypt(&[salt1, salt2].concat()), + (Err(_), _) | (_, Some(Err(_))) => { + debug!("Invalid hmac-secret salt length(s): salt1: {}, salt2: {:?} (expected 32 and 32|None)", + self.salt1.len(), self.salt2.as_ref().map(Vec::len)); + Err(CryptoError::WrongSaltLength) } - None => secret.encrypt(&self.salt1[..32]), }?; let salt_auth = secret.authenticate(&salt_enc)?; let public_key = secret.client_input().clone(); @@ -124,6 +214,11 @@ impl HmacSecretExtension { salt_auth, }); + // CTAP2.1 platforms MUST include this parameter if the value of pinUvAuthProtocol is not 1. + self.pin_protocol = puat + .map(|puat| puat.pin_protocol.id()) + .filter(|id| *id != 1); + Ok(()) } } @@ -134,11 +229,12 @@ impl Serialize for HmacSecretExtension { S: Serializer, { if let Some(calc) = &self.calculated_hmac { - serialize_map! { + serialize_map_optional! { serializer, - &1 => &calc.public_key, - &2 => serde_bytes::Bytes::new(&calc.salt_enc), - &3 => serde_bytes::Bytes::new(&calc.salt_auth), + &1 => Some(&calc.public_key), + &2 => Some(serde_bytes::Bytes::new(&calc.salt_enc)), + &3 => Some(serde_bytes::Bytes::new(&calc.salt_auth)), + &4 => &self.pin_protocol, } } else { Err(SerError::custom( @@ -152,15 +248,29 @@ impl Serialize for HmacSecretExtension { pub struct GetAssertionExtensions { #[serde(skip_serializing)] pub app_id: Option, - #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] - pub hmac_secret: Option, + #[serde( + rename = "hmac-secret", + skip_serializing_if = "HmacGetSecretOrPrf::skip_serializing" + )] + pub hmac_secret: Option, } impl From for GetAssertionExtensions { fn from(input: AuthenticationExtensionsClientInputs) -> Self { + let prf = input.prf; Self { app_id: input.app_id, - ..Default::default() + hmac_secret: input + .hmac_get_secret + .map(|hmac_secret| { + HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + hmac_secret.salt1.into(), + hmac_secret.salt2.map(|salt2| salt2.into()), + )) + }) + .or_else( + || prf.map(HmacGetSecretOrPrf::PrfUninitialized), // Cannot calculate hmac-secret inputs here because we don't yet know which eval or evalByCredential entry to use + ), } } } @@ -206,6 +316,54 @@ impl GetAssertion { } } + pub fn process_hmac_secret_and_prf_extension( + mut self, + shared_secret: Option<(&SharedSecret, &PinUvAuthResult)>, + ) -> Result { + let (new_hmac_secret, new_allow_list) = self + .extensions + .hmac_secret + .take() + .and_then(|hmac_get_secret_or_prf| { + if let Some((secret, pin_uv_auth_result)) = shared_secret { + Some(hmac_get_secret_or_prf.calculate( + secret, + &self.allow_list, + pin_uv_auth_result.get_pin_uv_auth_token().as_ref(), + )) + } else { + debug!( + "Shared secret not available - will not send hmac-secret extension input: {:?}", + hmac_get_secret_or_prf + ); + match hmac_get_secret_or_prf { + HmacGetSecretOrPrf::HmacGetSecret(_) => None, + HmacGetSecretOrPrf::PrfUninitialized(_) + | HmacGetSecretOrPrf::PrfUnmatched + | HmacGetSecretOrPrf::Prf(_) => { + Some(Ok((HmacGetSecretOrPrf::PrfUnmatched, None))) + } + } + } + }) + .transpose() + .map_err(|err| match err { + CryptoError::WrongSaltLength => AuthenticatorError::InvalidRelyingPartyInput, + e => e.into(), + })? + .map(|(nhs, nal)| (Some(nhs), nal)) + .unwrap_or((None, None)); + + (self.extensions.hmac_secret, self.allow_list) = ( + new_hmac_secret, + new_allow_list + .map(|selected_credential| vec![selected_credential.clone()]) + .unwrap_or(self.allow_list), + ); + + Ok(self) + } + pub fn finalize_result(&self, dev: &Dev, result: &mut GetAssertionResult) { result.attachment = match dev.get_authenticator_info() { Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform, @@ -219,6 +377,61 @@ impl GetAssertion { result.extensions.app_id = Some(result.assertion.auth_data.rp_id_hash == RelyingParty::from(app_id).hash()); } + + // 2. prf + // If the prf extension was requested and hmac-secret returned secrets, + // we need to decrypt and output them as prf client outputs. + match self.extensions.hmac_secret { + Some(HmacGetSecretOrPrf::HmacGetSecret(_)) => { + result.extensions.hmac_get_secret = + if let Some(hmac_response @ HmacSecretResponse::Secret(_)) = + &result.assertion.auth_data.extensions.hmac_secret + { + dev.get_shared_secret() + .and_then(|shared_secret| hmac_response.decrypt_secrets(shared_secret)) + .and_then(|result| match result { + Ok(ok) => Some(ok), + Err(err) => { + debug!("Failed to decrypt hmac-secret response: {:?}", err); + None + } + }) + } else { + None + }; + } + Some(HmacGetSecretOrPrf::PrfUninitialized(_)) => { + unreachable!("Reached GetAssertion.finalize_result without replacing PrfUninitialized instance with Prf") + } + Some(HmacGetSecretOrPrf::PrfUnmatched) => { + result.extensions.prf = Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: None, + }); + } + Some(HmacGetSecretOrPrf::Prf(_)) => { + result.extensions.prf = Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: if let Some(hmac_response @ HmacSecretResponse::Secret(_)) = + &result.assertion.auth_data.extensions.hmac_secret + { + dev.get_shared_secret() + .and_then(|shared_secret| hmac_response.decrypt_secrets(shared_secret)) + .and_then(|result| match result { + Ok(ok) => Some(ok), + Err(err) => { + debug!("Failed to decrypt hmac-secret response: {:?}", err); + None + } + }) + .map(|outputs| outputs.into()) + } else { + None + }, + }); + } + None => {} + } } } @@ -606,9 +819,13 @@ pub mod test { }; use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthParam}; use crate::ctap2::attestation::{AAGuid, AuthenticatorData, AuthenticatorDataFlags}; - use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::client_data::{ + Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType, + }; + use crate::ctap2::commands::client_pin::PinUvAuthTokenPermission; use crate::ctap2::commands::get_assertion::{ - CalculatedHmacSecretExtension, GetAssertionExtensions, HmacSecretExtension, + CalculatedHmacSecretExtension, GetAssertionExtensions, HmacGetSecretOrPrf, + HmacSecretExtension, }; use crate::ctap2::commands::get_info::tests::AAGUID_RAW; use crate::ctap2::commands::get_info::{ @@ -619,8 +836,8 @@ pub mod test { do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, }; use crate::ctap2::server::{ - AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, - RelyingParty, RpIdHash, Transport, + AuthenticationExtensionsPRFInputs, AuthenticatorAttachment, PublicKeyCredentialDescriptor, + PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, Transport, }; use crate::transport::device_selector::Device; use crate::transport::hid::HIDDevice; @@ -812,21 +1029,24 @@ pub mod test { }], extensions: GetAssertionExtensions { app_id: Some("https://example.com".to_string()), - hmac_secret: Some(HmacSecretExtension::new_test( - vec![32; 32], - None, - CalculatedHmacSecretExtension { - public_key: COSEKey { - alg: COSEAlgorithm::ECDH_ES_HKDF256, - key: COSEKeyType::EC2(COSEEC2Key { - curve: Curve::SECP256R1, - x: vec![], - y: vec![], - }), + hmac_secret: Some(HmacGetSecretOrPrf::HmacGetSecret( + HmacSecretExtension::new_test( + vec![32; 32], + None, + CalculatedHmacSecretExtension { + public_key: COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![], + y: vec![], + }), + }, + salt_enc: vec![7; 32], + salt_auth: vec![8; 16], }, - salt_enc: vec![7; 32], - salt_auth: vec![8; 16], - }, + None, + ), )), }, options: GetAssertionOptions { @@ -858,6 +1078,120 @@ pub mod test { ); } + #[test] + fn test_serialize_get_assertion_ctap2_pin_protocol_2() { + let assertion = GetAssertion { + client_data_hash: ClientDataHash([0; 32]), + rp: RelyingParty::from("example.com"), + allow_list: vec![], + extensions: GetAssertionExtensions { + app_id: None, + hmac_secret: Some(HmacGetSecretOrPrf::HmacGetSecret( + HmacSecretExtension::new_test( + vec![32; 32], + None, + CalculatedHmacSecretExtension { + public_key: COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![], + y: vec![], + }), + }, + salt_enc: vec![7; 32], + salt_auth: vec![8; 16], + }, + Some(2), + ), + )), + }, + options: GetAssertionOptions { + user_presence: None, + user_verification: None, + }, + pin_uv_auth_param: Some(PinUvAuthParam::create_test( + 2, + vec![9; 4], + PinUvAuthTokenPermission::GetAssertion, + )), + }; + let req_serialized = assertion + .wire_format() + .expect("Failed to serialize GetAssertion request"); + assert_eq!( + req_serialized, + [ + // Value copied from test failure output as regression test snapshot + 165, 1, 107, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 2, 88, 32, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4, 161, 107, 104, 109, 97, 99, 45, 115, 101, 99, 114, 101, 116, 164, 1, 165, 1, + 2, 3, 56, 24, 32, 1, 33, 64, 34, 64, 2, 88, 32, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 3, 80, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 4, 2, 6, 68, 9, 9, 9, 9, 7, 2 + ] + ); + } + + #[test] + #[should_panic( + expected = "PrfUninitialized must be replaced with Prf or PrfUnmatched before serializing" + )] + fn test_serialize_prf_uninitialized() { + let assertion = GetAssertion { + client_data_hash: ClientDataHash([0; 32]), + rp: RelyingParty::from("example.com"), + allow_list: vec![], + extensions: GetAssertionExtensions { + app_id: None, + hmac_secret: Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: None, + }, + )), + }, + options: GetAssertionOptions { + user_presence: None, + user_verification: None, + }, + pin_uv_auth_param: None, + }; + assertion + .wire_format() + .expect("Failed to serialize GetAssertion request"); + } + + #[test] + fn test_serialize_prf_unmatched() { + let assertion = GetAssertion { + client_data_hash: ClientDataHash([0; 32]), + rp: RelyingParty::from("example.com"), + allow_list: vec![], + extensions: GetAssertionExtensions { + app_id: None, + hmac_secret: Some(HmacGetSecretOrPrf::PrfUnmatched), + }, + options: GetAssertionOptions { + user_presence: None, + user_verification: None, + }, + pin_uv_auth_param: None, + }; + let req_serialized = assertion + .wire_format() + .expect("Failed to serialize GetAssertion request"); + assert_eq!( + req_serialized, + [ + // Value copied from test failure output as regression test snapshot + 163, 1, 107, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 2, 88, 32, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4, 160 + ] + ); + } + fn fill_device_ctap1(device: &mut Device, cid: [u8; 4], flags: u8, answer_status: [u8; 2]) { // ctap2 request let mut msg = cid.to_vec(); @@ -1557,4 +1891,1071 @@ pub mod test { 0x05, // unsigned(5) 0x01, // unsigned(1) ]; + + mod hmac_secret { + use std::convert::TryFrom; + + use crate::{ + crypto::{ + COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, PinUvAuthProtocol, + SharedSecret, + }, + ctap2::{ + attestation::{ + AuthenticatorData, AuthenticatorDataFlags, Extension, HmacSecretResponse, + }, + client_data::ClientDataHash, + commands::{ + get_assertion::{ + CalculatedHmacSecretExtension, GetAssertion, GetAssertionExtensions, + HmacGetSecretOrPrf, HmacSecretExtension, + }, + CommandError, + }, + server::{ + AuthenticationExtensionsClientOutputs, AuthenticationExtensionsPRFOutputs, + AuthenticatorAttachment, RelyingParty, RpIdHash, + }, + }, + transport::platform::device::Device, + Assertion, AuthenticatorInfo, FidoDevice, GetAssertionResult, + }; + + fn make_test_secret_without_puat( + pin_protocol: u64, + ) -> Result<(SharedSecret, COSEKey), CommandError> { + let fake_client_key = COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![1], + y: vec![2], + }), + }; + let fake_peer_key = COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![3], + y: vec![4], + }), + }; + + let pin_protocol = PinUvAuthProtocol::try_from(&AuthenticatorInfo { + pin_protocols: Some(vec![pin_protocol]), + ..Default::default() + })?; + + let key = { + let aes_key = 0..32; + let hmac_key = 32..64; + match pin_protocol.id() { + 1 => aes_key.collect(), + 2 => hmac_key.chain(aes_key).collect(), + _ => unimplemented!(), + } + }; + + let shared_secret = + SharedSecret::new_test(pin_protocol, key, fake_client_key.clone(), fake_peer_key); + + Ok((shared_secret, fake_client_key)) + } + + #[cfg(not(feature = "crypto_dummy"))] + mod requires_crypto { + use sha2::{Digest, Sha256}; + + use super::*; + use crate::{ + crypto::{CryptoError, PinUvAuthToken}, + ctap2::{ + client_data::ClientDataHash, + commands::{ + client_pin::PinUvAuthTokenPermission, + get_assertion::{ + CalculatedHmacSecretExtension, GetAssertion, GetAssertionExtensions, + HmacGetSecretOrPrf, HmacSecretExtension, + }, + PinUvAuthResult, + }, + server::{ + AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFValues, + PublicKeyCredentialDescriptor, RelyingParty, + }, + }, + errors::AuthenticatorError, + }; + + fn make_test_secret( + pin_protocol: u64, + ) -> Result<(SharedSecret, COSEKey, PinUvAuthToken), CommandError> { + let (shared_secret, fake_client_key) = make_test_secret_without_puat(pin_protocol)?; + let puat = shared_secret.decrypt_pin_token( + PinUvAuthTokenPermission::empty(), + &shared_secret.encrypt(&[0x03; 32])?, + )?; + + Ok((shared_secret, fake_client_key, puat)) + } + + fn get_assertion_process_hmac_secret( + secret_available: bool, + allow_list: Vec, + hmac_secret: Option, + ) -> Result { + let (shared_secret, _, puat) = make_test_secret(1)?; + GetAssertion::new( + ClientDataHash([0x01; 32]), + RelyingParty::from("example.com"), + allow_list, + Default::default(), + GetAssertionExtensions { + hmac_secret, + ..Default::default() + }, + ) + .process_hmac_secret_and_prf_extension( + secret_available + .then_some((&shared_secret, &PinUvAuthResult::SuccessGetPinToken(puat))), + ) + } + + #[test] + fn get_assertion_hmac_secret_and_prf_absent_uses_no_input() { + let get_assertion = get_assertion_process_hmac_secret(true, vec![], None).unwrap(); + assert_matches!(get_assertion.extensions.hmac_secret, None); + } + + #[test] + fn get_assertion_prf_no_input_uses_unmatched_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: None, + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::PrfUnmatched) + ); + } + + #[test] + fn get_assertion_prf_no_secret_uses_unmatched_input() { + let get_assertion = get_assertion_process_hmac_secret( + false, + vec![], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![1, 2, 3, 4], + second: None, + }), + eval_by_credential: None, + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::PrfUnmatched) + ); + } + + #[test] + fn get_assertion_hmac_get_secret_uses_hmac_get_secret_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 32], + None, + ))), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension { + calculated_hmac: Some(_), + .. + })) + ); + } + + #[test] + fn get_assertion_hmac_get_secret_bad_length_returns_invalid_input_error() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 31], + None, + ))), + ); + assert_matches!( + get_assertion, + Err(AuthenticatorError::InvalidRelyingPartyInput) + ); + } + + #[test] + fn get_assertion_prf_eval_uses_eval_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![1, 2, 3, 4], + second: None, + }), + eval_by_credential: None, + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([1, 2, 3, 4].iter()) + .finalize() + .to_vec() + ); + } + + #[test] + fn get_assertion_prf_eval_by_credential_unmatched_uses_unmatched_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![5, 6, 7, 8], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::PrfUnmatched) + ); + } + + #[test] + fn get_assertion_prf_eval_by_credential_matched_uses_eval_by_credential_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([9, 10, 11, 12].iter()) + .finalize() + .to_vec() + ); + } + + #[test] + fn get_assertion_prf_eval_and_eval_by_credential_unmatched_uses_eval_input() { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![13, 14, 15, 16], + second: None, + }), + eval_by_credential: Some( + [( + vec![5, 6, 7, 8], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([13, 14, 15, 16].iter()) + .finalize() + .to_vec() + ); + } + + #[test] + fn get_assertion_prf_eval_and_eval_by_credential_matched_uses_eval_by_credential_input() + { + let get_assertion = get_assertion_process_hmac_secret( + true, + vec![PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }], + Some(HmacGetSecretOrPrf::PrfUninitialized( + AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![13, 14, 15, 16], + second: None, + }), + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![9, 10, 11, 12], + second: None, + }, + )] + .into(), + ), + }, + )), + ) + .unwrap(); + assert_matches!( + get_assertion.extensions.hmac_secret, + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1, + .. + })) if salt1 == Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update([9, 10, 11, 12].iter()) + .finalize() + .to_vec() + ); + } + + #[test] + fn calculate_hmac_get_secret_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key, puat) = make_test_secret(1)?; + let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 32], + Some(vec![0x02; 32]), + )); + let (extension, selected_cred) = + extension.calculate(&shared_secret, &[], Some(&puat))?; + + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension { + salt1: vec![0x01; 32], + salt2: Some(vec![0x02; 32]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![ + 117, 226, 8, 41, 23, 33, 18, 187, 242, 160, 77, 61, 43, 18, 67, 61, + 170, 97, 245, 245, 17, 42, 232, 186, 255, 190, 82, 1, 81, 152, 175, + 39, 113, 130, 62, 169, 215, 202, 143, 80, 116, 195, 117, 22, 39, + 64, 79, 110, 216, 117, 7, 144, 87, 73, 144, 75, 255, 173, 169, 201, + 122, 160, 48, 157 + ], + salt_auth: vec![ + 36, 74, 81, 146, 64, 28, 73, 44, 75, 111, 14, 79, 173, 146, 212, + 227 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_hmac_get_secret_wrong_length_salt1() -> Result<(), AuthenticatorError> { + let (shared_secret, _, puat) = make_test_secret(1)?; + for len in [0, 1, 31, 33, 64] { + let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; len], + None, + )); + let result = extension.calculate(&shared_secret, &[], Some(&puat)); + assert_eq!( + result, + Err(CryptoError::WrongSaltLength), + "At salt1 length: {}", + len, + ); + } + Ok(()) + } + + #[test] + fn calculate_hmac_get_secret_wrong_length_salt2() -> Result<(), AuthenticatorError> { + let (shared_secret, _, puat) = make_test_secret(1)?; + for len in [0, 1, 31, 33, 64] { + let extension = HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![0x01; 32], + Some(vec![0x02; len]), + )); + let result = extension.calculate(&shared_secret, &[], Some(&puat)); + assert_eq!( + result, + Err(CryptoError::WrongSaltLength), + "At salt2 length: {}", + len, + ); + } + Ok(()) + } + + #[test] + fn calculate_prf_eval_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key, puat) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: None, + }); + let (extension, selected_cred) = + extension.calculate(&shared_secret, &[], Some(&puat))?; + + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(1)]))) + salt1: vec![ + 5, 240, 179, 178, 62, 126, 205, 172, 176, 105, 211, 13, 86, 210, 48, + 210, 225, 219, 234, 248, 16, 182, 52, 219, 92, 135, 97, 119, 107, 245, + 30, 226 + ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(2)]))) + salt2: Some(vec![ + 96, 107, 65, 234, 77, 176, 251, 24, 193, 188, 98, 23, 59, 240, 212, 6, + 104, 176, 40, 242, 104, 190, 32, 124, 226, 244, 19, 160, 8, 105, 253, + 106 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) + salt_enc: vec![ + 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, + 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, + 180, 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, + 187, 197, 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, + 60, 109, 33, 112, 175 + ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) + salt_auth: vec![ + 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, + 174 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_eval_by_cred_fallback_to_eval_pin_protocol_1( + ) -> Result<(), AuthenticatorError> { + let (shared_secret, client_key, puat) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + )] + .iter() + .cloned() + .collect(), + ), + }); + let allow_list = [PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; + + assert_eq!(selected_cred, None); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(1)]))) + salt1: vec![ + 5, 240, 179, 178, 62, 126, 205, 172, 176, 105, 211, 13, 86, 210, 48, + 210, 225, 219, 234, 248, 16, 182, 52, 219, 92, 135, 97, 119, 107, 245, + 30, 226 + ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(2)]))) + salt2: Some(vec![ + 96, 107, 65, 234, 77, 176, 251, 24, 193, 188, 98, 23, 59, 240, 212, 6, + 104, 176, 40, 242, 104, 190, 32, 124, 226, 244, 19, 160, 8, 105, 253, + 106 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) + salt_enc: vec![ + 23, 99, 220, 93, 59, 246, 109, 157, 247, 33, 138, 91, 142, 40, 203, + 234, 96, 212, 26, 15, 56, 160, 191, 142, 138, 106, 2, 207, 219, + 180, 39, 31, 155, 232, 119, 179, 0, 65, 9, 37, 184, 194, 135, 173, + 187, 197, 51, 38, 68, 57, 197, 68, 249, 41, 143, 197, 46, 53, 72, + 60, 109, 33, 112, 175 + ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) + salt_auth: vec![ + 27, 222, 224, 22, 170, 39, 171, 5, 98, 207, 176, 58, 23, 108, 223, + 174 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key, puat) = make_test_secret(1)?; + let cred_id = PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: Some(AuthenticationExtensionsPRFValues { + first: vec![0x01; 8], + second: Some(vec![0x02; 8]), + }), + eval_by_credential: Some( + [ + ( + vec![9, 10, 11, 12], + AuthenticationExtensionsPRFValues { + first: vec![0x06; 8], + second: Some(vec![0x07; 8]), + }, + ), + ( + cred_id.id.clone(), + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + ), + ] + .iter() + .cloned() + .collect(), + ), + }); + let allow_list = [ + PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }, + cred_id, + PublicKeyCredentialDescriptor { + id: vec![9, 10, 11, 12], + transports: vec![], + }, + ]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; + + assert_eq!(selected_cred, Some(&allow_list[1])); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(4)]))) + salt1: vec![ + 141, 49, 215, 240, 110, 193, 84, 27, 113, 153, 129, 108, 71, 59, 98, 5, + 209, 45, 190, 142, 47, 4, 72, 78, 217, 85, 99, 243, 192, 217, 232, 88 + ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(5)]))) + salt2: Some(vec![ + 156, 88, 127, 151, 204, 90, 145, 200, 207, 201, 106, 124, 19, 60, 29, + 115, 145, 197, 27, 148, 117, 72, 18, 4, 78, 187, 161, 122, 144, 245, + 67, 1 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) + salt_enc: vec![ + 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, + 47, 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, + 209, 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, + 130, 69, 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, + 167, 210, 241, 238, 237 + ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) + salt_auth: vec![ + 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, + 232 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_only_eval_by_cred_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, client_key, puat) = make_test_secret(1)?; + let cred_id = PublicKeyCredentialDescriptor { + id: vec![1, 2, 3, 4], + transports: vec![], + }; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [ + ( + vec![9, 10, 11, 12], + AuthenticationExtensionsPRFValues { + first: vec![0x06; 8], + second: Some(vec![0x07; 8]), + }, + ), + ( + cred_id.id.clone(), + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + ), + ] + .iter() + .cloned() + .collect(), + ), + }); + let allow_list = [ + PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }, + cred_id, + PublicKeyCredentialDescriptor { + id: vec![9, 10, 11, 12], + transports: vec![], + }, + ]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; + + assert_eq!(selected_cred, Some(&allow_list[1])); + assert_eq!( + extension, + HmacGetSecretOrPrf::Prf(HmacSecretExtension { + // JS: salt1 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(4)]))) + salt1: vec![ + 141, 49, 215, 240, 110, 193, 84, 27, 113, 153, 129, 108, 71, 59, 98, 5, + 209, 45, 190, 142, 47, 4, 72, 78, 217, 85, 99, 243, 192, 217, 232, 88 + ], + // JS: salt2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([...new TextEncoder().encode("WebAuthn PRF"), 0, ...new Uint8Array(8).fill(5)]))) + salt2: Some(vec![ + 156, 88, 127, 151, 204, 90, 145, 200, 207, 201, 106, 124, 19, 60, 29, + 115, 145, 197, 27, 148, 117, 72, 18, 4, 78, 187, 161, 122, 144, 245, + 67, 1 + ]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + // JS: aesKey = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "AES-CBC" }, false, ["encrypt"]) + // JS: salt_enc = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, aesKey, new Uint8Array([...salt1, ...salt2]))).slice(0, 64) + // (Need to strip trailing padding block inserted by WebCrypto) + salt_enc: vec![ + 191, 228, 209, 183, 255, 132, 169, 88, 82, 9, 102, 239, 99, 201, + 47, 15, 174, 24, 191, 30, 80, 230, 67, 237, 178, 112, 105, 243, 53, + 209, 25, 189, 32, 51, 75, 255, 176, 160, 82, 113, 250, 141, 83, + 130, 69, 156, 230, 91, 95, 17, 149, 11, 81, 40, 23, 42, 24, 33, 25, + 167, 210, 241, 238, 237 + ], + // JS: hmacKeyP1 = await crypto.subtle.importKey("raw", new Uint8Array(32).map((b, i) => i), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + // JS: salt_auth = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKeyP1, salt_enc)).slice(0, 16) + salt_auth: vec![ + 211, 87, 229, 38, 186, 254, 65, 2, 69, 166, 122, 30, 84, 77, 116, + 232 + ], + }), + pin_protocol: None, + }), + ); + + Ok(()) + } + + #[test] + fn calculate_prf_unmatched_pin_protocol_1() -> Result<(), AuthenticatorError> { + let (shared_secret, _, puat) = make_test_secret(1)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + )] + .iter() + .cloned() + .collect(), + ), + }); + let allow_list = [PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; + + assert_eq!(selected_cred, None); + assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); + + Ok(()) + } + + #[test] + fn calculate_prf_unmatched_pin_protocol_2() -> Result<(), AuthenticatorError> { + let (shared_secret, _, puat) = make_test_secret(2)?; + let extension = + HmacGetSecretOrPrf::PrfUninitialized(AuthenticationExtensionsPRFInputs { + eval: None, + eval_by_credential: Some( + [( + vec![1, 2, 3, 4], + AuthenticationExtensionsPRFValues { + first: vec![0x04; 8], + second: Some(vec![0x05; 8]), + }, + )] + .iter() + .cloned() + .collect(), + ), + }); + let allow_list = [PublicKeyCredentialDescriptor { + id: vec![5, 6, 7, 8], + transports: vec![], + }]; + let (extension, selected_cred) = + extension.calculate(&shared_secret, &allow_list, Some(&puat))?; + + assert_eq!(selected_cred, None); + assert_eq!(extension, HmacGetSecretOrPrf::PrfUnmatched); + + Ok(()) + } + + #[test] + fn finalize_result_hmac_get_secret_input_with_secret_output_becomes_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Secret(vec![0x01; ONE_OUTPUT_LEN_PP2])), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: Some(_), + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_prf_input_with_secret_output_becomes_results_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Secret(vec![0x01; ONE_OUTPUT_LEN_PP2])), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: Some(_), + }), + .. + } + ); + } + } + + #[test] + #[should_panic( + expected = "unreachable code: hmac-secret inputs from PRF already initialized" + )] + fn calculate_prf_conflict_1() { + let (shared_secret, _) = make_test_secret_without_puat(2).unwrap(); + let extension = HmacGetSecretOrPrf::PrfUnmatched; + extension.calculate(&shared_secret, &[], None).unwrap(); + } + + #[test] + #[should_panic( + expected = "unreachable code: hmac-secret inputs from PRF already initialized" + )] + fn calculate_prf_conflict_2() { + let (shared_secret, client_key) = make_test_secret_without_puat(2).unwrap(); + let extension = HmacGetSecretOrPrf::Prf(HmacSecretExtension { + salt1: vec![], + salt2: Some(vec![]), + calculated_hmac: Some(CalculatedHmacSecretExtension { + public_key: client_key, + salt_enc: vec![], + salt_auth: vec![], + }), + pin_protocol: None, + }); + extension.calculate(&shared_secret, &[], None).unwrap(); + } + + fn finalize_result_with_hmac_secret_input_and_output( + hmac_secret_input: Option, + hmac_secret_response: Option, + ) -> Result { + let get_assertion = GetAssertion::new( + ClientDataHash([0x01; 32]), + RelyingParty::from("example.com"), + vec![], + Default::default(), + GetAssertionExtensions { + hmac_secret: hmac_secret_input, + ..Default::default() + }, + ); + let mut result = GetAssertionResult { + assertion: Assertion { + credentials: None, + auth_data: AuthenticatorData { + rp_id_hash: RpIdHash([0x01; 32]), + flags: AuthenticatorDataFlags::empty(), + counter: 0, + credential_data: None, + extensions: Extension { + cred_protect: None, + hmac_secret: hmac_secret_response, + min_pin_length: None, + }, + }, + signature: vec![], + user: None, + }, + attachment: AuthenticatorAttachment::Unknown, + extensions: AuthenticationExtensionsClientOutputs::default(), + }; + + let mut dev = Device::new_skipping_serialization("commands/get_assertion") + .expect("Failed to create mock Device"); + let (shared_secret, _) = make_test_secret_without_puat(2)?; + dev.set_shared_secret(shared_secret); + get_assertion.finalize_result(&dev, &mut result); + Ok(result) + } + + /// Encrypted salt output for pin protocol 2: iv || ct + const ONE_OUTPUT_LEN_PP2: usize = 16 + 32; + + #[test] + fn finalize_result_no_input_with_no_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output(None, None) + .expect("Failed to run test"); + assert_matches!(result.extensions.hmac_get_secret, None); + } + + #[test] + fn finalize_result_no_input_with_secret_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + None, + Some(HmacSecretResponse::Secret(vec![0x01; ONE_OUTPUT_LEN_PP2])), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_hmac_get_secret_input_with_no_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![], + None, + ))), + None, + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_hmac_get_secret_input_with_confirmed_output_becomes_no_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::HmacGetSecret(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Confirmed(true)), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: None, + .. + } + ); + } + + #[test] + fn finalize_result_prf_input_with_no_output_becomes_empty_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension::new( + vec![], + None, + ))), + None, + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: None, + }), + .. + } + ); + } + + #[test] + fn finalize_result_prf_input_with_confirmed_output_becomes_empty_client_output() { + let result = finalize_result_with_hmac_secret_input_and_output( + Some(HmacGetSecretOrPrf::Prf(HmacSecretExtension::new( + vec![], + None, + ))), + Some(HmacSecretResponse::Confirmed(true)), + ) + .expect("Failed to run test"); + assert_matches!( + result.extensions, + AuthenticationExtensionsClientOutputs { + hmac_get_secret: None, + prf: Some(AuthenticationExtensionsPRFOutputs { + enabled: None, + results: None, + }), + .. + } + ); + } + } } diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 53ceadb3..c64d4076 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -15,9 +15,9 @@ use crate::ctap2::attestation::{ use crate::ctap2::client_data::ClientDataHash; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, - AuthenticatorAttachment, CredentialProtectionPolicy, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, - UserVerificationRequirement, + AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, CredentialProtectionPolicy, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_byte, serde_parse_err}; use crate::errors::AuthenticatorError; @@ -239,11 +239,29 @@ pub struct MakeCredentialsExtensions { #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")] pub cred_protect: Option, #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] - pub hmac_secret: Option, + pub hmac_secret: Option, #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, } +#[derive(Debug, Clone)] +pub enum HmacCreateSecretOrPrf { + HmacCreateSecret(bool), + Prf, +} + +impl Serialize for HmacCreateSecretOrPrf { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + match self { + Self::HmacCreateSecret(hmac_secret) => s.serialize_bool(*hmac_secret), + Self::Prf => s.serialize_bool(true), + } + } +} + impl MakeCredentialsExtensions { fn has_content(&self) -> bool { self.cred_protect.is_some() || self.hmac_secret.is_some() || self.min_pin_length.is_some() @@ -255,7 +273,13 @@ impl From for MakeCredentialsExtensions { Self { cred_props: input.cred_props, cred_protect: input.credential_protection_policy, - hmac_secret: input.hmac_create_secret, + hmac_secret: match (input.hmac_create_secret, input.prf) { + (None, None) => None, + (_, Some(_)) => Some(HmacCreateSecretOrPrf::Prf), + (Some(hmac_secret), _) => { + Some(HmacCreateSecretOrPrf::HmacCreateSecret(hmac_secret)) + } + }, min_pin_length: input.min_pin_length, } } @@ -342,12 +366,48 @@ impl MakeCredentials { // 2. hmac-secret // The extension returns a flag in the authenticator data which we need to mirror as a // client output. - if self.extensions.hmac_secret == Some(true) { - if let Some(HmacSecretResponse::Confirmed(flag)) = - result.att_obj.auth_data.extensions.hmac_secret - { - result.extensions.hmac_create_secret = Some(flag); + // 3. prf + // hmac-secret returns a flag in the authenticator data + // which we need to mirror as a PRF "enabled" client output. + // If a future version of hmac-secret permits calculating secrets in makeCredential, + // we also need to decrypt and output them as client outputs. + match self.extensions.hmac_secret { + Some(HmacCreateSecretOrPrf::HmacCreateSecret(true)) => { + result.extensions.hmac_create_secret = + Some(match result.att_obj.auth_data.extensions.hmac_secret { + Some(HmacSecretResponse::Confirmed(flag)) => flag, + Some(HmacSecretResponse::Secret(_)) => true, + None => false, + }); } + Some(HmacCreateSecretOrPrf::Prf) => { + result.extensions.prf = + Some(match &result.att_obj.auth_data.extensions.hmac_secret { + None => AuthenticationExtensionsPRFOutputs { + enabled: Some(false), + results: None, + }, + Some(HmacSecretResponse::Confirmed(flag)) => { + AuthenticationExtensionsPRFOutputs { + enabled: Some(*flag), + results: None, + } + } + Some(hmac_response @ HmacSecretResponse::Secret(_)) => { + AuthenticationExtensionsPRFOutputs { + enabled: Some(true), + results: dev + .get_shared_secret() + .and_then(|shared_secret| { + hmac_response.decrypt_secrets(shared_secret) + }) + .and_then(Result::ok) + .map(|outputs| outputs.into()), + } + } + }) + } + None | Some(HmacCreateSecretOrPrf::HmacCreateSecret(false)) => {} } } } @@ -580,7 +640,9 @@ pub mod test { AuthenticatorDataFlags, Signature, }; use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; - use crate::ctap2::commands::make_credentials::MakeCredentialsExtensions; + use crate::ctap2::commands::make_credentials::{ + HmacCreateSecretOrPrf, MakeCredentialsExtensions, + }; use crate::ctap2::commands::{RequestCtap1, RequestCtap2}; use crate::ctap2::server::{ AuthenticatorAttachment, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, @@ -691,7 +753,7 @@ pub mod test { cred_protect: Some( crate::ctap2::server::CredentialProtectionPolicy::UserVerificationRequired, ), - hmac_secret: Some(true), + hmac_secret: Some(HmacCreateSecretOrPrf::HmacCreateSecret(true)), min_pin_length: Some(true), }, options: MakeCredentialsOptions { diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index bc45ceb9..6ec768b4 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -616,18 +616,6 @@ pub fn sign( ), callback ); - // Third, use the shared secret in the extensions, if requested - if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() { - if let Some(secret) = dev.get_shared_secret() { - match extension.calculate(secret) { - Ok(x) => x, - Err(e) => { - callback.call(Err(e)); - return false; - } - } - } - } // Do "pre-flight": Filter the allow-list let original_allow_list_was_empty = get_assertion.allow_list.is_empty(); @@ -672,6 +660,17 @@ pub fn sign( return false; } + // Use the shared secret in the extensions, if requested + get_assertion = match get_assertion.process_hmac_secret_and_prf_extension( + dev.get_shared_secret().map(|s| (s, &pin_uv_auth_result)), + ) { + Ok(value) => value, + Err(e) => { + callback.call(Err(e)); + return false; + } + }; + debug!("------------------------------------------------------------------"); debug!("{get_assertion:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index b70bf1c9..29ed7c1b 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -1,4 +1,5 @@ -use crate::crypto::COSEAlgorithm; +use super::commands::get_assertion::HmacSecretExtension; +use crate::crypto::{COSEAlgorithm, CryptoError, PinUvAuthToken, SharedSecret}; use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; use base64::Engine; use serde::de::MapAccess; @@ -8,6 +9,7 @@ use serde::{ }; use serde_bytes::{ByteBuf, Bytes}; use sha2::{Digest, Sha256}; +use std::collections::HashMap; use std::convert::{Into, TryFrom}; use std::fmt; @@ -59,13 +61,7 @@ impl RelyingParty { } pub fn hash(&self) -> RpIdHash { - let mut hasher = Sha256::new(); - hasher.update(&self.id); - - let mut output = [0u8; 32]; - output.copy_from_slice(hasher.finalize().as_slice()); - - RpIdHash(output) + RpIdHash(Sha256::digest(&self.id).into()) } } @@ -365,7 +361,9 @@ pub struct AuthenticationExtensionsClientInputs { pub credential_protection_policy: Option, pub enforce_credential_protection_policy: Option, pub hmac_create_secret: Option, + pub hmac_get_secret: Option, pub min_pin_length: Option, + pub prf: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -373,11 +371,130 @@ pub struct CredentialProperties { pub rk: bool, } +/// Salt inputs for the `hmac-secret` extension. +/// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#dictdef-hmacgetsecretinput +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct HMACGetSecretInput { + pub salt1: [u8; 32], + pub salt2: Option<[u8; 32]>, +} + +/// Decrypted HMAC outputs from the `hmac-secret` extension. +/// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#dictdef-hmacgetsecretoutput +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct HMACGetSecretOutput { + pub output1: [u8; 32], + pub output2: Option<[u8; 32]>, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct AuthenticationExtensionsPRFInputs { + pub eval: Option, + pub eval_by_credential: Option, AuthenticationExtensionsPRFValues>>, +} + +impl AuthenticationExtensionsPRFInputs { + /// Select an `eval` or `evalByCredential` entry and calculate hmac-secret salt inputs from those inputs. + /// + /// Returns [None] if the `eval` input was not given and no credential in `allow_credentials` matched any `evalByCredential` entry. + /// Otherwise returns the initialized [HmacSecretExtension] and, if an `evalByCredential` entry was used to compute the salt inputs, + /// the [PublicKeyCredentialDescriptor] matching that `evalByCredential` entry. + /// If present, `allowCredentials` SHOULD be set to contain only that [PublicKeyCredentialDescriptor] value. + pub fn calculate<'allow_cred>( + &self, + secret: &SharedSecret, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + puat: Option<&PinUvAuthToken>, + ) -> Result< + Option<( + HmacSecretExtension, + Option<&'allow_cred PublicKeyCredentialDescriptor>, + )>, + CryptoError, + > { + if let Some((selected_credential, ev)) = self.select_eval(allow_credentials) { + let mut hmac_secret = HmacSecretExtension::new( + Self::eval_to_salt(&ev.first).to_vec(), + ev.second + .as_ref() + .map(|second| Self::eval_to_salt(second).to_vec()), + ); + hmac_secret.calculate(secret, puat)?; + Ok(Some((hmac_secret, selected_credential))) + } else { + Ok(None) + } + } + + /// Select an `evalByCredential` entry matching any element of `allow_credentials`, + /// or otherwise fall back to `eval`, if present, if no match is found. + fn select_eval<'allow_cred>( + &self, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + ) -> Option<( + Option<&'allow_cred PublicKeyCredentialDescriptor>, + &AuthenticationExtensionsPRFValues, + )> { + self.select_credential(allow_credentials) + .map(|(cred, ev)| (Some(cred), ev)) + .or(self.eval.as_ref().map(|eval| (None, eval))) + } + + /// Select an `evalByCredential` entry matching any element of `allow_credentials`. + fn select_credential<'allow_cred>( + &self, + allow_credentials: &'allow_cred [PublicKeyCredentialDescriptor], + ) -> Option<( + &'allow_cred PublicKeyCredentialDescriptor, + &AuthenticationExtensionsPRFValues, + )> { + self.eval_by_credential + .as_ref() + .and_then(|eval_by_credential| { + allow_credentials + .iter() + .find_map(|pkcd| eval_by_credential.get(&pkcd.id).map(|eval| (pkcd, eval))) + }) + } + + /// Convert a PRF eval input to an hmac-secret salt input. + fn eval_to_salt(eval: &[u8]) -> [u8; 32] { + Sha256::new_with_prefix(b"WebAuthn PRF") + .chain_update([0x00].iter()) + .chain_update(eval.iter()) + .finalize() + .into() + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthenticationExtensionsPRFValues { + pub first: Vec, + pub second: Option>, +} + +impl From for AuthenticationExtensionsPRFValues { + fn from(hmac_output: HMACGetSecretOutput) -> Self { + Self { + first: hmac_output.output1.to_vec(), + second: hmac_output.output2.map(|o2| o2.to_vec()), + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthenticationExtensionsPRFOutputs { + pub enabled: Option, + pub results: Option, +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct AuthenticationExtensionsClientOutputs { pub app_id: Option, pub cred_props: Option, pub hmac_create_secret: Option, + pub hmac_get_secret: Option, + pub prf: Option, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src/transport/mock/device.rs b/src/transport/mock/device.rs index ac4e156d..211da465 100644 --- a/src/transport/mock/device.rs +++ b/src/transport/mock/device.rs @@ -32,6 +32,7 @@ pub struct Device { skip_serialization: bool, pub upcoming_requests: VecDeque>, pub upcoming_responses: VecDeque, HIDError>>, + pub shared_secret: Option, } impl Device { @@ -91,6 +92,7 @@ impl Device { skip_serialization: true, upcoming_requests: VecDeque::new(), upcoming_responses: VecDeque::new(), + shared_secret: None, }) } } @@ -171,6 +173,7 @@ impl HIDDevice for Device { skip_serialization: false, upcoming_requests: VecDeque::new(), upcoming_responses: VecDeque::new(), + shared_secret: None, }) } @@ -303,11 +306,11 @@ impl FidoDevice for Device { } fn get_shared_secret(&self) -> std::option::Option<&SharedSecret> { - None + self.shared_secret.as_ref() } - fn set_shared_secret(&mut self, _: SharedSecret) { - // Nothing + fn set_shared_secret(&mut self, shared_secret: SharedSecret) { + self.shared_secret = Some(shared_secret); } fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {