Skip to content
Merged
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
509 changes: 506 additions & 3 deletions libwebauthn-tests/tests/large_blob.rs

Large diffs are not rendered by default.

67 changes: 55 additions & 12 deletions libwebauthn/src/ops/webauthn/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON> for GetAssertionRequest
.and_then(|e| e.large_blob.as_ref())
{
// L3 §10.1.5: largeBlob without read=true or support is a no-op, not an error.
Some(lb) if lb.support.is_none() && lb.read != Some(true) => None,
Some(lb) if lb.support.is_none() && lb.read != Some(true) && lb.write.is_none() => None,
Some(lb) => Some(GetAssertionLargeBlobExtension::try_from(lb.clone())?),
None => None,
};
Expand Down Expand Up @@ -336,9 +336,12 @@ impl TryFrom<HmacGetSecretInputJson> for HMACGetSecretInput {

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GetAssertionLargeBlobExtension {
/// Per WebAuthn L3 §10.1.5 (read=true): fetch the credential's blob.
Read,
// Not yet supported
// Write(Vec<u8>),
/// Per WebAuthn L3 §10.1.5 (write=ArrayBuffer): store this blob against the credential.
Write(Vec<u8>),
/// CTAP 2.2 §6.10.6 erase branch. Not exposed via WebAuthn L3 JSON IDL.
Delete,
}

impl TryFrom<LargeBlobInputJson> for GetAssertionLargeBlobExtension {
Expand All @@ -350,13 +353,19 @@ impl TryFrom<LargeBlobInputJson> for GetAssertionLargeBlobExtension {
"largeBlob.support is only valid at registration".to_string(),
));
}
// WebAuthn L3 §10.1.5: read and write present together is an error.
if value.read.is_some() && value.write.is_some() {
return Err(GetAssertionPrepareError::NotSupported(
"largeBlob.read and largeBlob.write are mutually exclusive".to_string(),
));
}
if let Some(write) = value.write {
return Ok(GetAssertionLargeBlobExtension::Write(write.to_vec()));
}
match value.read {
Some(true) => Ok(GetAssertionLargeBlobExtension::Read),
Some(false) => Err(GetAssertionPrepareError::NotSupported(
"largeBlob writes not supported".to_string(),
)),
None => Err(GetAssertionPrepareError::NotSupported(
"largeBlob read not requested".to_string(),
_ => Err(GetAssertionPrepareError::NotSupported(
"largeBlob input must set read=true or write".to_string(),
)),
}
}
Expand All @@ -366,9 +375,8 @@ impl TryFrom<LargeBlobInputJson> for GetAssertionLargeBlobExtension {
pub struct GetAssertionLargeBlobExtensionOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<Vec<u8>>,
// Not yet supported
// #[serde(skip_serializing_if = "Option::is_none")]
// pub written: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub written: Option<bool>,
}

#[derive(Debug, Default, Clone, PartialEq)]
Expand Down Expand Up @@ -543,7 +551,7 @@ impl Assertion {
.blob
.as_ref()
.map(|b| Base64UrlString::from(b.as_slice())),
written: None, // Write not yet supported
written: large_blob.written,
});
}

Expand Down Expand Up @@ -596,6 +604,15 @@ impl DowngradableRequest<Vec<SignRequest>> for GetAssertionRequest {
return false;
}

if matches!(
self.extensions.as_ref().and_then(|e| e.large_blob.as_ref()),
Some(GetAssertionLargeBlobExtension::Write(_))
| Some(GetAssertionLargeBlobExtension::Delete)
) {
debug!("Not downgradable: largeBlob write/delete requires FIDO2");
return false;
}

true
}

Expand Down Expand Up @@ -1401,6 +1418,7 @@ mod tests {
GetAssertionLargeBlobExtension::try_from(LargeBlobInputJson {
support: None,
read: Some(true),
write: None,
})
.unwrap(),
GetAssertionLargeBlobExtension::Read
Expand All @@ -1409,6 +1427,7 @@ mod tests {
GetAssertionLargeBlobExtension::try_from(LargeBlobInputJson {
support: Some("required".to_string()),
read: Some(true),
write: None,
}),
Err(GetAssertionPrepareError::NotSupported(_))
));
Expand Down Expand Up @@ -1445,4 +1464,28 @@ mod tests {
.expect("largeBlob.read=false must be a no-op, not an error");
assert!(req.extensions.and_then(|e| e.large_blob).is_none());
}

#[test]
fn large_blob_json_write_input_and_mutual_exclusion() {
use crate::ops::webauthn::idl::get::LargeBlobInputJson;

let blob = b"blob to write".to_vec();
assert_eq!(
GetAssertionLargeBlobExtension::try_from(LargeBlobInputJson {
support: None,
read: None,
write: Some(Base64UrlString::from(blob.clone())),
})
.unwrap(),
GetAssertionLargeBlobExtension::Write(blob)
);
assert!(matches!(
GetAssertionLargeBlobExtension::try_from(LargeBlobInputJson {
support: None,
read: Some(true),
write: Some(Base64UrlString::from(b"x".to_vec())),
}),
Err(GetAssertionPrepareError::NotSupported(_))
));
}
}
1 change: 1 addition & 0 deletions libwebauthn/src/ops/webauthn/idl/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub struct GetAssertionRequestExtensionsJSON {
pub struct LargeBlobInputJson {
pub support: Option<String>,
pub read: Option<bool>,
pub write: Option<Base64UrlString>,
}

#[derive(Debug, Clone, Deserialize)]
Expand Down
Loading
Loading