diff --git a/libwebauthn/examples/webauthn_extensions_hid.rs b/libwebauthn/examples/webauthn_extensions_hid.rs index 02685c8..1c65b3b 100644 --- a/libwebauthn/examples/webauthn_extensions_hid.rs +++ b/libwebauthn/examples/webauthn_extensions_hid.rs @@ -7,7 +7,7 @@ use rand::{thread_rng, Rng}; use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ - GetAssertionRequest, GetAssertionRequestExtensions, MakeCredentialRequest, + GetAssertionRequest, GetAssertionRequestExtensions, HMACGetSecretInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, UserVerificationRequirement, }; use libwebauthn::pin::{UvProvider, StdinPromptPinProvider}; @@ -100,6 +100,10 @@ pub async fn main() -> Result<(), Box> { user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions { cred_blob: Some(true), + hmac_secret: Some(HMACGetSecretInput { + salt1: [1; 32], + salt2: None, + }), }), timeout: TIMEOUT, }; diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index 8ff5f53..81bda10 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -180,7 +180,7 @@ impl UpgradableResponse for SignResponse { }; // Let authenticatorGetAssertionResponse be a CBOR map with the following keys whose values are as follows: [..] - let upgraded_response: GetAssertionResponse = Ctap2GetAssertionResponse { + let response = Ctap2GetAssertionResponse { credential_id: Some(Ctap2PublicKeyCredentialDescriptor { r#type: Ctap2PublicKeyCredentialType::PublicKey, id: ByteBuf::from(request.key_handle.clone()), @@ -195,8 +195,8 @@ impl UpgradableResponse for SignResponse { unsigned_extension_outputs: None, enterprise_attestation: None, attestation_statement: None, - } - .into(); + }; + let upgraded_response = [response.into_assertion_output(None)].as_slice().into(); trace!(?upgraded_response); Ok(upgraded_response) diff --git a/libwebauthn/src/ops/webauthn.rs b/libwebauthn/src/ops/webauthn.rs index 834f63e..3622ff6 100644 --- a/libwebauthn/src/ops/webauthn.rs +++ b/libwebauthn/src/ops/webauthn.rs @@ -1,15 +1,18 @@ -use std::time::Duration; +use std::{collections::BTreeMap, time::Duration}; use ctap_types::ctap2::credential_management::CredentialProtectionPolicy; use serde::{Deserialize, Serialize}; +use serde_cbor::Value; use sha2::{Digest, Sha256}; -use tracing::{debug, instrument, trace}; +use tracing::{debug, error, instrument, trace}; use crate::{ + fido::AuthenticatorData, + pin::PinUvAuthProtocol, proto::{ ctap1::{Ctap1RegisteredKey, Ctap1Version}, ctap2::{ - Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, Ctap2GetAssertionResponse, + Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, Ctap2MakeCredentialResponse, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, }, @@ -145,55 +148,100 @@ pub struct GetAssertionRequest { pub timeout: Duration, } -#[derive(Debug, Default, Clone, Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct HMACGetSecretInput { + pub salt1: [u8; 32], + pub salt2: Option<[u8; 32]>, +} + +#[derive(Debug, Default, Clone)] pub struct GetAssertionRequestExtensions { - #[serde(skip_serializing_if = "Option::is_none")] pub cred_blob: Option, - // Thanks, FIDO-spec for this consistent naming scheme... - // #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] - // TODO: Do this properly with the salts - // pub hmac_secret: Option>, + pub hmac_secret: Option, } -impl GetAssertionRequestExtensions { - pub fn skip_serializing(&self) -> bool { - self.cred_blob.is_none() /* && self.hmac_secret.is_none() */ +#[derive(Clone, Debug, Default)] +pub struct HMACGetSecretOutput { + pub output1: [u8; 32], + pub output2: Option<[u8; 32]>, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(transparent)] +pub struct Ctap2HMACGetSecretOutput { + // We get this from the device, but have to decrypt it, and + // potentially split it into 2 arrays + #[serde(with = "serde_bytes")] + pub(crate) encrypted_output: Vec, +} + +impl Ctap2HMACGetSecretOutput { + pub(crate) fn decrypt_output( + self, + shared_secret: &[u8], + uv_proto: &Box, + ) -> Option { + let output = match uv_proto.decrypt(shared_secret, &self.encrypted_output) { + Ok(o) => o, + Err(e) => { + error!("Failed to decrypt HMAC Secret output with the shared secret: {e:?}. Skipping HMAC extension"); + return None; + } + }; + let mut res = HMACGetSecretOutput::default(); + if output.len() == 32 { + res.output1.copy_from_slice(&output); + } else if output.len() == 64 { + let (o1, o2) = output.split_at(32); + res.output1.copy_from_slice(&o1); + res.output2 = Some(o2.try_into().unwrap()); + } else { + error!("Failed to split HMAC Secret outputs. Unexpected output length: {}. Skipping HMAC extension", output.len()); + return None; + } + + Some(res) } } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Default, Clone)] pub struct GetAssertionResponseExtensions { // Stored credBlob - #[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")] pub cred_blob: Option>, // Thanks, FIDO-spec for this consistent naming scheme... - #[serde( - rename = "hmac-secret", - default, - skip_serializing_if = "Option::is_none", - with = "serde_bytes" - )] - pub hmac_secret: Option>, + pub hmac_secret: Option, } #[derive(Debug, Clone)] pub struct GetAssertionResponse { - pub assertions: Vec, + pub assertions: Vec, +} + +#[derive(Debug, Clone)] +pub struct Assertion { + pub credential_id: Option, + pub authenticator_data: AuthenticatorData, + pub signature: Vec, + pub user: Option, + pub credentials_count: Option, + pub user_selected: Option, + pub large_blob_key: Option>, + pub unsigned_extension_outputs: Option>, + pub enterprise_attestation: Option, + pub attestation_statement: Option, } -impl From<&[Ctap2GetAssertionResponse]> for GetAssertionResponse { - fn from(assertions: &[Ctap2GetAssertionResponse]) -> Self { +impl From<&[Assertion]> for GetAssertionResponse { + fn from(assertions: &[Assertion]) -> Self { Self { assertions: assertions.to_owned(), } } } -impl From for GetAssertionResponse { - fn from(assertion: Ctap2GetAssertionResponse) -> Self { +impl From for GetAssertionResponse { + fn from(assertion: Assertion) -> Self { Self { assertions: vec![assertion], } diff --git a/libwebauthn/src/proto/ctap2/model/client_pin.rs b/libwebauthn/src/proto/ctap2/model/client_pin.rs index 4936a1a..94f9bbf 100644 --- a/libwebauthn/src/proto/ctap2/model/client_pin.rs +++ b/libwebauthn/src/proto/ctap2/model/client_pin.rs @@ -3,6 +3,8 @@ use serde_bytes::ByteBuf; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use crate::pin::{PinUvAuthProtocol, PinUvAuthProtocolOne, PinUvAuthProtocolTwo}; + #[derive(Debug, Clone, SerializeIndexed)] #[serde_indexed(offset = 1)] pub struct Ctap2ClientPinRequest { @@ -212,6 +214,15 @@ pub enum Ctap2PinUvAuthProtocol { Two = 2, } +impl Ctap2PinUvAuthProtocol { + pub(crate) fn create_protocol_object(&self) -> Box { + match self { + Ctap2PinUvAuthProtocol::One => Box::new(PinUvAuthProtocolOne::new()), + Ctap2PinUvAuthProtocol::Two => Box::new(PinUvAuthProtocolTwo::new()), + } + } +} + #[repr(u32)] #[derive(Debug, Clone, FromPrimitive, PartialEq, Serialize_repr, Deserialize_repr)] pub enum Ctap2PinUvAuthProtocolCommand { diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 847df68..455c0f8 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -1,9 +1,11 @@ use crate::{ fido::AuthenticatorData, ops::webauthn::{ - GetAssertionRequest, GetAssertionRequestExtensions, GetAssertionResponseExtensions, + Assertion, Ctap2HMACGetSecretOutput, GetAssertionRequest, GetAssertionRequestExtensions, + GetAssertionResponseExtensions, HMACGetSecretInput, }, pin::PinUvAuthProtocol, + transport::AuthTokenData, }; use super::{ @@ -11,11 +13,13 @@ use super::{ Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialUserEntity, Ctap2UserVerifiableRequest, }; +use cosey::PublicKey; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use serde_cbor::Value; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; use std::collections::BTreeMap; +use tracing::error; #[derive(Debug, Clone, Copy, Serialize, Default)] pub struct Ctap2GetAssertionOptions { @@ -111,7 +115,7 @@ pub struct Ctap2GetAssertionRequest { /// extensions (0x04) #[serde(skip_serializing_if = "Self::skip_serializing_extensions")] - pub extensions: Option, + pub extensions: Option, /// options (0x05) #[serde(skip_serializing_if = "Option::is_none")] @@ -127,7 +131,9 @@ pub struct Ctap2GetAssertionRequest { } impl Ctap2GetAssertionRequest { - pub fn skip_serializing_extensions(extensions: &Option) -> bool { + pub fn skip_serializing_extensions( + extensions: &Option, + ) -> bool { extensions .as_ref() .map_or(true, |extensions| extensions.skip_serializing()) @@ -140,7 +146,7 @@ impl From<&GetAssertionRequest> for Ctap2GetAssertionRequest { relying_party_id: op.relying_party_id.clone(), client_data_hash: ByteBuf::from(op.hash.clone()), allow: op.allow.clone(), - extensions: op.extensions.clone(), + extensions: op.extensions.as_ref().map(|x| x.clone().into()), options: Some(Ctap2GetAssertionOptions { require_user_presence: true, require_user_verification: op.user_verification.is_required(), @@ -151,13 +157,89 @@ impl From<&GetAssertionRequest> for Ctap2GetAssertionRequest { } } +#[derive(Debug, Default, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Ctap2GetAssertionRequestExtensions { + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, + // Thanks, FIDO-spec for this consistent naming scheme... + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, + // From which we calculate hmac_secret + #[serde(skip)] + pub hmac_salts: Option, +} + +impl From for Ctap2GetAssertionRequestExtensions { + fn from(other: GetAssertionRequestExtensions) -> Self { + Ctap2GetAssertionRequestExtensions { + cred_blob: other.cred_blob, + hmac_secret: None, // Get's calculated later + hmac_salts: other.hmac_secret, + } + } +} + +impl Ctap2GetAssertionRequestExtensions { + pub fn skip_serializing(&self) -> bool { + self.cred_blob.is_none() && self.hmac_secret.is_none() + } + + pub fn calculate_hmac(&mut self, auth_data: &AuthTokenData) { + let input = if let Some(hmac_input) = &self.hmac_salts { + hmac_input + } else { + return; + }; + let uv_proto = auth_data.protocol_version.create_protocol_object(); + + let public_key = auth_data.key_agreement.clone(); + // saltEnc(0x02): Encryption of the one or two salts (called salt1 (32 bytes) and salt2 (32 bytes)) using the shared secret as follows: + // One salt case: encrypt(shared secret, salt1) + // Two salt case: encrypt(shared secret, salt1 || salt2) + let mut salts = input.salt1.to_vec(); + if let Some(salt2) = input.salt2 { + salts.extend(salt2); + } + let salt_enc = if let Ok(res) = uv_proto.encrypt(&auth_data.shared_secret, &salts) { + ByteBuf::from(res) + } else { + error!("Failed to encrypt HMAC salts with shared secret! Skipping HMAC"); + return; + }; + + let salt_auth = ByteBuf::from(uv_proto.authenticate(&auth_data.shared_secret, &salt_enc)); + + self.hmac_secret = Some(CalculatedHMACGetSecretInput { + public_key, + salt_enc, + salt_auth, + pin_auth_proto: Some(auth_data.protocol_version as u32), + }) + } +} + +#[derive(Debug, Clone, SerializeIndexed)] +#[serde_indexed(offset = 1)] +pub struct CalculatedHMACGetSecretInput { + // keyAgreement(0x01): public key of platform key-agreement key. + pub public_key: PublicKey, + // saltEnc(0x02): Encryption of the one or two salts + pub salt_enc: ByteBuf, + // saltAuth(0x03): authenticate(shared secret, saltEnc) + pub salt_auth: ByteBuf, + // pinUvAuthProtocol(0x04): (optional) as selected when getting the shared secret. CTAP2.1 platforms MUST include this parameter if the value of pinUvAuthProtocol is not 1. + #[serde(skip_serializing_if = "Option::is_none")] + pub pin_auth_proto: Option, +} + #[derive(Debug, Clone, DeserializeIndexed)] #[serde_indexed(offset = 1)] pub struct Ctap2GetAssertionResponse { #[serde(skip_serializing_if = "Option::is_none")] pub credential_id: Option, - pub authenticator_data: AuthenticatorData, + pub authenticator_data: AuthenticatorData, pub signature: ByteBuf, @@ -221,3 +303,65 @@ impl Ctap2UserVerifiableRequest for Ctap2GetAssertionRequest { // No-op } } + +impl Ctap2GetAssertionResponse { + pub fn into_assertion_output(self, auth_data: Option<&AuthTokenData>) -> Assertion { + let authenticator_data = AuthenticatorData:: { + rp_id_hash: self.authenticator_data.rp_id_hash, + flags: self.authenticator_data.flags, + signature_count: self.authenticator_data.signature_count, + attested_credential: self.authenticator_data.attested_credential, + extensions: self + .authenticator_data + .extensions + .map(|x| x.into_output(auth_data)), + }; + Assertion { + credential_id: self.credential_id, + authenticator_data, + signature: self.signature.into_vec(), + user: self.user, + credentials_count: self.credentials_count, + user_selected: self.user_selected, + large_blob_key: self.large_blob_key.map(ByteBuf::into_vec), + unsigned_extension_outputs: self.unsigned_extension_outputs, + enterprise_attestation: self.enterprise_attestation, + attestation_statement: self.attestation_statement, + } + } +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Ctap2GetAssertionResponseExtensions { + // Stored credBlob + #[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")] + pub cred_blob: Option>, + + // Thanks, FIDO-spec for this consistent naming scheme... + #[serde( + rename = "hmac-secret", + default, + skip_serializing_if = "Option::is_none" + )] + pub hmac_secret: Option, +} + +impl Ctap2GetAssertionResponseExtensions { + pub(crate) fn into_output( + self, + auth_data: Option<&AuthTokenData>, + ) -> GetAssertionResponseExtensions { + GetAssertionResponseExtensions { + cred_blob: self.cred_blob, + hmac_secret: self.hmac_secret.and_then(|x| { + if let Some(auth_data) = auth_data { + let uv_proto = auth_data.protocol_version.create_protocol_object(); + x.decrypt_output(&auth_data.shared_secret, &uv_proto) + } else { + None + } + }), + } + } +} diff --git a/libwebauthn/src/transport/ble/channel.rs b/libwebauthn/src/transport/ble/channel.rs index 53c43c2..cc6dcdf 100644 --- a/libwebauthn/src/transport/ble/channel.rs +++ b/libwebauthn/src/transport/ble/channel.rs @@ -7,9 +7,7 @@ use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; use crate::proto::CtapError; use crate::transport::ble::bluez; -use crate::transport::channel::{ - Channel, ChannelStatus, Ctap2AuthTokenPermission, Ctap2AuthTokenStore, -}; +use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; use crate::transport::device::SupportedProtocols; use crate::transport::error::{Error, TransportError}; @@ -27,7 +25,7 @@ pub struct BleChannel<'a> { device: &'a BleDevice, connection: Connection, revision: FidoRevision, - auth_token: Option<(Ctap2AuthTokenPermission, Vec)>, + auth_token_data: Option, } impl<'a> BleChannel<'a> { @@ -46,7 +44,7 @@ impl<'a> BleChannel<'a> { device, connection, revision, - auth_token: None, + auth_token_data: None, }; bluez::notify_start(&channel.connection) .await @@ -159,24 +157,15 @@ impl<'a> Channel for BleChannel<'a> { } impl Ctap2AuthTokenStore for BleChannel<'_> { - fn store_uv_auth_token( - &mut self, - permission: Ctap2AuthTokenPermission, - pin_uv_auth_token: &[u8], - ) { - self.auth_token = Some((permission, pin_uv_auth_token.to_vec())); + fn store_auth_data(&mut self, auth_token_data: AuthTokenData) { + self.auth_token_data = Some(auth_token_data); } - fn get_uv_auth_token(&self, requested_permission: &Ctap2AuthTokenPermission) -> Option<&[u8]> { - if let Some((stored_permission, stored_token)) = &self.auth_token { - if stored_permission.contains(requested_permission) { - return Some(stored_token); - } - } - None + fn get_auth_data(&self) -> Option<&AuthTokenData> { + self.auth_token_data.as_ref() } fn clear_uv_auth_token_store(&mut self) { - self.auth_token = None; + self.auth_token_data = None; } } diff --git a/libwebauthn/src/transport/cable/channel.rs b/libwebauthn/src/transport/cable/channel.rs index f332f7e..d4dc335 100644 --- a/libwebauthn/src/transport/cable/channel.rs +++ b/libwebauthn/src/transport/cable/channel.rs @@ -11,9 +11,9 @@ use crate::proto::{ ctap2::cbor::{CborRequest, CborResponse}, }; use crate::transport::error::{Error, TransportError}; +use crate::transport::AuthTokenData; use crate::transport::{ - channel::ChannelStatus, device::SupportedProtocols, Channel, Ctap2AuthTokenPermission, - Ctap2AuthTokenStore, + channel::ChannelStatus, device::SupportedProtocols, Channel, Ctap2AuthTokenStore, }; use super::known_devices::CableKnownDevice; @@ -90,14 +90,9 @@ impl<'d> Channel for CableChannel<'d> { } impl<'d> Ctap2AuthTokenStore for CableChannel<'d> { - fn store_uv_auth_token( - &mut self, - _permission: Ctap2AuthTokenPermission, - _pin_uv_auth_token: &[u8], - ) { - } + fn store_auth_data(&mut self, _auth_token_data: AuthTokenData) {} - fn get_uv_auth_token(&self, _requested_permission: &Ctap2AuthTokenPermission) -> Option<&[u8]> { + fn get_auth_data(&self) -> Option<&AuthTokenData> { None } diff --git a/libwebauthn/src/transport/channel.rs b/libwebauthn/src/transport/channel.rs index 98e046b..2647ffd 100644 --- a/libwebauthn/src/transport/channel.rs +++ b/libwebauthn/src/transport/channel.rs @@ -9,6 +9,7 @@ use crate::proto::{ use crate::transport::error::Error; use async_trait::async_trait; +use cosey::PublicKey; use super::device::SupportedProtocols; @@ -34,9 +35,9 @@ pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Ctap2AuthTokenPermission { - pin_uv_auth_protocol: Ctap2PinUvAuthProtocol, - role: Ctap2AuthTokenPermissionRole, - rpid: Option, + pub(crate) pin_uv_auth_protocol: Ctap2PinUvAuthProtocol, + pub(crate) role: Ctap2AuthTokenPermissionRole, + pub(crate) rpid: Option, } impl Ctap2AuthTokenPermission { @@ -63,13 +64,26 @@ impl Ctap2AuthTokenPermission { } } +#[derive(Debug, Clone)] +pub struct AuthTokenData { + pub shared_secret: Vec, + pub permission: Ctap2AuthTokenPermission, + pub pin_uv_auth_token: Vec, + pub protocol_version: Ctap2PinUvAuthProtocol, + pub key_agreement: PublicKey, +} + #[async_trait] pub trait Ctap2AuthTokenStore { - fn store_uv_auth_token( - &mut self, - permission: Ctap2AuthTokenPermission, - pin_uv_auth_token: &[u8], - ); - fn get_uv_auth_token(&self, requested_permission: &Ctap2AuthTokenPermission) -> Option<&[u8]>; + fn store_auth_data(&mut self, auth_token_data: AuthTokenData); + fn get_auth_data(&self) -> Option<&AuthTokenData>; fn clear_uv_auth_token_store(&mut self); + fn get_uv_auth_token(&self, requested_permission: &Ctap2AuthTokenPermission) -> Option<&[u8]> { + if let Some(stored_data) = self.get_auth_data() { + if stored_data.permission.contains(requested_permission) { + return Some(&stored_data.pin_uv_auth_token); + } + } + None + } } diff --git a/libwebauthn/src/transport/hid/channel.rs b/libwebauthn/src/transport/hid/channel.rs index 9debeba..757357c 100644 --- a/libwebauthn/src/transport/hid/channel.rs +++ b/libwebauthn/src/transport/hid/channel.rs @@ -17,9 +17,7 @@ use tokio::net::UdpSocket; use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; -use crate::transport::channel::{ - Channel, ChannelStatus, Ctap2AuthTokenPermission, Ctap2AuthTokenStore, -}; +use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; use crate::transport::device::SupportedProtocols; use crate::transport::error::{Error, TransportError}; use crate::transport::hid::framing::{ @@ -52,7 +50,7 @@ pub struct HidChannel<'d> { device: &'d HidDevice, open_device: OpenHidDevice, init: InitResponse, - auth_token: Option<(Ctap2AuthTokenPermission, Vec)>, + auth_token_data: Option, } impl<'d> HidChannel<'d> { @@ -69,7 +67,7 @@ impl<'d> HidChannel<'d> { HidBackendDevice::VirtualDevice(_) => OpenHidDevice::VirtualDevice, }, init: InitResponse::default(), - auth_token: None, + auth_token_data: None, }; channel.init = channel.init(INIT_TIMEOUT).await?; Ok(channel) @@ -449,24 +447,15 @@ bitflags! { } impl Ctap2AuthTokenStore for HidChannel<'_> { - fn store_uv_auth_token( - &mut self, - permission: Ctap2AuthTokenPermission, - pin_uv_auth_token: &[u8], - ) { - self.auth_token = Some((permission, pin_uv_auth_token.to_vec())); + fn store_auth_data(&mut self, auth_token_data: AuthTokenData) { + self.auth_token_data = Some(auth_token_data); } - fn get_uv_auth_token(&self, requested_permission: &Ctap2AuthTokenPermission) -> Option<&[u8]> { - if let Some((stored_permission, stored_token)) = &self.auth_token { - if stored_permission.contains(requested_permission) { - return Some(stored_token); - } - } - None + fn get_auth_data(&self) -> Option<&AuthTokenData> { + self.auth_token_data.as_ref() } fn clear_uv_auth_token_store(&mut self) { - self.auth_token = None; + self.auth_token_data = None; } } diff --git a/libwebauthn/src/transport/mod.rs b/libwebauthn/src/transport/mod.rs index 234a8b0..6b729a9 100644 --- a/libwebauthn/src/transport/mod.rs +++ b/libwebauthn/src/transport/mod.rs @@ -8,7 +8,7 @@ pub mod hid; mod channel; mod transport; -pub(crate) use channel::Ctap2AuthTokenPermission; +pub(crate) use channel::{AuthTokenData, Ctap2AuthTokenPermission}; pub use channel::{Channel, Ctap2AuthTokenStore}; pub use device::Device; pub use transport::Transport; diff --git a/libwebauthn/src/webauthn.rs b/libwebauthn/src/webauthn.rs index 56561a1..20ed93d 100644 --- a/libwebauthn/src/webauthn.rs +++ b/libwebauthn/src/webauthn.rs @@ -22,7 +22,7 @@ use crate::proto::ctap2::{ Ctap2UserVerificationOperation, }; pub use crate::transport::error::{CtapError, Error, PlatformError, TransportError}; -use crate::transport::{Channel, Ctap2AuthTokenPermission}; +use crate::transport::{AuthTokenData, Channel, Ctap2AuthTokenPermission}; macro_rules! handle_errors { ($channel: expr, $resp: expr, $uv_auth_used: expr, $pin_provider: expr, $timeout: expr) => { @@ -184,6 +184,13 @@ where op.timeout, ) .await?; + + if let Some(auth_data) = self.get_auth_data() { + if let Some(e) = ctap2_request.extensions.as_mut() { + e.calculate_hmac(auth_data) + } + } + handle_errors!( self, self.ctap2_get_assertion(&ctap2_request, op.timeout).await, @@ -193,11 +200,12 @@ where ) }?; let count = response.credentials_count.unwrap_or(1); - let mut assertions = vec![response]; + let mut assertions = vec![response.into_assertion_output(self.get_auth_data())]; for i in 1..count { debug!({ i }, "Fetching additional credential"); // GetNextAssertion doesn't use PinUVAuthToken, so we don't need to check uv_auth_used here - assertions.push(self.ctap2_get_next_assertion(op.timeout).await?); + let response = self.ctap2_get_next_assertion(op.timeout).await?; + assertions.push(response.into_assertion_output(self.get_auth_data())); } Ok(assertions.as_slice().into()) } @@ -340,7 +348,7 @@ where let skip_uv = !ctap2_request.can_use_uv(&get_info_response); let mut uv_blocked = false; - let (uv_proto, token_response, shared_secret) = loop { + let (uv_proto, token_response, shared_secret, public_key) = loop { let uv_operation = get_info_response .uv_operation(uv_blocked || skip_uv) .ok_or({ @@ -399,14 +407,14 @@ where Ctap2UserVerificationOperation::GetPinToken => { Ctap2ClientPinRequest::new_get_pin_token( uv_proto.version(), - public_key, + public_key.clone(), &uv_proto.encrypt(&shared_secret, &pin_hash(&pin.unwrap()))?, ) } Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions => { Ctap2ClientPinRequest::new_get_pin_token_with_perm( uv_proto.version(), - public_key, + public_key.clone(), &uv_proto.encrypt(&shared_secret, &pin_hash(&pin.unwrap()))?, ctap2_request.permissions(), ctap2_request.permissions_rpid(), @@ -415,7 +423,7 @@ where Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions => { Ctap2ClientPinRequest::new_get_uv_token_with_perm( uv_proto.version(), - public_key, + public_key.clone(), ctap2_request.permissions(), ctap2_request.permissions_rpid(), ) @@ -424,7 +432,7 @@ where match channel.ctap2_client_pin(&token_request, timeout).await { Ok(t) => { - break (uv_proto, t, shared_secret); + break (uv_proto, t, shared_secret, public_key); } // Internal retry, because we otherwise can't fall back to PIN, if the UV is blocked Err(Error::Ctap(CtapError::UvBlocked)) => { @@ -470,7 +478,16 @@ where ctap2_request.permissions(), ctap2_request.permissions_rpid(), ); - channel.store_uv_auth_token(token_identifier, &uv_auth_token); + + // Storing auth token for later (re)use, or for calculating HMAC secrects, etc. + let auth_token_data = AuthTokenData { + shared_secret: shared_secret.to_vec(), + permission: token_identifier, + pin_uv_auth_token: uv_auth_token.clone(), + protocol_version: uv_proto.version(), + key_agreement: public_key, + }; + channel.store_auth_data(auth_token_data); // If successful, the platform creates the pinUvAuthParam parameter by calling // authenticate(pinUvAuthToken, clientDataHash), and goes to Step 1.1.1.