Skip to content

Commit d345b3d

Browse files
fix(ctap2): tolerate unknown authenticator transports in descriptors
1 parent dca2054 commit d345b3d

3 files changed

Lines changed: 130 additions & 4 deletions

File tree

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,40 @@ mod tests {
838838
assert_eq!(req, request_base());
839839
}
840840

841+
#[tokio::test]
842+
async fn test_request_from_json_passes_through_unknown_transports() {
843+
use crate::proto::ctap2::Ctap2Transport;
844+
845+
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();
846+
let req_json = json_field_add(
847+
REQUEST_BASE_JSON,
848+
"allowCredentials",
849+
r#"[{"type":"public-key","id":"bXktY3JlZGVudGlhbC1pZA","transports":["usb","smart-card","future-transport"]}]"#,
850+
);
851+
852+
let req: GetAssertionRequest = from_json(
853+
&request_origin,
854+
&MockPublicSuffixList,
855+
RelatedOrigins::Disabled,
856+
&req_json,
857+
)
858+
.await
859+
.unwrap();
860+
861+
let transports = req.allow[0]
862+
.transports
863+
.as_ref()
864+
.expect("transports preserved");
865+
assert_eq!(
866+
transports,
867+
&vec![
868+
Ctap2Transport::Usb,
869+
Ctap2Transport::SmartCard,
870+
Ctap2Transport::Other("future-transport".to_string()),
871+
]
872+
);
873+
}
874+
841875
#[tokio::test]
842876
async fn test_request_from_json_ignore_missing_rp_id() {
843877
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();

libwebauthn/src/proto/ctap1/model.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ impl TryFrom<&Ctap2Transport> for Ctap1Transport {
2525
Ctap2Transport::Ble => Ok(Ctap1Transport::Ble),
2626
Ctap2Transport::Usb => Ok(Ctap1Transport::Usb),
2727
Ctap2Transport::Nfc => Ok(Ctap1Transport::Nfc),
28-
Ctap2Transport::Internal => Err(CtapError::UnsupportedOption),
29-
Ctap2Transport::Hybrid => Err(CtapError::UnsupportedOption),
28+
_ => Err(CtapError::UnsupportedOption),
3029
}
3130
}
3231
}
@@ -353,4 +352,42 @@ mod tests {
353352
assert_eq!(decoded.attestation, hex::decode("3082015930820100A003020102020102300A06082A8648CE3D0403023028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653022180F32303030303130313030303030305A180F32303939313233313233353935395A3028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653059301306072A8648CE3D020106082A8648CE3D030107034200040393AF897BE858E88C1953876A1A538477C4DA6E6EA14ACF0A2FD89A4DCCF95878A8CD2929029CC1D794BFFB9C37547CBBB5BB31AB3A6756ACF74F123CECD45CA31730153013060B2B0601040182E51C020101040403020470300A06082A8648CE3D040302034700304402207F958ABE6CF08CB2E9A03774D52DF8C0EA261E1AC0C283409FEDD8D36DFAF09302204EEB7501C720428D206E1B092D8D26CA8536B70F5F09AEA99562390BEF1BA7EC").unwrap());
354353
assert_eq!(decoded.signature, hex::decode("3044022031413D6E238A5F998B26B3931655C411847D99776B6E5CF15AA2E11BFAF325F00220098745DA82C11BB242934BAC6AE95155EAAD68520D695D46982DA9B2C94F94E3").unwrap());
355354
}
355+
356+
#[test]
357+
fn known_ctap2_transports_downgrade_to_ctap1() {
358+
use crate::proto::ctap1::Ctap1Transport;
359+
use crate::proto::ctap2::Ctap2Transport;
360+
361+
assert!(matches!(
362+
Ctap1Transport::try_from(&Ctap2Transport::Usb),
363+
Ok(Ctap1Transport::Usb)
364+
));
365+
assert!(matches!(
366+
Ctap1Transport::try_from(&Ctap2Transport::Ble),
367+
Ok(Ctap1Transport::Ble)
368+
));
369+
assert!(matches!(
370+
Ctap1Transport::try_from(&Ctap2Transport::Nfc),
371+
Ok(Ctap1Transport::Nfc)
372+
));
373+
}
374+
375+
#[test]
376+
fn unsupported_ctap2_transports_fail_to_downgrade() {
377+
use crate::proto::ctap1::Ctap1Transport;
378+
use crate::proto::ctap2::Ctap2Transport;
379+
use crate::webauthn::CtapError;
380+
381+
for transport in [
382+
Ctap2Transport::Internal,
383+
Ctap2Transport::Hybrid,
384+
Ctap2Transport::SmartCard,
385+
Ctap2Transport::Other("future-transport".to_string()),
386+
] {
387+
assert!(matches!(
388+
Ctap1Transport::try_from(&transport),
389+
Err(CtapError::UnsupportedOption)
390+
));
391+
}
392+
}
356393
}

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,54 @@ pub enum Ctap2PublicKeyCredentialType {
147147
Unknown,
148148
}
149149

150-
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
151-
#[serde(rename_all = "lowercase")]
150+
/// AuthenticatorTransport from a credential descriptor. Unknown values are kept in `Other` so they pass through unchanged.
151+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152+
#[serde(from = "String", into = "String")]
152153
pub enum Ctap2Transport {
153154
Ble,
154155
Nfc,
155156
Usb,
156157
Internal,
157158
Hybrid,
159+
SmartCard,
160+
Other(String),
161+
}
162+
163+
impl Ctap2Transport {
164+
fn as_str(&self) -> &str {
165+
match self {
166+
Self::Ble => "ble",
167+
Self::Nfc => "nfc",
168+
Self::Usb => "usb",
169+
Self::Internal => "internal",
170+
Self::Hybrid => "hybrid",
171+
Self::SmartCard => "smart-card",
172+
Self::Other(value) => value,
173+
}
174+
}
175+
}
176+
177+
impl From<String> for Ctap2Transport {
178+
fn from(value: String) -> Self {
179+
match value.as_str() {
180+
"ble" => Self::Ble,
181+
"nfc" => Self::Nfc,
182+
"usb" => Self::Usb,
183+
"internal" => Self::Internal,
184+
"hybrid" => Self::Hybrid,
185+
"smart-card" => Self::SmartCard,
186+
_ => Self::Other(value),
187+
}
188+
}
189+
}
190+
191+
impl From<Ctap2Transport> for String {
192+
fn from(transport: Ctap2Transport) -> Self {
193+
match transport {
194+
Ctap2Transport::Other(value) => value,
195+
known => known.as_str().to_owned(),
196+
}
197+
}
158198
}
159199

160200
impl From<&Ctap1Transport> for Ctap2Transport {
@@ -507,4 +547,19 @@ mod tests {
507547
);
508548
assert!(Ctap2COSEAlgorithmIdentifier::ESP256.is_known());
509549
}
550+
551+
#[test]
552+
fn unrecognised_transport_roundtrips_through_cbor() {
553+
let value = Ctap2Transport::Other("future-transport".to_string());
554+
let encoded = serde_cbor::to_vec(&value).unwrap();
555+
let decoded: Ctap2Transport = serde_cbor::from_slice(&encoded).unwrap();
556+
assert_eq!(decoded, value);
557+
}
558+
559+
#[test]
560+
fn smart_card_transport_roundtrips_through_cbor() {
561+
let encoded = serde_cbor::to_vec(&Ctap2Transport::SmartCard).unwrap();
562+
let decoded: Ctap2Transport = serde_cbor::from_slice(&encoded).unwrap();
563+
assert_eq!(decoded, Ctap2Transport::SmartCard);
564+
}
510565
}

0 commit comments

Comments
 (0)