Skip to content

Commit c0e4a86

Browse files
authored
Implement pin protocol tests (#151)
Add a way to force the pin protocol supported by the platform, in order to test both protocols for various actions, such as setting and changing PINs and doing PRF.
1 parent 12e4b45 commit c0e4a86

File tree

7 files changed

+143
-5
lines changed

7 files changed

+143
-5
lines changed

libwebauthn/src/pin.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,13 @@ where
406406
return Err(Error::Platform(PlatformError::PinTooLong));
407407
}
408408

409-
let Some(uv_proto) = select_uv_proto(&get_info_response).await else {
409+
let Some(uv_proto) = select_uv_proto(
410+
#[cfg(test)]
411+
self.get_forced_pin_protocol(),
412+
&get_info_response,
413+
)
414+
.await
415+
else {
410416
error!("No supported PIN/UV auth protocols found");
411417
return Err(Error::Ctap(CtapError::Other));
412418
};

libwebauthn/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod basic_ctap1;
22
pub mod basic_ctap2;
3+
pub mod pin_protocols;
34
pub mod prf;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::time::Duration;
2+
3+
use crate::pin::PinManagement;
4+
use crate::proto::ctap2::Ctap2PinUvAuthProtocol;
5+
use crate::transport::hid::get_virtual_device;
6+
use crate::transport::{Channel, Device};
7+
use crate::UvUpdate;
8+
use test_log::test;
9+
use tokio::sync::broadcast::error::TryRecvError;
10+
use tokio::sync::broadcast::Receiver;
11+
12+
const TIMEOUT: Duration = Duration::from_secs(10);
13+
14+
async fn handle_updates(mut state_recv: Receiver<UvUpdate>) {
15+
let Ok(UvUpdate::PinRequired(p)) = state_recv.recv().await else {
16+
panic!("Did not receive PinRequired UvUpdate!");
17+
};
18+
p.send_pin("1234").expect("Failed to send first PIN");
19+
}
20+
21+
#[test(tokio::test)]
22+
async fn test_webauthn_change_pin_once() {
23+
let protos = [Ctap2PinUvAuthProtocol::One, Ctap2PinUvAuthProtocol::Two];
24+
for proto in protos {
25+
let mut device = get_virtual_device();
26+
let mut channel = device.channel().await.unwrap();
27+
28+
let mut state_recv = channel.get_ux_update_receiver();
29+
30+
channel.set_forced_pin_protocol(proto);
31+
32+
channel
33+
.change_pin(String::from("1234"), TIMEOUT)
34+
.await
35+
.unwrap();
36+
37+
assert_eq!(state_recv.try_recv(), Err(TryRecvError::Empty))
38+
}
39+
}
40+
41+
#[test(tokio::test)]
42+
async fn test_webauthn_change_pin_twice() {
43+
let protos = [Ctap2PinUvAuthProtocol::One, Ctap2PinUvAuthProtocol::Two];
44+
for proto in protos {
45+
let mut device = get_virtual_device();
46+
let mut channel = device.channel().await.unwrap();
47+
48+
let state_recv = channel.get_ux_update_receiver();
49+
let update_handle = tokio::spawn(handle_updates(state_recv));
50+
51+
channel.set_forced_pin_protocol(proto);
52+
53+
channel
54+
.change_pin(String::from("1234"), TIMEOUT)
55+
.await
56+
.unwrap();
57+
58+
channel
59+
.change_pin(String::from("4321"), TIMEOUT)
60+
.await
61+
.unwrap();
62+
63+
// Keeping the update-recv alive until the end to check all updates
64+
update_handle.await.unwrap();
65+
}
66+
}

libwebauthn/src/tests/prf.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::ops::webauthn::{
77
PRFValue,
88
};
99
use crate::pin::PinManagement;
10-
use crate::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
10+
use crate::proto::ctap2::{Ctap2PinUvAuthProtocol, Ctap2PublicKeyCredentialDescriptor};
1111
use crate::transport::hid::channel::HidChannel;
1212
use crate::transport::hid::get_virtual_device;
1313
use crate::transport::{Channel, Device};
@@ -44,6 +44,30 @@ async fn test_webauthn_prf_with_pin_set() {
4444
run_test_battery(&mut channel, true).await;
4545
}
4646

47+
#[test(tokio::test)]
48+
async fn test_webauthn_prf_with_pin_set_forced_pin_protocol_one() {
49+
let mut device = get_virtual_device();
50+
let mut channel = device.channel().await.unwrap();
51+
channel.set_forced_pin_protocol(Ctap2PinUvAuthProtocol::One);
52+
channel
53+
.change_pin(String::from("1234"), TIMEOUT)
54+
.await
55+
.unwrap();
56+
run_test_battery(&mut channel, true).await;
57+
}
58+
59+
#[test(tokio::test)]
60+
async fn test_webauthn_prf_with_pin_set_forced_pin_protocol_two() {
61+
let mut device = get_virtual_device();
62+
let mut channel = device.channel().await.unwrap();
63+
channel.set_forced_pin_protocol(Ctap2PinUvAuthProtocol::Two);
64+
channel
65+
.change_pin(String::from("1234"), TIMEOUT)
66+
.await
67+
.unwrap();
68+
run_test_battery(&mut channel, true).await;
69+
}
70+
4771
enum UvUpdateShim {
4872
PresenceRequired,
4973
PinRequired,

libwebauthn/src/transport/channel.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore {
6161
fn supports_preflight() -> bool {
6262
true
6363
}
64+
65+
// Default no-op implementations for these, as we currently only have a test device
66+
// for HidChannel, and that will override these default implementations.
67+
#[cfg(test)]
68+
fn set_forced_pin_protocol(&mut self, _protocols: Ctap2PinUvAuthProtocol) {}
69+
70+
#[cfg(test)]
71+
fn get_forced_pin_protocol(&mut self) -> Option<Ctap2PinUvAuthProtocol> {
72+
None
73+
}
6474
}
6575

6676
#[derive(Debug, Clone, PartialEq, Eq)]

libwebauthn/src/transport/hid/channel.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use tracing::{debug, info, instrument, trace, warn, Level};
1818
use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse};
1919
use crate::proto::ctap1::{Ctap1, Ctap1RegisterRequest};
2020
use crate::proto::ctap2::cbor::{CborRequest, CborResponse};
21+
#[cfg(test)]
22+
use crate::proto::ctap2::Ctap2PinUvAuthProtocol;
2123
use crate::proto::ctap2::{Ctap2, Ctap2MakeCredentialRequest};
2224
use crate::proto::CtapError;
2325
use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore};
@@ -72,6 +74,8 @@ pub struct HidChannel<'d> {
7274
auth_token_data: Option<AuthTokenData>,
7375
ux_update_sender: broadcast::Sender<UvUpdate>,
7476
handle: HidChannelHandle,
77+
#[cfg(test)]
78+
pin_protocol_override: Option<Ctap2PinUvAuthProtocol>,
7579
}
7680

7781
impl<'d> HidChannel<'d> {
@@ -97,6 +101,8 @@ impl<'d> HidChannel<'d> {
97101
auth_token_data: None,
98102
ux_update_sender,
99103
handle,
104+
#[cfg(test)]
105+
pin_protocol_override: None,
100106
};
101107
channel.init = channel.init(INIT_TIMEOUT).await?;
102108
Ok(channel)
@@ -499,6 +505,16 @@ impl Channel for HidChannel<'_> {
499505
fn get_ux_update_sender(&self) -> &broadcast::Sender<UvUpdate> {
500506
&self.ux_update_sender
501507
}
508+
509+
#[cfg(test)]
510+
fn set_forced_pin_protocol(&mut self, protocols: Ctap2PinUvAuthProtocol) {
511+
self.pin_protocol_override = Some(protocols);
512+
}
513+
514+
#[cfg(test)]
515+
fn get_forced_pin_protocol(&mut self) -> Option<Ctap2PinUvAuthProtocol> {
516+
self.pin_protocol_override
517+
}
502518
}
503519

504520
#[derive(Debug, Clone, Copy, Default)]

libwebauthn/src/webauthn/pin_uv_auth_token.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ pub(crate) enum UsedPinUvAuthToken {
2929
}
3030

3131
pub(crate) async fn select_uv_proto(
32+
#[cfg(test)] override_protocol: Option<Ctap2PinUvAuthProtocol>,
3233
get_info_response: &Ctap2GetInfoResponse,
3334
) -> Option<Box<dyn PinUvAuthProtocol>> {
35+
#[cfg(test)]
36+
if let Some(proto) = override_protocol {
37+
return Some(proto.create_protocol_object());
38+
}
3439
for &protocol in get_info_response.pin_auth_protos.iter().flatten() {
3540
match protocol {
3641
1 => return Some(Box::new(PinUvAuthProtocolOne::new())),
@@ -56,8 +61,12 @@ where
5661
{
5762
let get_info_response = channel.ctap2_get_info().await?;
5863
ctap2_request.handle_legacy_preview(&get_info_response);
59-
let maybe_uv_proto = select_uv_proto(&get_info_response).await;
60-
64+
let maybe_uv_proto = select_uv_proto(
65+
#[cfg(test)]
66+
channel.get_forced_pin_protocol(),
67+
&get_info_response,
68+
)
69+
.await;
6170
if let Some(uv_proto) = maybe_uv_proto {
6271
let token_identifier = Ctap2AuthTokenPermission::new(
6372
uv_proto.version(),
@@ -161,7 +170,13 @@ where
161170
uv_operation = Ctap2UserVerificationOperation::OnlyForSharedSecret;
162171
}
163172

164-
let Some(uv_proto) = select_uv_proto(get_info_response).await else {
173+
let Some(uv_proto) = select_uv_proto(
174+
#[cfg(test)]
175+
channel.get_forced_pin_protocol(),
176+
get_info_response,
177+
)
178+
.await
179+
else {
165180
error!("No supported PIN/UV auth protocols found");
166181
return Err(Error::Ctap(CtapError::Other));
167182
};

0 commit comments

Comments
 (0)