Skip to content

Commit 8640a90

Browse files
fix(webauthn): largeBlob.read no longer leaks largeBlobKey to RP (#198)
Per [WebAuthn L3 sec. 10.1.5 (largeBlob extension)](https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension), the relying party expects the `blob` output to be the decrypted plaintext blob payload, fetched by the platform via the CTAP [`authenticatorLargeBlobs`](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorLargeBlobs) command using the per-credential `largeBlobKey` as an AES-256-GCM key. The library was instead writing the raw `largeBlobKey` into `blob` and never calling `authenticatorLargeBlobs`. That means an RP receives the AES key itself (not the blob), and if the RP can also read the device's `largeBlobArray` (publicly readable region of the authenticator over CTAP), it can decrypt and forge entries. Until `authenticatorLargeBlobs` is wired up (follow-up PR #206), the safe behaviour is to set `large_blob.blob = None`. The CTAP-level model keeps the field so the follow-up can use it. ## Changes - Stop routing `largeBlobKey` into the WebAuthn `large_blob.blob` output. - The CTAP-level `Ctap2GetAssertionResponse.large_blob_key` remains so the next PR can use it. - Regression test asserts the WebAuthn response no longer contains the key. ## References - [WebAuthn L3 sec. 10.1.5: Large blob storage extension (`largeBlob`)](https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension) - [CTAP 2.1 sec. 6.10: `authenticatorLargeBlobs` (0x0C)](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorLargeBlobs)
1 parent a6f60bf commit 8640a90

2 files changed

Lines changed: 33 additions & 13 deletions

File tree

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,6 @@ pub struct Assertion {
405405
pub user: Option<Ctap2PublicKeyCredentialUserEntity>,
406406
pub credentials_count: Option<u32>,
407407
pub user_selected: Option<bool>,
408-
pub large_blob_key: Option<Vec<u8>>,
409408
pub unsigned_extensions_output: Option<GetAssertionResponseUnsignedExtensions>,
410409
pub enterprise_attestation: Option<bool>,
411410
pub attestation_statement: Option<Ctap2AttestationStatement>,
@@ -815,7 +814,6 @@ mod tests {
815814
user: None,
816815
credentials_count: None,
817816
user_selected: None,
818-
large_blob_key: None,
819817
unsigned_extensions_output: None,
820818
enterprise_attestation: None,
821819
attestation_statement: None,

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

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,6 @@ impl Ctap2GetAssertionResponse {
493493
user: self.user,
494494
credentials_count: self.credentials_count,
495495
user_selected: self.user_selected,
496-
large_blob_key: self.large_blob_key.map(ByteBuf::into_vec),
497496
unsigned_extensions_output,
498497
enterprise_attestation: self.enterprise_attestation,
499498
attestation_statement: self.attestation_statement,
@@ -521,7 +520,7 @@ impl Ctap2GetAssertionResponseExtensions {
521520
pub(crate) fn to_unsigned_extensions(
522521
&self,
523522
request: &GetAssertionRequest,
524-
response: &Ctap2GetAssertionResponse,
523+
_response: &Ctap2GetAssertionResponse,
525524
auth_data: Option<&AuthTokenData>,
526525
) -> GetAssertionResponseUnsignedExtensions {
527526
let decrypted_hmac = self.hmac_secret.as_ref().and_then(|x| {
@@ -548,19 +547,14 @@ impl Ctap2GetAssertionResponseExtensions {
548547
})
549548
});
550549

551-
// LargeBlobs was requested
550+
// `blob` stays `None` until `authenticatorLargeBlobs` is wired up; returning
551+
// the raw `largeBlobKey` here would disclose the per-credential AES key to
552+
// the RP instead of the decrypted blob payload.
552553
let large_blob = request
553554
.extensions
554555
.as_ref()
555556
.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-
});
557+
.map(|_| GetAssertionLargeBlobExtensionOutput { blob: None });
564558

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

0 commit comments

Comments
 (0)