Skip to content

Commit 6bfea1f

Browse files
authored
Add more UV-negotiation tests (#149)
This PR adds another layer of testing opportunities, by introducing a `TestChannel` that can be pre-filled with expected requests and associated responses it should give. I have used this to test `user_verification(..)` in various states of complexity, and discovered a few bugs along the way. Notable changes to make this possible: - `PinUvAuthProtocol`s get a deterministic private key, to 'predict' crypto results (for testing only!) - Some (the ones I needed) Ctap2-response structs got a `#[cfg_attr(test, derive(SerializeIndexed))]` to serialize them to CBOR, before sending it back as if it were a real message from a device. This should be easy to extend to other response structs, once they are needed. - Removed a second, subsequent `GetInfo`-call, by simply reusing the result of the first call in `user_verification(..)` - Fixed some corner cases in `user_verification(..)`, mostly around the "shared secret only"-scenarios - Renamed `ClientPinOnlyForSharedSecret` to `OnlyForSharedSecret` as this is not bound to `ClientPin` anymore - Added loads of tests. Still not exhaustive, though. More should be written around full ceremony scenarios (I'll open tickets for some of those, once this lands).
1 parent e0f88c4 commit 6bfea1f

File tree

11 files changed

+792
-48
lines changed

11 files changed

+792
-48
lines changed

libwebauthn/src/ops/webauthn.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ pub enum UserVerificationRequirement {
2626
}
2727

2828
impl UserVerificationRequirement {
29+
/// Check if user verification is discouraged
30+
pub fn is_discouraged(&self) -> bool {
31+
match self {
32+
Self::Required | Self::Preferred => false,
33+
Self::Discouraged => true,
34+
}
35+
}
36+
2937
/// Check if user verification is preferred or required for this request
3038
pub fn is_preferred(&self) -> bool {
3139
match self {

libwebauthn/src/pin.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use p256::{
1010
ecdh::EphemeralSecret, elliptic_curve::sec1::FromEncodedPoint, EncodedPoint,
1111
PublicKey as P256PublicKey,
1212
};
13-
use rand::{rngs::OsRng, thread_rng, Rng};
13+
use rand::{rngs::OsRng, thread_rng, Rng, SeedableRng};
1414
use sha2::{Digest, Sha256};
1515
use tracing::{error, instrument, warn};
1616
use x509_parser::nom::AsBytes;
@@ -97,7 +97,14 @@ pub struct PinUvAuthProtocolOne {
9797

9898
impl PinUvAuthProtocolOne {
9999
pub fn new() -> Self {
100-
let private_key = EphemeralSecret::random(&mut OsRng);
100+
let private_key = if cfg!(test) {
101+
// For testing only!!
102+
// We need a deterministic, seedable RNG to be able to "predict" crypto-operations
103+
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
104+
EphemeralSecret::random(&mut rng)
105+
} else {
106+
EphemeralSecret::random(&mut OsRng)
107+
};
101108
let public_key = private_key.public_key();
102109
Self {
103110
private_key,
@@ -247,7 +254,14 @@ pub struct PinUvAuthProtocolTwo {
247254

248255
impl PinUvAuthProtocolTwo {
249256
pub fn new() -> Self {
250-
let private_key = EphemeralSecret::random(&mut OsRng);
257+
let private_key = if cfg!(test) {
258+
// For testing only!!
259+
// We need a deterministic, seedable RNG to be able to "predict" crypto-operations
260+
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
261+
EphemeralSecret::random(&mut rng)
262+
} else {
263+
EphemeralSecret::random(&mut OsRng)
264+
};
251265
let public_key = private_key.public_key();
252266
Self {
253267
private_key,

libwebauthn/src/proto/ctap2/cbor/request.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::proto::ctap2::Ctap2AuthenticatorConfigRequest;
99
use crate::proto::ctap2::Ctap2BioEnrollmentRequest;
1010
use crate::proto::ctap2::Ctap2CredentialManagementRequest;
1111

12-
#[derive(Debug, Clone)]
12+
#[derive(Debug, Clone, PartialEq)]
1313
pub struct CborRequest {
1414
pub command: Ctap2CommandCode,
1515
pub encoded_data: Vec<u8>,
@@ -18,7 +18,7 @@ pub struct CborRequest {
1818
impl CborRequest {
1919
pub fn new(command: Ctap2CommandCode) -> Self {
2020
Self {
21-
command: command,
21+
command,
2222
encoded_data: vec![],
2323
}
2424
}

libwebauthn/src/proto/ctap2/mod.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ mod model;
66
mod protocol;
77

88
pub use model::Ctap2GetInfoResponse;
9+
#[cfg(test)]
10+
pub use model::Ctap2PinUvAuthProtocolCommand;
911
pub use model::{
1012
Ctap2AttestationStatement, Ctap2AuthTokenPermissionRole, Ctap2COSEAlgorithmIdentifier,
11-
Ctap2ClientPinRequest, Ctap2CommandCode, Ctap2CredentialType, Ctap2MakeCredentialOptions,
12-
Ctap2PinUvAuthProtocol, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
13-
Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity, Ctap2Transport,
14-
Ctap2UserVerifiableRequest, Ctap2UserVerificationOperation, FidoU2fAttestationStmt,
13+
Ctap2ClientPinRequest, Ctap2ClientPinResponse, Ctap2CommandCode, Ctap2CredentialType,
14+
Ctap2MakeCredentialOptions, Ctap2PinUvAuthProtocol, Ctap2PublicKeyCredentialDescriptor,
15+
Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialType,
16+
Ctap2PublicKeyCredentialUserEntity, Ctap2Transport, Ctap2UserVerifiableRequest,
17+
Ctap2UserVerificationOperation, FidoU2fAttestationStmt,
1518
};
19+
1620
pub use model::{
1721
Ctap2AuthenticatorConfigCommand, Ctap2AuthenticatorConfigParams,
1822
Ctap2AuthenticatorConfigRequest,

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ pub use authenticator_config::{
1919
Ctap2AuthenticatorConfigRequest,
2020
};
2121
mod client_pin;
22+
#[cfg(test)]
23+
pub use client_pin::Ctap2PinUvAuthProtocolCommand;
2224
pub use client_pin::{
2325
Ctap2AuthTokenPermissionRole, Ctap2ClientPinRequest, Ctap2ClientPinResponse,
2426
Ctap2PinUvAuthProtocol,
2527
};
28+
2629
mod make_credential;
2730
pub use make_credential::{
2831
Ctap2MakeCredentialOptions, Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse,
@@ -218,7 +221,7 @@ pub enum Ctap2UserVerificationOperation {
218221
GetPinUvAuthTokenUsingUvWithPermissions,
219222
GetPinUvAuthTokenUsingPinWithPermissions,
220223
GetPinToken,
221-
ClientPinOnlyForSharedSecret,
224+
OnlyForSharedSecret,
222225
LegacyUv,
223226
}
224227

libwebauthn/src/proto/ctap2/model/client_pin.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ pub enum Ctap2PinUvAuthProtocolCommand {
221221
GetPinUvAuthTokenUsingPinWithPermissions = 0x09,
222222
}
223223

224+
#[cfg_attr(test, derive(SerializeIndexed))]
224225
#[derive(Debug, Clone, Default, DeserializeIndexed)]
225226
pub struct Ctap2ClientPinResponse {
226227
/// keyAgreement (0x01)

libwebauthn/src/proto/ctap2/model/get_info.rs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ use std::collections::HashMap;
22

33
use serde_bytes::ByteBuf;
44
use serde_indexed::DeserializeIndexed;
5+
#[cfg(test)]
6+
use serde_indexed::SerializeIndexed;
57
use tracing::debug;
68

79
use super::{Ctap2CredentialType, Ctap2UserVerificationOperation};
810

11+
#[cfg_attr(test, derive(SerializeIndexed))]
912
#[derive(Debug, Clone, DeserializeIndexed, Default)]
1013
pub struct Ctap2GetInfoResponse {
1114
/// versions (0x01)
@@ -227,7 +230,7 @@ impl Ctap2GetInfoResponse {
227230
// clientPIN exists, but is not enabled, aka PIN is not yet set on the device
228231
// We can use it for establishing a shared secret, but not for creating a pinUvAuthToken
229232
if self.option_exists("clientPin") && !self.option_enabled("clientPin") {
230-
return Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret);
233+
return Some(Ctap2UserVerificationOperation::OnlyForSharedSecret);
231234
}
232235

233236
// clientPin is not enabled (not supported or Pin not set) and
@@ -238,7 +241,7 @@ impl Ctap2GetInfoResponse {
238241
&& self.option_exists("uv")
239242
&& self.option_enabled("pinUvAuthToken")
240243
{
241-
return Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret);
244+
return Some(Ctap2UserVerificationOperation::OnlyForSharedSecret);
242245
}
243246

244247
// If we do have a PIN, check if we need to use legacy getPinToken or new getPinUvAuthToken..-command
@@ -355,11 +358,11 @@ mod test {
355358
assert!(info.can_establish_shared_secret());
356359
assert_eq!(
357360
info.uv_operation(false),
358-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
361+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
359362
);
360363
assert_eq!(
361364
info.uv_operation(true),
362-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
365+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
363366
);
364367
}
365368

@@ -410,7 +413,7 @@ mod test {
410413
);
411414
assert_eq!(
412415
info.uv_operation(true),
413-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
416+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
414417
);
415418
}
416419

@@ -425,11 +428,11 @@ mod test {
425428
assert!(info.can_establish_shared_secret());
426429
assert_eq!(
427430
info.uv_operation(false),
428-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
431+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
429432
);
430433
assert_eq!(
431434
info.uv_operation(true),
432-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
435+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
433436
);
434437
}
435438

@@ -463,7 +466,7 @@ mod test {
463466
);
464467
assert_eq!(
465468
info.uv_operation(true),
466-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
469+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
467470
);
468471
}
469472

@@ -495,11 +498,11 @@ mod test {
495498
assert!(info.can_establish_shared_secret());
496499
assert_eq!(
497500
info.uv_operation(false),
498-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
501+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
499502
);
500503
assert_eq!(
501504
info.uv_operation(true),
502-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
505+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
503506
);
504507
}
505508

@@ -513,11 +516,11 @@ mod test {
513516
assert!(info.can_establish_shared_secret());
514517
assert_eq!(
515518
info.uv_operation(false),
516-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
519+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
517520
);
518521
assert_eq!(
519522
info.uv_operation(true),
520-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
523+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
521524
);
522525
}
523526

@@ -547,7 +550,7 @@ mod test {
547550
);
548551
assert_eq!(
549552
info.uv_operation(true),
550-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
553+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
551554
);
552555
}
553556

@@ -564,11 +567,11 @@ mod test {
564567
assert!(info.can_establish_shared_secret());
565568
assert_eq!(
566569
info.uv_operation(false),
567-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
570+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
568571
);
569572
assert_eq!(
570573
info.uv_operation(true),
571-
Some(Ctap2UserVerificationOperation::ClientPinOnlyForSharedSecret)
574+
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
572575
);
573576
}
574577
}

libwebauthn/src/tests/channel.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use async_trait::async_trait;
2+
use std::{collections::VecDeque, fmt::Display, time::Duration};
3+
use tokio::sync::broadcast;
4+
5+
use crate::{
6+
proto::{
7+
ctap1::apdu::{ApduRequest, ApduResponse},
8+
ctap2::cbor::{CborRequest, CborResponse},
9+
},
10+
transport::{
11+
device::SupportedProtocols, AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore,
12+
},
13+
webauthn::Error,
14+
UvUpdate,
15+
};
16+
17+
pub struct TestChannel {
18+
expected_requests: VecDeque<CborRequest>,
19+
responses: VecDeque<CborResponse>,
20+
auth_token_data: Option<AuthTokenData>,
21+
ux_update_sender: broadcast::Sender<UvUpdate>,
22+
}
23+
24+
impl TestChannel {
25+
pub fn new() -> Self {
26+
let (ux_update_sender, _) = broadcast::channel(16);
27+
Self {
28+
expected_requests: VecDeque::new(),
29+
responses: VecDeque::new(),
30+
auth_token_data: None,
31+
ux_update_sender,
32+
}
33+
}
34+
35+
pub fn push_command_pair(&mut self, expected_request: CborRequest, response: CborResponse) {
36+
self.expected_requests.push_front(expected_request);
37+
self.responses.push_front(response);
38+
}
39+
}
40+
41+
impl Ctap2AuthTokenStore for TestChannel {
42+
fn store_auth_data(&mut self, auth_token_data: AuthTokenData) {
43+
self.auth_token_data = Some(auth_token_data);
44+
}
45+
46+
fn get_auth_data(&self) -> Option<&AuthTokenData> {
47+
self.auth_token_data.as_ref()
48+
}
49+
50+
fn clear_uv_auth_token_store(&mut self) {
51+
self.auth_token_data = None;
52+
}
53+
}
54+
55+
impl Display for TestChannel {
56+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57+
write!(f, "TestChannel")
58+
}
59+
}
60+
61+
#[async_trait]
62+
impl Channel for TestChannel {
63+
type UxUpdate = UvUpdate;
64+
65+
fn get_ux_update_sender(&self) -> &broadcast::Sender<Self::UxUpdate> {
66+
&self.ux_update_sender
67+
}
68+
69+
async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
70+
Ok(SupportedProtocols {
71+
u2f: false,
72+
fido2: true,
73+
})
74+
}
75+
async fn status(&self) -> ChannelStatus {
76+
unimplemented!();
77+
}
78+
async fn close(&mut self) {
79+
unimplemented!();
80+
}
81+
82+
async fn apdu_send(&self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> {
83+
unimplemented!();
84+
}
85+
async fn apdu_recv(&self, _timeout: Duration) -> Result<ApduResponse, Error> {
86+
unimplemented!();
87+
}
88+
89+
async fn cbor_send(&mut self, request: &CborRequest, _timeout: Duration) -> Result<(), Error> {
90+
let expected = self
91+
.expected_requests
92+
.pop_back()
93+
.expect("No expected request found, but one was sent");
94+
assert_eq!(
95+
&expected,
96+
request,
97+
"{} items still in the queue",
98+
self.expected_requests.len()
99+
);
100+
Ok(())
101+
}
102+
async fn cbor_recv(&mut self, _timeout: Duration) -> Result<CborResponse, Error> {
103+
let response = self
104+
.responses
105+
.pop_back()
106+
.expect("No response found, but one was requested");
107+
Ok(response)
108+
}
109+
}

libwebauthn/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod basic_ctap1;
22
pub mod basic_ctap2;
3+
pub mod channel;

libwebauthn/src/transport/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,9 @@ mod transport;
1212

1313
pub(crate) use channel::{AuthTokenData, Ctap2AuthTokenPermission};
1414
pub use channel::{Channel, Ctap2AuthTokenStore};
15+
16+
#[cfg(test)]
17+
pub use channel::ChannelStatus;
18+
1519
pub use device::Device;
1620
pub use transport::Transport;

0 commit comments

Comments
 (0)