Skip to content

Commit 637ca93

Browse files
feat(webauthn): largeBlob write and delete
Implements WebAuthn L3 §10.1.5 write and a libwebauthn-side delete on top of CTAP 2.2 §6.10.6 update-or-erase. The platform fetches the existing large-blob array, drops any entry that decrypts under the credential's largeBlobKey, optionally appends a fresh encrypted entry, and re-uploads the canonical CBOR array with a fresh SHA-256 trailer. Foreign entries (different key, malformed, unknown fields) are preserved byte-for-byte per the §6.10.2 platform contract. Upload is chunked per maxFragmentLength with pinUvAuthParam computed as authenticate(token, 32*0xff || 0x0c 0x00 || u32_le(offset) || SHA-256(set)) per §6.10.2. The Ctap2GetAssertionRequest now ORs LARGE_BLOB_WRITE into its permissions when write or delete is requested so user_verification negotiates a token covering the lbw permission. To keep PIN-protected devices working under UV=Discouraged, the Ctap2UserVerifiableRequest trait gains needs_pin_uv_auth_token, which suppresses the OnlyForSharedSecret downgrade in user_verification. Unprotected authenticators continue to accept the write without auth params per spec line 137. Delete with no matching entry returns LargeBlobError::NoMatch (written=false), matching the strict §6.10.6 'Return an error' branch at line 303.
1 parent fa2aa83 commit 637ca93

8 files changed

Lines changed: 795 additions & 81 deletions

File tree

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,12 @@ impl TryFrom<HmacGetSecretInputJson> for HMACGetSecretInput {
336336

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

344347
impl TryFrom<LargeBlobInputJson> for GetAssertionLargeBlobExtension {
@@ -350,13 +353,19 @@ impl TryFrom<LargeBlobInputJson> for GetAssertionLargeBlobExtension {
350353
"largeBlob.support is only valid at registration".to_string(),
351354
));
352355
}
356+
// WebAuthn L3 §10.1.5: read and write are mutually exclusive.
357+
if value.read == Some(true) && value.write.is_some() {
358+
return Err(GetAssertionPrepareError::NotSupported(
359+
"largeBlob.read and largeBlob.write are mutually exclusive".to_string(),
360+
));
361+
}
362+
if let Some(write) = value.write {
363+
return Ok(GetAssertionLargeBlobExtension::Write(write.to_vec()));
364+
}
353365
match value.read {
354366
Some(true) => Ok(GetAssertionLargeBlobExtension::Read),
355-
Some(false) => Err(GetAssertionPrepareError::NotSupported(
356-
"largeBlob writes not supported".to_string(),
357-
)),
358-
None => Err(GetAssertionPrepareError::NotSupported(
359-
"largeBlob read not requested".to_string(),
367+
_ => Err(GetAssertionPrepareError::NotSupported(
368+
"largeBlob input must set read=true or write".to_string(),
360369
)),
361370
}
362371
}
@@ -366,9 +375,8 @@ impl TryFrom<LargeBlobInputJson> for GetAssertionLargeBlobExtension {
366375
pub struct GetAssertionLargeBlobExtensionOutput {
367376
#[serde(skip_serializing_if = "Option::is_none")]
368377
pub blob: Option<Vec<u8>>,
369-
// Not yet supported
370-
// #[serde(skip_serializing_if = "Option::is_none")]
371-
// pub written: Option<bool>,
378+
#[serde(skip_serializing_if = "Option::is_none")]
379+
pub written: Option<bool>,
372380
}
373381

374382
#[derive(Debug, Default, Clone, PartialEq)]
@@ -543,7 +551,7 @@ impl Assertion {
543551
.blob
544552
.as_ref()
545553
.map(|b| Base64UrlString::from(b.as_slice())),
546-
written: None, // Write not yet supported
554+
written: large_blob.written,
547555
});
548556
}
549557

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub struct GetAssertionRequestExtensionsJSON {
6363
pub struct LargeBlobInputJson {
6464
pub support: Option<String>,
6565
pub read: Option<bool>,
66+
pub write: Option<Base64UrlString>,
6667
}
6768

6869
#[derive(Debug, Clone, Deserialize)]

0 commit comments

Comments
 (0)