Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions libwebauthn-tests/tests/large_blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ async fn test_webauthn_large_blob_read_returns_planted_blob() {
allow: vec![credential],
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
appid: None,
cred_blob: false,
prf: None,
Expand Down Expand Up @@ -137,6 +138,7 @@ async fn capture_large_blob_key(
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
appid: None,
cred_blob: false,
prf: None,
Expand Down Expand Up @@ -294,6 +296,7 @@ fn ga_request(
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
appid: None,
cred_blob: false,
prf: None,
Expand Down
4 changes: 4 additions & 0 deletions libwebauthn-tests/tests/prf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ async fn run_success_test(
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Preferred,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(prf),
..Default::default()
}),
Expand Down Expand Up @@ -638,6 +639,7 @@ async fn run_failed_test(
allow: credential.map(|x| vec![x.clone()]).unwrap_or_default(),
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(prf),
..Default::default()
}),
Expand Down Expand Up @@ -732,6 +734,7 @@ async fn test_webauthn_prf_variable_length_input() {
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Preferred,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first,
Expand Down Expand Up @@ -952,6 +955,7 @@ async fn test_webauthn_prf_upgrades_uv_at_assertion() {
allow: vec![credential],
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(prf),
..Default::default()
}),
Expand Down
1 change: 1 addition & 0 deletions libwebauthn/examples/features/prf_replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ async fn run_success_test(
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Preferred,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(prf),
..Default::default()
}),
Expand Down
1 change: 1 addition & 0 deletions libwebauthn/examples/features/webauthn_extensions_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
allow: vec![credential],
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
cred_blob: true,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
Expand Down
1 change: 1 addition & 0 deletions libwebauthn/examples/features/webauthn_prf_cable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ async fn get(credential_id: Option<&str>) -> Result<(), Box<dyn Error>> {
allow,
user_verification: UserVerificationRequirement::Preferred,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: GET_EVAL_FIRST.to_vec(),
Expand Down
2 changes: 2 additions & 0 deletions libwebauthn/examples/features/webauthn_prf_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ async fn run_success_test(
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(prf),
..Default::default()
}),
Expand Down Expand Up @@ -375,6 +376,7 @@ async fn run_failed_test(
allow: credential.map(|x| vec![x.clone()]).unwrap_or_default(),
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(prf),
..Default::default()
}),
Expand Down
60 changes: 58 additions & 2 deletions libwebauthn/src/ops/webauthn/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON> for GetAssertionRequest
None => None,
};

let hmac_get_secret = match inner
.extensions
.as_ref()
.and_then(|e| e.hmac_get_secret.as_ref())
{
Some(hmac_json) => Some(HMACGetSecretInput::try_from(hmac_json.clone())?),
None => None,
};

let large_blob = match inner
.extensions
.as_ref()
Expand All @@ -230,6 +239,7 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON> for GetAssertionRequest
cred_blob: extensions_opt.cred_blob.unwrap_or(false),
large_blob: large_blob.clone(),
prf: prf.clone(),
hmac_get_secret: hmac_get_secret.clone(),
appid: appid.clone(),
});

Expand Down Expand Up @@ -321,14 +331,14 @@ impl TryFrom<HmacGetSecretInputJson> for HMACGetSecretInput {
fn try_from(value: HmacGetSecretInputJson) -> Result<Self, Self::Error> {
let salt1 = value.salt1.as_slice().try_into().map_err(|_| {
GetAssertionPrepareError::UnexpectedLengthError(
"extensions.hmacCreateSecret.salt1".to_string(),
"extensions.hmacGetSecret.salt1".to_string(),
value.salt1.as_slice().len(),
)
})?;
let salt2 = match value.salt2 {
Some(s) => Some(s.as_slice().try_into().map_err(|_| {
GetAssertionPrepareError::UnexpectedLengthError(
"extensions.hmacCreateSecret.salt2".to_string(),
"extensions.hmacGetSecret.salt2".to_string(),
s.as_slice().len(),
)
})?),
Expand Down Expand Up @@ -388,6 +398,9 @@ pub struct GetAssertionRequestExtensions {
pub cred_blob: bool,
/// PRF extension input. At the CTAP level, this is converted to HMAC secret.
pub prf: Option<PrfInput>,
/// hmacGetSecret extension input (the pre-PRF hmac-secret client extension): the raw
/// salts, sent as they are. Ignored when `prf` is also present.
pub hmac_get_secret: Option<HMACGetSecretInput>,
pub large_blob: Option<GetAssertionLargeBlobExtension>,
/// FIDO AppID extension (WebAuthn L3 §10.1.1). When the relying party has
/// existing U2F credentials registered under a legacy AppID, this URL is
Expand Down Expand Up @@ -1408,6 +1421,7 @@ mod tests {

let mut req = request_base();
req.extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
cred_blob: false,
prf: None,
large_blob: None,
Expand Down Expand Up @@ -1464,6 +1478,48 @@ mod tests {
.expect("prf extension")
}

#[tokio::test]
async fn test_request_from_json_hmac_get_secret_extension() {
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();
let salt = base64_url::encode(&[0x5Au8; 32]);
let req_json = json_field_add(
REQUEST_BASE_JSON,
"extensions",
&format!(r#"{{"hmacGetSecret":{{"salt1":"{salt}"}}}}"#),
);
let req = from_json(
&request_origin,
&MockPublicSuffixList,
RelatedOrigins::Disabled,
&req_json,
)
.await
.expect("request should parse");
let ext = req.extensions.expect("extensions");
assert!(ext.prf.is_none());
assert_eq!(
ext.hmac_get_secret,
Some(HMACGetSecretInput {
salt1: [0x5A; 32],
salt2: None,
})
);
// A salt that is not 32 bytes is refused.
let req_json = json_field_add(
REQUEST_BASE_JSON,
"extensions",
r#"{"hmacGetSecret":{"salt1":"AQID"}}"#,
);
assert!(from_json(
&request_origin,
&MockPublicSuffixList,
RelatedOrigins::Disabled,
&req_json,
)
.await
.is_err());
}

#[tokio::test]
async fn test_request_from_json_prf_extension() {
// Non-32-byte inputs must now parse (W3C WebAuthn L3 §10.1.4). "AQID"
Expand Down
3 changes: 3 additions & 0 deletions libwebauthn/src/ops/webauthn/large_blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,7 @@ mod tests {
top_origin: None,
allow: vec![],
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
appid: None,
cred_blob: false,
prf: None,
Expand Down Expand Up @@ -1051,6 +1052,7 @@ mod tests {
top_origin: None,
allow: vec![],
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
appid: None,
cred_blob: false,
prf: None,
Expand Down Expand Up @@ -1152,6 +1154,7 @@ mod tests {
top_origin: None,
allow: vec![],
extensions: Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
appid: None,
cred_blob: false,
prf: None,
Expand Down
64 changes: 52 additions & 12 deletions libwebauthn/src/proto/ctap2/model/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,11 @@ impl From<GetAssertionRequestExtensions> for Ctap2GetAssertionRequestExtensions
prf: None, // Set by convert_prf_to_native when the device advertises `prf`
cred_blob: other.cred_blob,
hmac_secret: None, // Gets calculated later
hmac_or_prf: other.prf.map(GetAssertionHmacOrPrfInput::Prf),
hmac_or_prf: other.prf.map(GetAssertionHmacOrPrfInput::Prf).or_else(|| {
other
.hmac_get_secret
.map(GetAssertionHmacOrPrfInput::HmacGetSecret)
}),
large_blob_key: if needs_key { Some(true) } else { None },
large_blob_write: is_write,
}
Expand Down Expand Up @@ -720,20 +724,23 @@ impl Ctap2GetAssertionResponseExtensions {
}
});

let prf = decrypted_hmac.and_then(|decrypted| {
// At WebAuthn level, we only support PRF (not raw HMAC).
// The PRF input was converted to HMAC internally.
request
.extensions
.as_ref()
.and_then(|ext| ext.prf.as_ref())
.map(|_| GetAssertionPrfOutput {
// The answer goes back under the extension that asked: prf (whose input was
// converted to hmac-secret internally) or, failing that, hmacGetSecret.
let (prf, hmac_get_secret) = match (decrypted_hmac, request.extensions.as_ref()) {
(Some(decrypted), Some(ext)) if ext.prf.is_some() => (
Some(GetAssertionPrfOutput {
results: Some(PrfOutputValue {
first: decrypted.output1,
second: decrypted.output2,
}),
})
});
}),
None,
),
(Some(decrypted), Some(ext)) if ext.hmac_get_secret.is_some() => {
(None, Some(decrypted))
}
_ => (None, None),
};

// `blob` stays `None` until `authenticatorLargeBlobs` is wired up; returning
// the raw `largeBlobKey` here would disclose the per-credential AES key to
Expand All @@ -758,7 +765,7 @@ impl Ctap2GetAssertionResponseExtensions {
.map(|_| false);

GetAssertionResponseUnsignedExtensions {
hmac_get_secret: None,
hmac_get_secret,
large_blob,
prf,
appid,
Expand All @@ -770,6 +777,37 @@ impl Ctap2GetAssertionResponseExtensions {
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn hmac_get_secret_input_is_sent_raw_and_answered_as_itself() {
let input = crate::ops::webauthn::HMACGetSecretInput {
salt1: [0x5A; 32],
salt2: None,
};
let ext = GetAssertionRequestExtensions {
hmac_get_secret: Some(input.clone()),
..Default::default()
};
let ctap = Ctap2GetAssertionRequestExtensions::from(ext);
assert_eq!(
ctap.hmac_or_prf,
Some(GetAssertionHmacOrPrfInput::HmacGetSecret(input))
);
// prf wins when both are asked: the authenticator answers one hmac-secret.
let both = GetAssertionRequestExtensions {
hmac_get_secret: Some(crate::ops::webauthn::HMACGetSecretInput::default()),
prf: Some(crate::ops::webauthn::PrfInput {
eval: None,
eval_by_credential: Default::default(),
}),
..Default::default()
};
assert!(matches!(
Ctap2GetAssertionRequestExtensions::from(both).hmac_or_prf,
Some(GetAssertionHmacOrPrfInput::Prf(_))
));
}
use super::*;
use crate::fido::AuthenticatorDataFlags;
use crate::proto::ctap2::Ctap2PublicKeyCredentialType;
use std::time::Duration;
Expand Down Expand Up @@ -870,6 +908,7 @@ mod tests {

let mut request = make_request(vec![cred]);
request.extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
cred_blob: false,
prf: None,
large_blob: Some(GetAssertionLargeBlobExtension::Read),
Expand Down Expand Up @@ -909,6 +948,7 @@ mod tests {
) -> GetAssertionRequest {
let mut request = make_request(allow);
request.extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
cred_blob: false,
prf: Some(crate::ops::webauthn::PrfInput {
eval,
Expand Down
7 changes: 7 additions & 0 deletions libwebauthn/src/webauthn/pin_uv_auth_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,7 @@ mod test {
info_extensions.as_deref(),
UserVerificationRequirement::Discouraged,
Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: vec![0; 32],
Expand Down Expand Up @@ -1153,6 +1154,7 @@ mod test {
Some(&["hmac-secret"]),
UserVerificationRequirement::Preferred,
Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: vec![0; 32],
Expand Down Expand Up @@ -1225,6 +1227,7 @@ mod test {

for (info_options, uv_requirement) in testcases {
let extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: vec![0; 32],
Expand Down Expand Up @@ -1298,6 +1301,7 @@ mod test {
channel.push_command_pair(info_req, info_resp);

let extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: vec![0; 32],
Expand Down Expand Up @@ -1348,6 +1352,7 @@ mod test {

for (info_options, uv_requirement) in testcases {
let extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: vec![0; 32],
Expand Down Expand Up @@ -1468,6 +1473,7 @@ mod test {

for (info_options, uv_requirement) in testcases {
let extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: vec![0; 32],
Expand Down Expand Up @@ -2214,6 +2220,7 @@ mod test {
channel.push_command_pair(key_agreement_req, key_agreement_resp);

let extensions = Some(GetAssertionRequestExtensions {
hmac_get_secret: None,
large_blob: Some(GetAssertionLargeBlobExtension::Write(vec![1, 2, 3, 4])),
..Default::default()
});
Expand Down