Skip to content

Commit fce8da2

Browse files
[WIP] Web IDL support (make credentials); next: extension parsing
1 parent 5d3c9d2 commit fce8da2

File tree

8 files changed

+276
-8
lines changed

8 files changed

+276
-8
lines changed

libwebauthn/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ snow = { version = "0.10", features = ["use-p256"] }
6666
ctap-types = { version = "0.4.0" }
6767
btleplug = "0.11.7"
6868
thiserror = "2.0.12"
69+
serde_json = "1.0.141"
6970

7071

7172
[dev-dependencies]
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use super::idl::Base64UrlString;
2+
use crate::{
3+
ops::webauthn::{ResidentKeyRequirement, UserVerificationRequirement},
4+
proto::ctap2::{
5+
Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
6+
Ctap2PublicKeyCredentialUserEntity,
7+
},
8+
};
9+
10+
use serde::Deserialize;
11+
use serde_json::{Map as JsonMap, Value as JsonValue};
12+
13+
/**
14+
* https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
15+
*/
16+
17+
#[derive(Debug, Clone, Deserialize)]
18+
pub struct PublicKeyCredentialDescriptorJSON {
19+
pub r#type: String,
20+
pub id: Base64UrlString,
21+
pub transports: Vec<String>,
22+
}
23+
24+
#[derive(Debug, Clone, Deserialize)]
25+
pub struct PublicKeyCredentialParameters {
26+
pub r#type: String,
27+
pub alg: i32,
28+
}
29+
30+
#[derive(Debug, Clone, Deserialize)]
31+
pub struct AuthenticatorSelectionCriteria {
32+
#[serde(rename = "authenticatorAttachment")]
33+
pub authenticator_attachment: Option<String>,
34+
#[serde(rename = "residentKey")]
35+
pub resident_key: Option<ResidentKeyRequirement>,
36+
#[serde(rename = "requireResidentKey")]
37+
#[serde(default)]
38+
pub require_resident_key: bool,
39+
#[serde(rename = "userVerification")]
40+
#[serde(default = "default_user_verification")]
41+
pub user_verification: UserVerificationRequirement,
42+
}
43+
44+
fn default_user_verification() -> UserVerificationRequirement {
45+
UserVerificationRequirement::Preferred
46+
}
47+
48+
type JsonObject = JsonMap<String, JsonValue>;
49+
50+
#[derive(Debug, Clone, Deserialize)]
51+
pub struct PublicKeyCredentialCreationOptionsJSON {
52+
pub rp: Ctap2PublicKeyCredentialRpEntity,
53+
pub user: Ctap2PublicKeyCredentialUserEntity,
54+
pub challenge: Base64UrlString,
55+
#[serde(rename = "pubKeyCredParams")]
56+
pub params: Vec<Ctap2CredentialType>,
57+
pub timeout: u32,
58+
#[serde(rename = "excludeCredentials")]
59+
pub exclude_credentials: Vec<Ctap2PublicKeyCredentialDescriptor>,
60+
#[serde(rename = "authenticatorSelection")]
61+
pub authenticator_selection: Option<AuthenticatorSelectionCriteria>,
62+
pub hints: Vec<String>,
63+
pub attestation: String,
64+
#[serde(rename = "attestationFormats")]
65+
pub attestation_formats: Vec<String>,
66+
pub extensions: JsonObject,
67+
}

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use super::idl::WebAuthnIDL;
2+
13
use std::{collections::HashMap, time::Duration};
24

35
use serde::{Deserialize, Serialize};
@@ -6,6 +8,11 @@ use tracing::{debug, error, trace};
68

79
use crate::{
810
fido::AuthenticatorData,
11+
ops::webauthn::{
12+
create::PublicKeyCredentialCreationOptionsJSON,
13+
idl::{FromInnerModel, JsonError},
14+
rpid::RelyingPartyId,
15+
},
916
pin::PinUvAuthProtocol,
1017
proto::ctap2::{
1118
Ctap2AttestationStatement, Ctap2GetAssertionResponseExtensions,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use base64_url;
2+
use serde::{de::DeserializeOwned, Deserialize, Serialize};
3+
use serde_json;
4+
5+
use super::rpid::RelyingPartyId;
6+
7+
pub type JsonError = serde_json::Error;
8+
9+
pub trait WebAuthnIDL<E>: Sized
10+
where
11+
E: std::error::Error, // Validation error type.
12+
Self: FromInnerModel<Self::InnerModel, E>,
13+
{
14+
/// An error type that can be returned when deserializing from JSON, including
15+
/// JSON parsing errors and any additional validation errors.
16+
type Error: std::error::Error + From<JsonError> + From<E>;
17+
18+
/// The JSON model that this IDL can deserialize from.
19+
type InnerModel: DeserializeOwned;
20+
21+
fn from_json(rpid: &RelyingPartyId, json: &str) -> Result<Self, Self::Error> {
22+
let inner_model: Self::InnerModel = serde_json::from_str(json)?;
23+
Self::from_inner_model(rpid, inner_model).map_err(From::from)
24+
}
25+
}
26+
27+
pub trait FromInnerModel<T, E>: Sized
28+
where
29+
T: DeserializeOwned,
30+
E: std::error::Error,
31+
{
32+
fn from_inner_model(rpid: &RelyingPartyId, inner: T) -> Result<Self, E>;
33+
}
34+
35+
// TODO(afresta): Move to ctap2 module.
36+
#[derive(Debug, Clone)]
37+
pub struct Base64UrlString(pub Vec<u8>);
38+
39+
impl<'de> Deserialize<'de> for Base64UrlString {
40+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41+
where
42+
D: serde::Deserializer<'de>,
43+
{
44+
let s: String = Deserialize::deserialize(deserializer)?;
45+
base64_url::decode(&s)
46+
.map_err(serde::de::Error::custom)
47+
.map(|bytes| Base64UrlString(bytes))
48+
}
49+
}
50+
51+
impl Serialize for Base64UrlString {
52+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53+
where
54+
S: serde::Serializer,
55+
{
56+
let encoded = base64_url::encode(&self.0);
57+
serializer.serialize_str(&encoded)
58+
}
59+
}
60+
61+
impl Into<Vec<u8>> for Base64UrlString {
62+
fn into(self) -> Vec<u8> {
63+
self.0
64+
}
65+
}
66+
67+
impl Base64UrlString {
68+
pub fn as_slice(&self) -> &[u8] {
69+
&self.0
70+
}
71+
}

libwebauthn/src/ops/webauthn/make_credential.rs

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ use tracing::{debug, instrument, trace};
77

88
use crate::{
99
fido::AuthenticatorData,
10+
ops::webauthn::{
11+
create::{AuthenticatorSelectionCriteria, PublicKeyCredentialCreationOptionsJSON},
12+
idl::{FromInnerModel, JsonError, WebAuthnIDL},
13+
rpid::RelyingPartyId,
14+
},
1015
proto::{
1116
ctap1::{Ctap1RegisteredKey, Ctap1Version},
1217
ctap2::{
@@ -149,10 +154,13 @@ impl MakeCredentialsResponseUnsignedExtensions {
149154
}
150155
}
151156

152-
#[derive(Debug, Clone, Copy)]
157+
#[derive(Debug, Clone, Copy, Deserialize)]
153158
pub enum ResidentKeyRequirement {
159+
#[serde(rename = "required")]
154160
Required,
161+
#[serde(rename = "preferred")]
155162
Preferred,
163+
#[serde(rename = "discouraged", other)]
156164
Discouraged,
157165
}
158166

@@ -175,6 +183,68 @@ pub struct MakeCredentialRequest {
175183
pub timeout: Duration,
176184
}
177185

186+
impl FromInnerModel<PublicKeyCredentialCreationOptionsJSON, MakeCredentialRequestParsingError>
187+
for MakeCredentialRequest
188+
{
189+
fn from_inner_model(
190+
rpid: &RelyingPartyId,
191+
inner: PublicKeyCredentialCreationOptionsJSON,
192+
) -> Result<Self, MakeCredentialRequestParsingError> {
193+
let resident_key = if inner
194+
.authenticator_selection
195+
.as_ref()
196+
.and_then(|s| Some(s.require_resident_key))
197+
== Some(true)
198+
{
199+
Some(ResidentKeyRequirement::Required)
200+
} else {
201+
inner.authenticator_selection.and_then(|s| s.resident_key)
202+
};
203+
204+
let user_verification = inner
205+
.authenticator_selection
206+
.map_or(UserVerificationRequirement::Discouraged, |s| {
207+
s.user_verification
208+
});
209+
210+
let exclude = match inner.exclude_credentials[..] {
211+
[] => None,
212+
_ => Some(inner.exclude_credentials),
213+
};
214+
215+
let extensions = serde_json::from_value(serde_json::Value::Object(inner.extensions))
216+
.map_err(MakeCredentialRequestParsingError::ExtensionError)?;
217+
218+
Ok(Self {
219+
hash: inner.challenge.into(),
220+
origin: rpid.as_str().to_owned(),
221+
relying_party: inner.rp,
222+
user: inner.user,
223+
resident_key,
224+
user_verification,
225+
algorithms: inner.params,
226+
exclude,
227+
extensions,
228+
timeout: Duration::from_secs(inner.timeout),
229+
})
230+
}
231+
}
232+
233+
#[derive(thiserror::Error, Debug)]
234+
pub enum MakeCredentialRequestParsingError {
235+
/// The client must throw an "EncodingError" DOMException.
236+
#[error("Invalid JSON format: {0}")]
237+
EncodingError(#[from] JsonError),
238+
239+
#[error("Invalid extension: {0}")]
240+
ExtensionError(JsonError),
241+
}
242+
243+
impl WebAuthnIDL<MakeCredentialRequestParsingError> for MakeCredentialRequest {
244+
type Error = MakeCredentialRequestParsingError;
245+
type InnerModel = PublicKeyCredentialCreationOptionsJSON;
246+
}
247+
178248
#[derive(Debug, Default, Clone)]
179249
pub enum MakeCredentialHmacOrPrfInput {
180250
#[default]
@@ -315,10 +385,7 @@ impl DowngradableRequest<RegisterRequest> for MakeCredentialRequest {
315385
}
316386

317387
// Options must not include "rk" set to true.
318-
if matches!(
319-
self.resident_key,
320-
Some(ResidentKeyRequirement::Required)
321-
) {
388+
if matches!(self.resident_key, Some(ResidentKeyRequirement::Required)) {
322389
debug!("Not downgradable: request requires resident key");
323390
return false;
324391
}

libwebauthn/src/ops/webauthn.rs renamed to libwebauthn/src/ops/webauthn/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
mod create;
12
mod get_assertion;
3+
pub(crate) mod idl;
24
mod make_credential;
5+
mod rpid;
36

47
use super::u2f::{RegisterRequest, SignRequest};
58
use crate::webauthn::CtapError;
@@ -17,12 +20,16 @@ pub use make_credential::{
1720
MakeCredentialResponse, MakeCredentialsRequestExtensions, MakeCredentialsResponseExtensions,
1821
MakeCredentialsResponseUnsignedExtensions, ResidentKeyRequirement,
1922
};
23+
use serde::Deserialize;
2024

21-
#[derive(Debug, Clone, Copy)]
25+
#[derive(Debug, Clone, Copy, Deserialize)]
2226
pub enum UserVerificationRequirement {
27+
#[serde(rename = "required")]
2328
Required,
24-
Preferred,
29+
#[serde(rename = "discouraged")]
2530
Discouraged,
31+
#[serde(rename = "preferred", other)]
32+
Preferred,
2633
}
2734

2835
impl UserVerificationRequirement {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use serde::Deserialize;
2+
use std::convert::TryFrom;
3+
4+
#[derive(Clone, Debug)]
5+
pub struct RelyingPartyId(pub String);
6+
7+
impl Into<&str> for &RelyingPartyId {
8+
fn into(self) -> &str {
9+
self.0.as_str()
10+
}
11+
}
12+
13+
impl Into<String> for RelyingPartyId {
14+
fn into(self) -> String {
15+
self.0
16+
}
17+
}
18+
19+
#[derive(thiserror::Error, Debug, Clone)]
20+
// TODO(#137): Validate RelyingPartyId
21+
pub enum Error {
22+
#[error("Empty Relying Party ID is not allowed")]
23+
EmptyRelyingPartyId,
24+
}
25+
26+
impl TryFrom<&str> for RelyingPartyId {
27+
type Error = Error;
28+
29+
fn try_from(value: &str) -> Result<Self, Self::Error> {
30+
// TODO(#137): Validate RelyingPartyId, including IDNA normalization
31+
// and checking for valid characters.
32+
match value {
33+
"" => Err(Error::EmptyRelyingPartyId),
34+
_ => Ok(RelyingPartyId(value.to_string())),
35+
}
36+
}
37+
}
38+
39+
impl<'de> Deserialize<'de> for RelyingPartyId {
40+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41+
where
42+
D: serde::Deserializer<'de>,
43+
{
44+
let s = String::deserialize(deserializer)?;
45+
RelyingPartyId::try_from(s.as_str()).map_err(serde::de::Error::custom)
46+
}
47+
}

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::ops::webauthn::idl::Base64UrlString;
12
use crate::pin::PinUvAuthProtocol;
23
use crate::proto::ctap1::Ctap1Transport;
34

@@ -145,7 +146,7 @@ impl From<&Ctap1Transport> for Ctap2Transport {
145146

146147
#[derive(Debug, Clone, Serialize, Deserialize)]
147148
pub struct Ctap2PublicKeyCredentialDescriptor {
148-
pub id: ByteBuf,
149+
pub id: Base64UrlString,
149150
pub r#type: Ctap2PublicKeyCredentialType,
150151

151152
#[serde(skip_serializing_if = "Option::is_none")]

0 commit comments

Comments
 (0)