Skip to content

Commit 64d5b9a

Browse files
Verify signature on linking info
1 parent 8c68e66 commit 64d5b9a

File tree

4 files changed

+65
-11
lines changed

4 files changed

+65
-11
lines changed

libwebauthn/src/transport/cable/known_devices.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice {
191191
let noise_state = tunnel::do_handshake(&mut ws_stream, psk, &connection_type).await?;
192192

193193
tunnel::channel(
194+
&connection_type,
194195
noise_state,
195196
&self.device_info.tunnel_domain,
196197
&Some(self.store.clone()),
@@ -203,7 +204,7 @@ impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice {
203204
type ClientNonce = [u8; 16];
204205

205206
// Key 3: either the string “ga” to hint that a getAssertion will follow, or “mc” to hint that a makeCredential will follow.
206-
#[derive(Debug, SerializeIndexed)]
207+
#[derive(Clone, Debug, SerializeIndexed)]
207208
#[serde(offset = 1)]
208209
pub struct ClientPayload {
209210
pub link_id: ByteBuf,

libwebauthn/src/transport/cable/qr_code_device.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl ToString for CableQrCode {
6969
pub struct CableQrCodeDevice {
7070
/// The QR code to be scanned by the new authenticator.
7171
pub qr_code: CableQrCode,
72-
/// An ephemeral private, corresponding to the public key within the QR code.
72+
/// An ephemeral private key, corresponding to the public key within the QR code.
7373
pub private_key: NonZeroScalar,
7474
/// An optional reference to the store. This may be None, if no persistence is desired.
7575
pub(crate) store: Option<Arc<dyn CableKnownDeviceInfoStore>>,
@@ -189,7 +189,14 @@ impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice {
189189
};
190190
let mut ws_stream = tunnel::connect(&tunnel_domain, &connection_type).await?;
191191
let noise_state = tunnel::do_handshake(&mut ws_stream, psk, &connection_type).await?;
192-
tunnel::channel(noise_state, &tunnel_domain, &self.store, ws_stream).await
192+
tunnel::channel(
193+
&connection_type,
194+
noise_state,
195+
&tunnel_domain,
196+
&self.store,
197+
ws_stream,
198+
)
199+
.await
193200
}
194201

195202
// #[instrument(skip_all)]

libwebauthn/src/transport/cable/tunnel.rs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use std::sync::Arc;
33

44
use ctap_types::serde::cbor_deserialize;
55
use futures::{SinkExt, StreamExt};
6-
use p256::NonZeroScalar;
6+
use hmac::{Hmac, Mac};
7+
use p256::{ecdh, NonZeroScalar};
8+
use p256::{PublicKey, SecretKey};
79
use serde::Deserialize;
810
use serde_bytes::ByteBuf;
911
use serde_cbor::Value;
@@ -150,6 +152,7 @@ pub fn decode_tunnel_server_domain(encoded: u16) -> Option<String> {
150152
Some(ret)
151153
}
152154

155+
#[derive(Clone)]
153156
pub(crate) enum CableTunnelConnectionType {
154157
QrCode {
155158
routing_id: String,
@@ -340,6 +343,7 @@ pub(crate) async fn do_handshake(
340343
}
341344

342345
pub(crate) async fn channel(
346+
connection_type: &CableTunnelConnectionType,
343347
noise_state: TunnelNoiseState,
344348
tunnel_domain: &str,
345349
maybe_known_device_store: &Option<Arc<dyn CableKnownDeviceInfoStore>>,
@@ -350,8 +354,10 @@ pub(crate) async fn channel(
350354

351355
let tunnel_domain: String = tunnel_domain.to_string();
352356
let maybe_known_device_store = maybe_known_device_store.clone();
357+
let connection_type = connection_type.to_owned();
353358
let handle_connection = task::spawn(async move {
354359
connection(
360+
&connection_type,
355361
&tunnel_domain,
356362
&maybe_known_device_store,
357363
ws_stream,
@@ -375,6 +381,7 @@ pub(crate) async fn channel(
375381
}
376382

377383
async fn connection(
384+
connection_type: &CableTunnelConnectionType,
378385
tunnel_domain: &str,
379386
known_device_store: &Option<Arc<dyn CableKnownDeviceInfoStore>>,
380387
mut ws_stream: WebSocketStream<MaybeTlsStream<TcpStream>>,
@@ -414,7 +421,7 @@ async fn connection(
414421
Ok(message) => {
415422
debug!("Received WSS message");
416423
trace!(?message);
417-
let _ = connection_recv(tunnel_domain, known_device_store, message, &cbor_rx_send, &mut noise_state).await;
424+
let _ = connection_recv(connection_type, tunnel_domain, known_device_store, message, &cbor_rx_send, &mut noise_state).await;
418425
}
419426
};
420427
}
@@ -644,6 +651,7 @@ async fn connection_recv_update(message: &[u8]) -> Result<Option<CableLinkingInf
644651
}
645652

646653
async fn connection_recv(
654+
connection_type: &CableTunnelConnectionType,
647655
tunnel_domain: &str,
648656
known_device_store: &Option<Arc<dyn CableKnownDeviceInfoStore>>,
649657
message: Message,
@@ -695,14 +703,23 @@ async fn connection_recv(
695703
return Ok(());
696704
};
697705

706+
let CableTunnelConnectionType::QrCode { private_key, .. } = connection_type else {
707+
warn!("Ignoring update message for non-QR code connection");
708+
return Ok(());
709+
};
710+
698711
debug!("Received update message with linking info");
699712
trace!(?linking_info);
700713

701714
let device_id: CableKnownDeviceId = (&linking_info).into();
702715
match known_device_store {
703716
Some(store) => {
704-
match parse_known_device(tunnel_domain, &device_id, &linking_info, &noise_state)
705-
{
717+
match parse_known_device(
718+
private_key,
719+
tunnel_domain,
720+
&linking_info,
721+
&noise_state,
722+
) {
706723
Ok(known_device) => {
707724
debug!(?device_id, "Updating known device");
708725
trace!(?known_device);
@@ -728,17 +745,44 @@ async fn connection_recv(
728745
Ok(())
729746
}
730747

748+
/// Validation requires a shared key computed on the QR code ephemeral identity key (private_key here).
749+
/// We're currently unable to validate the signature on linking information received for state-assisted transactions,
750+
/// so these should be discarded. This is the same Chrome currently does, although it may change in future spec versions.
751+
/// See: https://github.com/chromium/chromium/blob/88e250200e59daf52554bcc74870138143a830c4/device/fido/cable/fido_tunnel_device.cc#L547-L549
731752
fn parse_known_device(
753+
private_key: &NonZeroScalar,
732754
tunnel_domain: &str,
733-
_device_id: &CableKnownDeviceId,
734755
linking_info: &CableLinkingInfo,
735-
_noise_state: &TunnelNoiseState,
756+
noise_state: &TunnelNoiseState,
736757
) -> Result<CableKnownDeviceInfo, Error> {
737758
let known_device = CableKnownDeviceInfo::new(tunnel_domain, linking_info)?;
759+
let secret_key = SecretKey::from(private_key);
738760

739-
// TODO: validate the signature
740-
// TODO: check public key hasn't changed for previous
761+
let Ok(authenticator_public_key) =
762+
PublicKey::from_sec1_bytes(&linking_info.authenticator_public_key)
763+
else {
764+
error!("Failed to parse public key.");
765+
return Err(Error::Transport(TransportError::InvalidKey));
766+
};
767+
768+
let shared_secret: Vec<u8> = ecdh::diffie_hellman(
769+
secret_key.to_nonzero_scalar(),
770+
authenticator_public_key.as_affine(),
771+
)
772+
.raw_secret_bytes()
773+
.to_vec();
774+
775+
let mut hmac = Hmac::<Sha256>::new_from_slice(&shared_secret).expect("Any key size is valid");
776+
hmac.update(&noise_state.handshake_hash);
777+
let expected_mac = hmac.finalize().into_bytes().to_vec();
778+
779+
if expected_mac != linking_info.handshake_signature {
780+
error!("Invalid handshake signature, rejecting update message");
781+
trace!(?expected_mac, ?linking_info.handshake_signature);
782+
return Err(Error::Transport(TransportError::InvalidSignature));
783+
}
741784

785+
debug!("Parsed known device with valid signature");
742786
Ok(known_device)
743787
}
744788

libwebauthn/src/transport/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ pub enum TransportError {
2929
TransportUnavailable,
3030
Timeout,
3131
UnknownDevice,
32+
InvalidKey,
33+
InvalidSignature,
3234
}
3335

3436
impl std::error::Error for TransportError {}

0 commit comments

Comments
 (0)