Skip to content

Commit bdb1328

Browse files
fix(webauthn): largeBlob.read no longer leaks largeBlobKey to RP
When the RP requests `largeBlob: { read: true }`, libwebauthn was populating the WebAuthn response's `blob` field with the per-credential `largeBlobKey` (a 32-byte AES-256-GCM key) instead of the decrypted blob payload. The CTAP 2.1 `authenticatorLargeBlobs` command is not yet implemented; until it is, the safe behaviour is to drop the key from the WebAuthn response. The CTAP-level `Ctap2GetAssertionResponse.large_blob_key` field is unchanged so the next PR can wire up the proper flow. Refs: WebAuthn L3 sec. 10.1.5, CTAP 2.1 sec. 6.10.
1 parent a6f60bf commit bdb1328

1 file changed

Lines changed: 37 additions & 10 deletions

File tree

libwebauthn/src/proto/ctap2/model/get_assertion.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ impl Ctap2GetAssertionResponseExtensions {
521521
pub(crate) fn to_unsigned_extensions(
522522
&self,
523523
request: &GetAssertionRequest,
524-
response: &Ctap2GetAssertionResponse,
524+
_response: &Ctap2GetAssertionResponse,
525525
auth_data: Option<&AuthTokenData>,
526526
) -> GetAssertionResponseUnsignedExtensions {
527527
let decrypted_hmac = self.hmac_secret.as_ref().and_then(|x| {
@@ -548,19 +548,14 @@ impl Ctap2GetAssertionResponseExtensions {
548548
})
549549
});
550550

551-
// LargeBlobs was requested
551+
// `blob` stays `None` until `authenticatorLargeBlobs` is wired up; returning
552+
// the raw `largeBlobKey` here would disclose the per-credential AES key to
553+
// the RP instead of the decrypted blob payload.
552554
let large_blob = request
553555
.extensions
554556
.as_ref()
555557
.and_then(|ext| ext.large_blob.as_ref())
556-
.map(|_| GetAssertionLargeBlobExtensionOutput {
557-
blob: response
558-
.large_blob_key
559-
.as_ref()
560-
.map(|x| x.clone().into_vec()),
561-
// Not yet supported
562-
// written: None,
563-
});
558+
.map(|_| GetAssertionLargeBlobExtensionOutput { blob: None });
564559

565560
GetAssertionResponseUnsignedExtensions {
566561
hmac_get_secret: None,
@@ -658,4 +653,36 @@ mod tests {
658653
let assertion = response.into_assertion_output(&request, None);
659654
assert_eq!(assertion.credential_id, None);
660655
}
656+
657+
#[test]
658+
fn large_blob_read_does_not_leak_key_into_webauthn_response() {
659+
let cred = make_credential(b"cred-1");
660+
let device_returned_key = vec![0xAAu8; 32];
661+
let mut response = make_response(Some(cred.clone()));
662+
response.large_blob_key = Some(ByteBuf::from(device_returned_key.clone()));
663+
response.authenticator_data.extensions = Some(Ctap2GetAssertionResponseExtensions {
664+
cred_blob: None,
665+
hmac_secret: None,
666+
});
667+
668+
let mut request = make_request(vec![cred]);
669+
request.extensions = Some(GetAssertionRequestExtensions {
670+
cred_blob: false,
671+
prf: None,
672+
large_blob: Some(GetAssertionLargeBlobExtension::Read),
673+
});
674+
675+
let assertion = response.into_assertion_output(&request, None);
676+
let large_blob = assertion
677+
.unsigned_extensions_output
678+
.expect("unsigned extensions present")
679+
.large_blob
680+
.expect("largeBlob extension output present");
681+
682+
assert!(large_blob.blob.is_none());
683+
assert_eq!(
684+
assertion.large_blob_key.as_deref(),
685+
Some(&device_returned_key[..])
686+
);
687+
}
661688
}

0 commit comments

Comments
 (0)