Skip to content

Commit deb8c10

Browse files
Add tests for get assertion
1 parent e22fb75 commit deb8c10

File tree

3 files changed

+289
-95
lines changed

3 files changed

+289
-95
lines changed

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 207 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ use tracing::{debug, error, trace};
77
use crate::{
88
fido::AuthenticatorData,
99
ops::webauthn::{
10-
idl::{FromInnerModel, JsonError},
11-
Base64UrlString, WebAuthnIDL,
10+
client_data::ClientData,
11+
idl::{
12+
get::{
13+
HmacGetSecretInputJson, LargeBlobInputJson, PrfInputJson,
14+
PublicKeyCredentialRequestOptionsJSON,
15+
},
16+
FromInnerModel, JsonError,
17+
},
18+
Operation, WebAuthnIDL,
1219
},
1320
pin::PinUvAuthProtocol,
1421
proto::ctap2::{
@@ -22,15 +29,15 @@ use super::{DowngradableRequest, RelyingPartyId, SignRequest, UserVerificationRe
2229

2330
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
2431

25-
#[derive(Debug, Default, Clone, Serialize)]
32+
#[derive(Debug, Default, Clone, Serialize, PartialEq)]
2633
pub struct PRFValue {
2734
#[serde(with = "serde_bytes")]
2835
pub first: [u8; 32],
2936
#[serde(skip_serializing_if = "Option::is_none", with = "serde_bytes")]
3037
pub second: Option<[u8; 32]>,
3138
}
3239

33-
#[derive(Debug, Clone)]
40+
#[derive(Debug, Clone, PartialEq)]
3441
pub struct GetAssertionRequest {
3542
pub relying_party_id: String,
3643
pub hash: Vec<u8>,
@@ -68,22 +75,6 @@ impl WebAuthnIDL<GetAssertionRequestParsingError> for GetAssertionRequest {
6875
AuthenticationExtensionsClientInputsJSON extensions;
6976
}; */
7077

71-
#[derive(Deserialize, Debug, Clone)]
72-
pub struct PublicKeyCredentialRequestOptionsJSON {
73-
pub challenge: Base64UrlString,
74-
pub timeout: Option<u32>,
75-
#[serde(rename = "rpId")]
76-
pub relying_party_id: Option<String>,
77-
#[serde(rename = "allowCredentials")]
78-
#[serde(default)]
79-
pub allow_credentials: Vec<Ctap2PublicKeyCredentialDescriptor>,
80-
#[serde(rename = "userVerification")]
81-
pub uv_requirement: UserVerificationRequirement,
82-
#[serde(default)]
83-
pub hints: Vec<String>,
84-
pub extensions: Option<GetAssertionRequestExtensionsJSON>,
85-
}
86-
8778
impl FromInnerModel<PublicKeyCredentialRequestOptionsJSON, GetAssertionRequestParsingError>
8879
for GetAssertionRequest
8980
{
@@ -123,89 +114,43 @@ impl FromInnerModel<PublicKeyCredentialRequestOptionsJSON, GetAssertionRequestPa
123114

124115
let timeout: Duration = inner
125116
.timeout
126-
.map(|s| Duration::from_secs(s.into()))
117+
.map(|s| Duration::from_millis(s.into()))
127118
.unwrap_or(DEFAULT_TIMEOUT);
128119

120+
let client_data_json = ClientData {
121+
operation: Operation::GetAssertion,
122+
challenge: inner.challenge.to_vec(),
123+
origin: rpid.to_string(),
124+
cross_origin: None,
125+
};
126+
129127
Ok(GetAssertionRequest {
130128
relying_party_id: rpid.to_string(),
131-
hash: inner.challenge.into(),
132-
allow: inner.allow_credentials,
129+
hash: client_data_json.hash(),
130+
allow: inner
131+
.allow_credentials
132+
.into_iter()
133+
.map(|c| c.into())
134+
.collect(),
133135
extensions,
134136
user_verification: inner.uv_requirement,
135137
timeout,
136138
})
137139
}
138140
}
139141

140-
#[derive(Debug, Clone)]
142+
#[derive(Debug, Clone, PartialEq)]
141143
pub enum GetAssertionHmacOrPrfInput {
142144
HmacGetSecret(HMACGetSecretInput),
143145
Prf(PrfInput),
144146
}
145147

146-
#[derive(Debug, Clone)]
148+
#[derive(Debug, Clone, PartialEq)]
147149
pub struct PrfInput {
148150
pub eval: Option<PRFValue>,
149151
pub eval_by_credential: HashMap<String, PRFValue>,
150152
}
151153

152-
#[derive(Debug, Default, Clone, Serialize)]
153-
pub struct GetAssertionPrfOutput {
154-
#[serde(skip_serializing_if = "Option::is_none")]
155-
pub results: Option<PRFValue>,
156-
}
157-
158-
#[derive(Clone, Debug, Default, Eq, PartialEq)]
159-
pub struct HMACGetSecretInput {
160-
pub salt1: [u8; 32],
161-
pub salt2: Option<[u8; 32]>,
162-
}
163-
164-
#[derive(Debug, Clone, Deserialize)]
165-
pub struct HmacGetSecretInputJson {
166-
pub salt1: Base64UrlString,
167-
pub salt2: Option<Base64UrlString>,
168-
}
169-
170-
impl TryFrom<HmacGetSecretInputJson> for HMACGetSecretInput {
171-
type Error = GetAssertionRequestParsingError;
172-
173-
fn try_from(value: HmacGetSecretInputJson) -> Result<Self, Self::Error> {
174-
let salt1 = value.salt1.as_slice().try_into().map_err(|_| {
175-
GetAssertionRequestParsingError::UnexpectedLengthError(
176-
"extensions.hmacCreateSecret.salt1".to_string(),
177-
value.salt1.as_slice().len(),
178-
)
179-
})?;
180-
let salt2 = match value.salt2 {
181-
Some(s) => Some(s.as_slice().try_into().map_err(|_| {
182-
GetAssertionRequestParsingError::UnexpectedLengthError(
183-
"extensions.hmacCreateSecret.salt2".to_string(),
184-
s.as_slice().len(),
185-
)
186-
})?),
187-
None => None,
188-
};
189-
Ok(HMACGetSecretInput { salt1, salt2 })
190-
}
191-
}
192-
#[derive(Debug, Clone, Deserialize)]
193-
pub struct LargeBlobInputJson {
194-
pub read: Option<bool>,
195-
}
196-
197-
#[derive(Debug, Clone, Deserialize)]
198-
pub struct PrfInputJson {
199-
pub eval: Option<PrfValuesJson>,
200-
pub eval_by_credential: Option<HashMap<String, PrfValuesJson>>,
201-
}
202-
203-
#[derive(Debug, Clone, Deserialize)]
204-
pub struct PrfValuesJson {
205-
pub first: Base64UrlString,
206-
pub second: Option<Base64UrlString>,
207-
}
208-
209154
impl TryFrom<PrfInputJson> for PrfInput {
210155
type Error = GetAssertionRequestParsingError;
211156

@@ -266,6 +211,41 @@ impl TryFrom<PrfInputJson> for PrfInput {
266211
}
267212
}
268213

214+
#[derive(Debug, Default, Clone, Serialize)]
215+
pub struct GetAssertionPrfOutput {
216+
#[serde(skip_serializing_if = "Option::is_none")]
217+
pub results: Option<PRFValue>,
218+
}
219+
220+
#[derive(Clone, Debug, Default, Eq, PartialEq)]
221+
pub struct HMACGetSecretInput {
222+
pub salt1: [u8; 32],
223+
pub salt2: Option<[u8; 32]>,
224+
}
225+
226+
impl TryFrom<HmacGetSecretInputJson> for HMACGetSecretInput {
227+
type Error = GetAssertionRequestParsingError;
228+
229+
fn try_from(value: HmacGetSecretInputJson) -> Result<Self, Self::Error> {
230+
let salt1 = value.salt1.as_slice().try_into().map_err(|_| {
231+
GetAssertionRequestParsingError::UnexpectedLengthError(
232+
"extensions.hmacCreateSecret.salt1".to_string(),
233+
value.salt1.as_slice().len(),
234+
)
235+
})?;
236+
let salt2 = match value.salt2 {
237+
Some(s) => Some(s.as_slice().try_into().map_err(|_| {
238+
GetAssertionRequestParsingError::UnexpectedLengthError(
239+
"extensions.hmacCreateSecret.salt2".to_string(),
240+
s.as_slice().len(),
241+
)
242+
})?),
243+
None => None,
244+
};
245+
Ok(HMACGetSecretInput { salt1, salt2 })
246+
}
247+
}
248+
269249
#[derive(Debug, Clone, PartialEq, Eq)]
270250
pub enum GetAssertionLargeBlobExtension {
271251
Read,
@@ -296,25 +276,13 @@ pub struct GetAssertionLargeBlobExtensionOutput {
296276
// pub written: Option<bool>,
297277
}
298278

299-
#[derive(Debug, Default, Clone)]
279+
#[derive(Debug, Default, Clone, PartialEq)]
300280
pub struct GetAssertionRequestExtensions {
301281
pub cred_blob: bool,
302282
pub hmac_or_prf: Option<GetAssertionHmacOrPrfInput>,
303283
pub large_blob: Option<GetAssertionLargeBlobExtension>,
304284
}
305285

306-
#[derive(Debug, Clone, Default, Deserialize)]
307-
pub struct GetAssertionRequestExtensionsJSON {
308-
#[serde(rename = "getCredBlob")]
309-
pub cred_blob: Option<bool>,
310-
#[serde(rename = "largeBlobKey")]
311-
pub large_blob: Option<LargeBlobInputJson>,
312-
#[serde(rename = "hmacCreateSecret")]
313-
pub hamc_get_secret: Option<HmacGetSecretInputJson>,
314-
#[serde(rename = "prf")]
315-
pub prf: Option<PrfInputJson>,
316-
}
317-
318286
#[derive(Clone, Debug, Default, Serialize)]
319287
#[serde(rename_all = "camelCase")]
320288
pub struct HMACGetSecretOutput {
@@ -460,3 +428,147 @@ impl DowngradableRequest<Vec<SignRequest>> for GetAssertionRequest {
460428
Ok(downgraded_requests)
461429
}
462430
}
431+
432+
#[cfg(test)]
433+
mod tests {
434+
use std::time::Duration;
435+
436+
use serde_bytes::ByteBuf;
437+
438+
use crate::ops::webauthn::GetAssertionRequest;
439+
use crate::ops::webauthn::RelyingPartyId;
440+
use crate::proto::ctap2::Ctap2PublicKeyCredentialType;
441+
442+
use super::*;
443+
444+
pub const REQUEST_BASE_JSON: &str = r#"
445+
{
446+
"challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu",
447+
"timeout": 30000,
448+
"rpId": "example.org",
449+
"allowCredentials": [
450+
{
451+
"type": "public-key",
452+
"id": "bXktY3JlZGVudGlhbC1pZA"
453+
}
454+
],
455+
"userVerification": "preferred"
456+
}
457+
"#;
458+
459+
fn request_base() -> GetAssertionRequest {
460+
let client_data_json = ClientData {
461+
operation: Operation::GetAssertion,
462+
challenge: base64_url::decode("Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu").unwrap(),
463+
origin: "example.org".to_string(),
464+
cross_origin: None,
465+
};
466+
GetAssertionRequest {
467+
relying_party_id: "example.org".to_owned(),
468+
hash: client_data_json.hash(),
469+
allow: vec![Ctap2PublicKeyCredentialDescriptor {
470+
r#type: Ctap2PublicKeyCredentialType::PublicKey,
471+
id: ByteBuf::from(base64_url::decode("bXktY3JlZGVudGlhbC1pZA").unwrap()),
472+
transports: None,
473+
}],
474+
extensions: GetAssertionRequestExtensions::default(),
475+
user_verification: UserVerificationRequirement::Preferred,
476+
timeout: Duration::from_secs(30),
477+
}
478+
}
479+
480+
fn json_field_add(str: &str, field: &str, value: &str) -> String {
481+
let mut v: serde_json::Value = serde_json::from_str(str).unwrap();
482+
v.as_object_mut()
483+
.unwrap()
484+
.insert(field.to_owned(), serde_json::from_str(value).unwrap());
485+
serde_json::to_string(&v).unwrap()
486+
}
487+
488+
fn json_field_rm(str: &str, field: &str) -> String {
489+
let mut v: serde_json::Value = serde_json::from_str(str).unwrap();
490+
v.as_object_mut().unwrap().remove(field);
491+
serde_json::to_string(&v).unwrap()
492+
}
493+
494+
#[test]
495+
fn test_request_from_json_base() {
496+
let rpid = RelyingPartyId::try_from("example.org").unwrap();
497+
let req: GetAssertionRequest =
498+
GetAssertionRequest::from_json(&rpid, REQUEST_BASE_JSON).unwrap();
499+
assert_eq!(req, request_base());
500+
}
501+
502+
#[test]
503+
fn test_request_from_json_ignore_missing_rp_id() {
504+
let rpid = RelyingPartyId::try_from("example.org").unwrap();
505+
let req_json = json_field_rm(REQUEST_BASE_JSON, "rpId");
506+
507+
let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap();
508+
assert_eq!(req, request_base());
509+
}
510+
511+
#[test]
512+
fn test_request_from_json_ignore_request_rp_id() {
513+
let rpid = RelyingPartyId::try_from("example.org").unwrap();
514+
let req_json = json_field_rm(REQUEST_BASE_JSON, "rpId");
515+
let req_json = json_field_add(&req_json, "rpId", r#""another-example.org""#);
516+
517+
let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap();
518+
assert_eq!(req, request_base());
519+
}
520+
521+
#[test]
522+
fn test_request_from_json_ignore_missing_allow_credentials() {
523+
let rpid = RelyingPartyId::try_from("example.org").unwrap();
524+
let req_json = json_field_rm(REQUEST_BASE_JSON, "allowCredentials");
525+
526+
let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap();
527+
assert_eq!(
528+
req,
529+
GetAssertionRequest {
530+
allow: vec![],
531+
..request_base()
532+
}
533+
);
534+
}
535+
536+
#[test]
537+
fn test_request_from_json_default_timeout() {
538+
let rpid = RelyingPartyId::try_from("example.org").unwrap();
539+
let req_json = json_field_rm(REQUEST_BASE_JSON, "timeout");
540+
541+
let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap();
542+
assert_eq!(req.timeout, DEFAULT_TIMEOUT);
543+
}
544+
545+
#[test]
546+
#[ignore] // FIXME(#134) allow arbitrary size input
547+
fn test_request_from_json_prf_extension() {
548+
let rpid = RelyingPartyId::try_from("example.org").unwrap();
549+
let req_json = json_field_add(
550+
REQUEST_BASE_JSON,
551+
"extensions",
552+
r#"{"prf":{"eval":{"first": "second"}}}"#,
553+
);
554+
555+
let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap();
556+
if let GetAssertionRequestExtensions {
557+
hmac_or_prf:
558+
Some(GetAssertionHmacOrPrfInput::Prf(PrfInput {
559+
eval: Some(ref prf_value),
560+
..
561+
})),
562+
..
563+
} = &req.extensions
564+
{
565+
assert_eq!(&prf_value.first[..], b"first");
566+
assert_eq!(
567+
prf_value.second.as_ref().map(|s| &s[..]),
568+
Some(&b"second"[..])
569+
);
570+
} else {
571+
panic!("Expected PRF extension with correct values");
572+
}
573+
}
574+
}

0 commit comments

Comments
 (0)