Skip to content

Commit 1129c2d

Browse files
feat(webauthn): perform largeBlob.read via authenticatorLargeBlobs
When the WebAuthn `largeBlob: { read: true }` extension is requested and the authenticator returns a per-credential `largeBlobKey`, libwebauthn now runs `authenticatorLargeBlobs(get)` to fetch the on-device serialized array, decrypts the matching entry, and exposes the plaintext via the WebAuthn response's `unsigned_extensions_output.large_blob.blob` field. The read flow uses a per-assertion `AuthenticatorLargeBlobStorage` handle (introduced in the previous commit), so each credential is read against its own `largeBlobKey`. Failures are non-fatal: per WebAuthn L3 §10.5 the `blob` output is optional on success. Combined with the earlier fix that removed the key-disclosure bug, this completes the read half of the WebAuthn `largeBlob` extension. Refs: WebAuthn L3 §10.5, CTAP 2.2 §6.10.
1 parent 9a70996 commit 1129c2d

1 file changed

Lines changed: 67 additions & 1 deletion

File tree

libwebauthn/src/webauthn.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ use tracing::{debug, error, info, instrument, trace, warn};
66

77
use crate::fido::FidoProtocol;
88
use crate::ops::u2f::{RegisterRequest, SignRequest, UpgradableResponse};
9-
use crate::ops::webauthn::{DowngradableRequest, GetAssertionRequest, GetAssertionResponse};
9+
use crate::ops::webauthn::{
10+
AuthenticatorLargeBlobStorage, DowngradableRequest, GetAssertionLargeBlobExtension,
11+
GetAssertionLargeBlobExtensionOutput, GetAssertionRequest, GetAssertionResponse,
12+
GetAssertionResponseUnsignedExtensions, LargeBlobStorage,
13+
};
1014
use crate::ops::webauthn::{MakeCredentialRequest, MakeCredentialResponse};
1115
use crate::proto::ctap1::Ctap1;
1216
use crate::proto::ctap2::preflight::ctap2_preflight;
@@ -235,6 +239,68 @@ where
235239
let response = self.ctap2_get_next_assertion(op.timeout).await?;
236240
assertions.push(response.into_assertion_output(op, self.get_auth_data()));
237241
}
242+
243+
// WebAuthn L3 §10.5 largeBlob.read: when the RP requested
244+
// `largeBlob: { read: true }` and the authenticator returned a
245+
// per-credential `largeBlobKey`, fetch the on-device serialized
246+
// largeBlobArray via `authenticatorLargeBlobs(get)`, locate this
247+
// credential's entry by GCM-tag verification, decrypt, decompress,
248+
// and surface the plaintext as the WebAuthn `large_blob.blob` value.
249+
//
250+
// Failures here are non-fatal: per spec the `blob` field is optional
251+
// on success and absent if the read could not be completed.
252+
let large_blob_read_requested = op.extensions.as_ref().and_then(|e| e.large_blob.as_ref())
253+
== Some(&GetAssertionLargeBlobExtension::Read);
254+
if large_blob_read_requested {
255+
for assertion in assertions.iter_mut() {
256+
let Some(key_vec) = assertion.large_blob_key.as_ref() else {
257+
continue;
258+
};
259+
let Ok(key) = <[u8; 32]>::try_from(key_vec.as_slice()) else {
260+
warn!(
261+
"Device returned largeBlobKey of unexpected length {} (expected 32). Skipping largeBlob fetch.",
262+
key_vec.len()
263+
);
264+
continue;
265+
};
266+
let credential_id = assertion
267+
.credential_id
268+
.as_ref()
269+
.map(|c| c.id.to_vec())
270+
.unwrap_or_default();
271+
let storage = AuthenticatorLargeBlobStorage::new(
272+
self,
273+
credential_id.clone(),
274+
key,
275+
op.timeout,
276+
);
277+
let blob = match storage.read(&credential_id).await {
278+
Ok(b) => b,
279+
Err(e) => {
280+
warn!(
281+
?e,
282+
"authenticatorLargeBlobs(get) failed; returning no blob to RP"
283+
);
284+
None
285+
}
286+
};
287+
let entry = GetAssertionLargeBlobExtensionOutput { blob };
288+
match assertion.unsigned_extensions_output.as_mut() {
289+
Some(unsigned) => {
290+
unsigned.large_blob = Some(entry);
291+
}
292+
None => {
293+
assertion.unsigned_extensions_output =
294+
Some(GetAssertionResponseUnsignedExtensions {
295+
hmac_get_secret: None,
296+
large_blob: Some(entry),
297+
prf: None,
298+
});
299+
}
300+
}
301+
}
302+
}
303+
238304
Ok(assertions.as_slice().into())
239305
}
240306

0 commit comments

Comments
 (0)