Skip to content

Commit e00499c

Browse files
feat(webauthn): support appid and appidExclude extensions (#204)
- Add appid and appidExclude inputs to the IDL types - Plumb appid through GetAssertionRequest and the U2F downgrade path - Preflight excludeList against appidExclude; signal appid in U2F output
1 parent 201d527 commit e00499c

9 files changed

Lines changed: 455 additions & 70 deletions

File tree

libwebauthn-tests/tests/preflight.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use libwebauthn::ops::webauthn::{
44
GetAssertionRequest, GetAssertionResponse, MakeCredentialRequest, ResidentKeyRequirement,
55
UserVerificationRequirement,
66
};
7-
use libwebauthn::proto::ctap2::preflight::ctap2_preflight;
7+
use libwebauthn::proto::ctap2::preflight::{ctap2_preflight, ctap2_preflight_with_appid};
88
use libwebauthn::proto::ctap2::{
99
Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
1010
Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity,
@@ -36,12 +36,21 @@ async fn make_credential_call(
3636
channel: &mut HidChannel<'_>,
3737
user_id: &[u8],
3838
exclude_list: Option<Vec<Ctap2PublicKeyCredentialDescriptor>>,
39+
) -> Result<(Ctap2PublicKeyCredentialDescriptor, [u8; 32]), Error> {
40+
make_credential_call_with_rp(channel, user_id, exclude_list, "example.org").await
41+
}
42+
43+
async fn make_credential_call_with_rp(
44+
channel: &mut HidChannel<'_>,
45+
user_id: &[u8],
46+
exclude_list: Option<Vec<Ctap2PublicKeyCredentialDescriptor>>,
47+
rp_id: &str,
3948
) -> Result<(Ctap2PublicKeyCredentialDescriptor, [u8; 32]), Error> {
4049
let challenge: [u8; 32] = thread_rng().gen();
4150
let make_credentials_request = MakeCredentialRequest {
42-
origin: "example.org".to_owned(),
51+
origin: rp_id.to_owned(),
4352
challenge: Vec::from(challenge),
44-
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
53+
relying_party: Ctap2PublicKeyCredentialRpEntity::new(rp_id, rp_id),
4554
user: Ctap2PublicKeyCredentialUserEntity::new(user_id, "mario.rossi", "Mario Rossi"),
4655
resident_key: Some(ResidentKeyRequirement::Discouraged),
4756
user_verification: UserVerificationRequirement::Preferred,
@@ -239,6 +248,58 @@ async fn preflight_nonsense_allow_list() {
239248
.await;
240249
}
241250

251+
#[test(tokio::test)]
252+
async fn preflight_with_appid_exclude_finds_legacy_credential() {
253+
// Register a credential under a "legacy" relying party id (standing
254+
// in for a U2F AppID), then run preflight against a different rpId
255+
// while passing the legacy rpId as `appid_exclude`. The credential
256+
// should be detected, matching WebAuthn L3 §10.1.2 semantics.
257+
let mut device = get_virtual_device();
258+
let mut channel = device.channel().await.unwrap();
259+
260+
let user_id: [u8; 32] = thread_rng().gen();
261+
let _state_recv = channel.get_ux_update_receiver();
262+
263+
// First, register the legacy credential.
264+
let (legacy_credential, _) =
265+
make_credential_call_with_rp(&mut channel, &user_id, None, "legacy.example.org")
266+
.await
267+
.expect("Failed to register legacy credential");
268+
269+
// Preflight under "example.org" without appid_exclude: legacy
270+
// credential is not detected.
271+
let hash: [u8; 32] = thread_rng().gen();
272+
let filtered_no_appid = ctap2_preflight_with_appid(
273+
&mut channel,
274+
std::slice::from_ref(&legacy_credential),
275+
&hash,
276+
"example.org",
277+
None,
278+
)
279+
.await;
280+
assert!(
281+
filtered_no_appid.is_empty(),
282+
"Without appid_exclude, the legacy credential should not be detected"
283+
);
284+
285+
// Preflight again, this time providing the legacy rpId as
286+
// appid_exclude. The credential must be detected.
287+
let filtered_with_appid = ctap2_preflight_with_appid(
288+
&mut channel,
289+
std::slice::from_ref(&legacy_credential),
290+
&hash,
291+
"example.org",
292+
Some("legacy.example.org"),
293+
)
294+
.await;
295+
assert_eq!(
296+
filtered_with_appid.len(),
297+
1,
298+
"With appid_exclude set, the legacy credential should be detected"
299+
);
300+
assert_eq!(filtered_with_appid[0].id, legacy_credential.id);
301+
}
302+
242303
#[test(tokio::test)]
243304
async fn preflight_mixed_allow_list() {
244305
// Get assertion with a mixed allow_list that contains 2 real ones. Should remove the two fake ones in preflight

libwebauthn/examples/features/webauthn_extensions_hid.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
4343
hmac_create_secret: Some(true),
4444
prf: None,
4545
cred_props: Some(true),
46+
appid_exclude: None,
4647
};
4748

4849
for mut device in devices {

0 commit comments

Comments
 (0)