Skip to content

Commit d4eb066

Browse files
feat(fido): model backup eligibility and state flags (#272)
Two authenticator-data flag bits carry backup eligibility and backup state but were modeled as reserved. This names them and adds read-only accessors derived from the parsed flags. The signed bytes are still returned verbatim, so signed output is unchanged. Closes #255.
1 parent c9d6b77 commit d4eb066

1 file changed

Lines changed: 58 additions & 4 deletions

File tree

libwebauthn/src/fido.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ bitflags! {
6363
const USER_PRESENT = 0x01;
6464
const RFU_1 = 0x02;
6565
const USER_VERIFIED = 0x04;
66-
const RFU_2_1 = 0x08;
67-
const RFU_2_2 = 0x10;
66+
const BACKUP_ELIGIBILITY = 0x08;
67+
const BACKUP_STATE = 0x10;
6868
const RFU_2_3 = 0x20;
6969
const ATTESTED_CREDENTIALS = 0x40;
7070
const EXTENSION_DATA = 0x80;
@@ -107,6 +107,19 @@ pub struct AuthenticatorData<T> {
107107
pub raw: Option<Vec<u8>>,
108108
}
109109

110+
impl<T> AuthenticatorData<T> {
111+
/// Backup Eligibility (BE): the credential may be backed up or synced.
112+
pub fn backup_eligible(&self) -> bool {
113+
self.flags
114+
.contains(AuthenticatorDataFlags::BACKUP_ELIGIBILITY)
115+
}
116+
117+
/// Backup State (BS): the credential is currently backed up.
118+
pub fn backed_up(&self) -> bool {
119+
self.flags.contains(AuthenticatorDataFlags::BACKUP_STATE)
120+
}
121+
}
122+
110123
impl<T> AuthenticatorData<T>
111124
where
112125
T: Clone + Serialize,
@@ -320,9 +333,11 @@ mod tests {
320333
0xe2, 0x75, 0x1e, 0x68, 0x2f, 0xab, 0x9f, 0x2d, 0x30, 0xab, 0x13, 0xd2, 0x12, 0x55,
321334
0x86, 0xce, 0x19, 0x47,
322335
];
323-
let flag_bits = 0b1100_0101;
336+
let flag_bits = 0b1101_1101;
324337
let flags = AuthenticatorDataFlags::USER_PRESENT
325338
| AuthenticatorDataFlags::USER_VERIFIED
339+
| AuthenticatorDataFlags::BACKUP_ELIGIBILITY
340+
| AuthenticatorDataFlags::BACKUP_STATE
326341
| AuthenticatorDataFlags::ATTESTED_CREDENTIALS
327342
| AuthenticatorDataFlags::EXTENSION_DATA;
328343
assert_eq!(flag_bits, flags.bits());
@@ -388,6 +403,14 @@ mod tests {
388403
cbor::from_slice(authdata_wrapped.as_slice()).unwrap();
389404
assert_eq!(auth_data.rp_id_hash, auth_data_reparsed.rp_id_hash);
390405
assert_eq!(auth_data.flags.bits(), auth_data_reparsed.flags.bits());
406+
assert!(auth_data_reparsed
407+
.flags
408+
.contains(AuthenticatorDataFlags::BACKUP_ELIGIBILITY));
409+
assert!(auth_data_reparsed
410+
.flags
411+
.contains(AuthenticatorDataFlags::BACKUP_STATE));
412+
assert!(auth_data_reparsed.backup_eligible());
413+
assert!(auth_data_reparsed.backed_up());
391414
assert_eq!(
392415
auth_data.signature_count,
393416
auth_data_reparsed.signature_count
@@ -468,7 +491,10 @@ mod tests {
468491
// signature verification. The bytes must round-trip unchanged.
469492
use crate::proto::ctap2::Ctap2MakeCredentialsResponseExtensions;
470493

471-
let flags = AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::EXTENSION_DATA;
494+
let flags = AuthenticatorDataFlags::USER_PRESENT
495+
| AuthenticatorDataFlags::BACKUP_ELIGIBILITY
496+
| AuthenticatorDataFlags::BACKUP_STATE
497+
| AuthenticatorDataFlags::EXTENSION_DATA;
472498
let mut input = [0x11u8; 32].to_vec();
473499
input.push(flags.bits());
474500
input.extend_from_slice(&[0x00, 0x00, 0x00, 0x07]); // signCount
@@ -486,10 +512,38 @@ mod tests {
486512
let parsed: AuthenticatorData<Ctap2MakeCredentialsResponseExtensions> =
487513
cbor::from_slice(&wrapped).unwrap();
488514

515+
assert!(parsed.backup_eligible());
516+
assert!(parsed.backed_up());
517+
489518
assert_eq!(
490519
parsed.to_response_bytes().unwrap(),
491520
input,
492521
"authenticatorData must be preserved byte-for-byte"
493522
);
494523
}
524+
525+
#[test]
526+
fn backup_flags_are_distinct_bits() {
527+
// BE and BS are separate bits per WebAuthn L3 section 6.1. Each accessor
528+
// must read its own bit, so a swap or a wrong wiring is caught.
529+
assert_eq!(AuthenticatorDataFlags::BACKUP_ELIGIBILITY.bits(), 0x08);
530+
assert_eq!(AuthenticatorDataFlags::BACKUP_STATE.bits(), 0x10);
531+
532+
let with_flags = |flags| AuthenticatorData::<()> {
533+
rp_id_hash: [0u8; 32],
534+
flags,
535+
signature_count: 0,
536+
attested_credential: None,
537+
extensions: None,
538+
raw: None,
539+
};
540+
541+
let be_only = with_flags(AuthenticatorDataFlags::BACKUP_ELIGIBILITY);
542+
assert!(be_only.backup_eligible());
543+
assert!(!be_only.backed_up());
544+
545+
let bs_only = with_flags(AuthenticatorDataFlags::BACKUP_STATE);
546+
assert!(!bs_only.backup_eligible());
547+
assert!(bs_only.backed_up());
548+
}
495549
}

0 commit comments

Comments
 (0)