Skip to content

Commit ba35dde

Browse files
fix(webauthn): base64url-decode excludeCredentials ids (#242)
During registration the excludeCredentials ids were stored as the raw text of the base64url string rather than the decoded bytes. The ids in the request never matched the credentials an authenticator already held, so duplicate-credential prevention was silently bypassed and a second credential could be created on a key that already had one. This decodes the ids the same way the authentication path already does, and treats an omitted excludeCredentials as an empty list. Includes regression tests asserting the ids decode to the expected bytes.
1 parent 3a440ad commit ba35dde

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

libwebauthn/src/ops/webauthn/idl/create.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
use super::get::PublicKeyCredentialDescriptorJSON;
12
use super::Base64UrlString;
23
use crate::{
34
ops::webauthn::{
45
MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement,
56
},
6-
proto::ctap2::{
7-
Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
8-
},
7+
proto::ctap2::{Ctap2CredentialType, Ctap2PublicKeyCredentialRpEntity},
98
};
109

1110
use serde::Deserialize;
@@ -46,10 +45,44 @@ pub struct PublicKeyCredentialCreationOptionsJSON {
4645
#[serde(rename = "pubKeyCredParams")]
4746
pub params: Vec<Ctap2CredentialType>,
4847
pub timeout: Option<u32>,
49-
pub exclude_credentials: Vec<Ctap2PublicKeyCredentialDescriptor>,
48+
#[serde(default)]
49+
pub exclude_credentials: Vec<PublicKeyCredentialDescriptorJSON>,
5050
pub authenticator_selection: Option<AuthenticatorSelectionCriteria>,
5151
pub hints: Option<Vec<String>>,
5252
pub attestation: Option<String>,
5353
pub attestation_formats: Option<Vec<String>>,
5454
pub extensions: Option<MakeCredentialsRequestExtensions>,
5555
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use super::*;
60+
use crate::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
61+
62+
#[test]
63+
fn exclude_credentials_id_is_base64url_decoded() {
64+
let json = r#"{
65+
"rp": { "id": "example.org", "name": "Example" },
66+
"user": { "id": "YWxpY2U", "name": "alice", "displayName": "Alice" },
67+
"challenge": "Y2hhbGxlbmdl",
68+
"pubKeyCredParams": [{ "type": "public-key", "alg": -7 }],
69+
"excludeCredentials": [{ "type": "public-key", "id": "AQIDBA" }]
70+
}"#;
71+
let options: PublicKeyCredentialCreationOptionsJSON = serde_json::from_str(json).unwrap();
72+
let descriptor: Ctap2PublicKeyCredentialDescriptor =
73+
options.exclude_credentials[0].clone().into();
74+
assert_eq!(descriptor.id, [1u8, 2, 3, 4]);
75+
}
76+
77+
#[test]
78+
fn exclude_credentials_defaults_to_empty_when_omitted() {
79+
let json = r#"{
80+
"rp": { "id": "example.org", "name": "Example" },
81+
"user": { "id": "YWxpY2U", "name": "alice", "displayName": "Alice" },
82+
"challenge": "Y2hhbGxlbmdl",
83+
"pubKeyCredParams": [{ "type": "public-key", "alg": -7 }]
84+
}"#;
85+
let options: PublicKeyCredentialCreationOptionsJSON = serde_json::from_str(json).unwrap();
86+
assert!(options.exclude_credentials.is_empty());
87+
}
88+
}

libwebauthn/src/ops/webauthn/make_credential.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,13 @@ impl FromIdlModel<PublicKeyCredentialCreationOptionsJSON> for MakeCredentialRequ
422422
exclude: if inner.exclude_credentials.is_empty() {
423423
None
424424
} else {
425-
Some(inner.exclude_credentials)
425+
Some(
426+
inner
427+
.exclude_credentials
428+
.into_iter()
429+
.map(|c| c.into())
430+
.collect(),
431+
)
426432
},
427433
extensions: inner.extensions,
428434
timeout,

0 commit comments

Comments
 (0)