@@ -214,7 +214,7 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON> for GetAssertionRequest
214214 . and_then ( |e| e. large_blob . as_ref ( ) )
215215 {
216216 // L3 §10.1.5: largeBlob without read=true or support is a no-op, not an error.
217- Some ( lb) if lb. support . is_none ( ) && lb. read != Some ( true ) => None ,
217+ Some ( lb) if lb. support . is_none ( ) && lb. read != Some ( true ) && lb . write . is_none ( ) => None ,
218218 Some ( lb) => Some ( GetAssertionLargeBlobExtension :: try_from ( lb. clone ( ) ) ?) ,
219219 None => None ,
220220 } ;
@@ -336,9 +336,12 @@ impl TryFrom<HmacGetSecretInputJson> for HMACGetSecretInput {
336336
337337#[ derive( Debug , Clone , PartialEq , Eq ) ]
338338pub 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
344347impl 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 present together is an error.
357+ if value. read . is_some ( ) && 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 {
366375pub 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
@@ -596,6 +604,15 @@ impl DowngradableRequest<Vec<SignRequest>> for GetAssertionRequest {
596604 return false ;
597605 }
598606
607+ if matches ! (
608+ self . extensions. as_ref( ) . and_then( |e| e. large_blob. as_ref( ) ) ,
609+ Some ( GetAssertionLargeBlobExtension :: Write ( _) )
610+ | Some ( GetAssertionLargeBlobExtension :: Delete )
611+ ) {
612+ debug ! ( "Not downgradable: largeBlob write/delete requires FIDO2" ) ;
613+ return false ;
614+ }
615+
599616 true
600617 }
601618
@@ -1401,6 +1418,7 @@ mod tests {
14011418 GetAssertionLargeBlobExtension :: try_from( LargeBlobInputJson {
14021419 support: None ,
14031420 read: Some ( true ) ,
1421+ write: None ,
14041422 } )
14051423 . unwrap( ) ,
14061424 GetAssertionLargeBlobExtension :: Read
@@ -1409,6 +1427,7 @@ mod tests {
14091427 GetAssertionLargeBlobExtension :: try_from( LargeBlobInputJson {
14101428 support: Some ( "required" . to_string( ) ) ,
14111429 read: Some ( true ) ,
1430+ write: None ,
14121431 } ) ,
14131432 Err ( GetAssertionPrepareError :: NotSupported ( _) )
14141433 ) ) ;
@@ -1445,4 +1464,28 @@ mod tests {
14451464 . expect ( "largeBlob.read=false must be a no-op, not an error" ) ;
14461465 assert ! ( req. extensions. and_then( |e| e. large_blob) . is_none( ) ) ;
14471466 }
1467+
1468+ #[ test]
1469+ fn large_blob_json_write_input_and_mutual_exclusion ( ) {
1470+ use crate :: ops:: webauthn:: idl:: get:: LargeBlobInputJson ;
1471+
1472+ let blob = b"blob to write" . to_vec ( ) ;
1473+ assert_eq ! (
1474+ GetAssertionLargeBlobExtension :: try_from( LargeBlobInputJson {
1475+ support: None ,
1476+ read: None ,
1477+ write: Some ( Base64UrlString :: from( blob. clone( ) ) ) ,
1478+ } )
1479+ . unwrap( ) ,
1480+ GetAssertionLargeBlobExtension :: Write ( blob)
1481+ ) ;
1482+ assert ! ( matches!(
1483+ GetAssertionLargeBlobExtension :: try_from( LargeBlobInputJson {
1484+ support: None ,
1485+ read: Some ( true ) ,
1486+ write: Some ( Base64UrlString :: from( b"x" . to_vec( ) ) ) ,
1487+ } ) ,
1488+ Err ( GetAssertionPrepareError :: NotSupported ( _) )
1489+ ) ) ;
1490+ }
14481491}
0 commit comments