Skip to content

Commit fa2aa83

Browse files
feat(ctap2): authenticatorLargeBlobs(set) chunked write constructors
Add new_set_first and new_set_continuation per CTAP 2.2 §6.10.2: length is present only when offset is zero, omitted otherwise. Both take Option<(auth_param, protocol)> so unprotected authenticators can issue the command without pinUvAuthParam.
1 parent 14895c1 commit fa2aa83

1 file changed

Lines changed: 109 additions & 1 deletion

File tree

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

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! CTAP 2.1 `authenticatorLargeBlobs` command (`0x0C`). Wire-level model only;
1+
//! CTAP 2.2 `authenticatorLargeBlobs` command (`0x0C`). Wire-level model only;
22
//! see [`crate::ops::webauthn::large_blob`] for the high-level read pipeline.
33
44
use serde_bytes::ByteBuf;
@@ -42,6 +42,48 @@ impl Ctap2LargeBlobsRequest {
4242
pin_uv_auth_protocol: None,
4343
}
4444
}
45+
46+
/// First chunk of a chunked write. CTAP 2.2 §6.10.2 requires `length` only when `offset == 0`.
47+
/// Pass `None` for `pin_uv_auth` on unprotected authenticators (no clientPin, no built-in UV).
48+
pub fn new_set_first(
49+
chunk: Vec<u8>,
50+
total_length: u32,
51+
pin_uv_auth: Option<(Vec<u8>, u32)>,
52+
) -> Self {
53+
let (pin_uv_auth_param, pin_uv_auth_protocol) = match pin_uv_auth {
54+
Some((p, v)) => (Some(ByteBuf::from(p)), Some(v)),
55+
None => (None, None),
56+
};
57+
Self {
58+
get: None,
59+
set: Some(ByteBuf::from(chunk)),
60+
offset: 0,
61+
length: Some(total_length),
62+
pin_uv_auth_param,
63+
pin_uv_auth_protocol,
64+
}
65+
}
66+
67+
/// Continuation chunk. CTAP 2.2 §6.10.2 forbids `length` when `offset != 0`.
68+
/// Pass `None` for `pin_uv_auth` on unprotected authenticators.
69+
pub fn new_set_continuation(
70+
chunk: Vec<u8>,
71+
offset: u32,
72+
pin_uv_auth: Option<(Vec<u8>, u32)>,
73+
) -> Self {
74+
let (pin_uv_auth_param, pin_uv_auth_protocol) = match pin_uv_auth {
75+
Some((p, v)) => (Some(ByteBuf::from(p)), Some(v)),
76+
None => (None, None),
77+
};
78+
Self {
79+
get: None,
80+
set: Some(ByteBuf::from(chunk)),
81+
offset,
82+
length: None,
83+
pin_uv_auth_param,
84+
pin_uv_auth_protocol,
85+
}
86+
}
4587
}
4688

4789
#[cfg_attr(test, derive(SerializeIndexed))]
@@ -68,4 +110,70 @@ mod tests {
68110
};
69111
assert_eq!(map.len(), 2);
70112
}
113+
114+
#[test]
115+
fn set_first_encodes_length_and_offset_zero() {
116+
let req =
117+
Ctap2LargeBlobsRequest::new_set_first(vec![0x01, 0x02], 17, Some((vec![0xAA; 16], 2)));
118+
let bytes = cbor::to_vec(&req).expect("serialize");
119+
let value: cbor::Value = cbor::from_slice(&bytes).expect("deserialize");
120+
let cbor::Value::Map(map) = value else {
121+
panic!("expected map");
122+
};
123+
let pairs: std::collections::BTreeMap<_, _> = map
124+
.into_iter()
125+
.filter_map(|(k, v)| match k {
126+
cbor::Value::Integer(i) => Some((i, v)),
127+
_ => None,
128+
})
129+
.collect();
130+
assert!(matches!(pairs.get(&0x02), Some(cbor::Value::Bytes(_))));
131+
assert_eq!(pairs.get(&0x03), Some(&cbor::Value::Integer(0)));
132+
assert_eq!(pairs.get(&0x04), Some(&cbor::Value::Integer(17)));
133+
assert!(matches!(pairs.get(&0x05), Some(cbor::Value::Bytes(_))));
134+
assert_eq!(pairs.get(&0x06), Some(&cbor::Value::Integer(2)));
135+
assert!(!pairs.contains_key(&0x01), "get must not be present");
136+
}
137+
138+
#[test]
139+
fn set_continuation_omits_length() {
140+
let req =
141+
Ctap2LargeBlobsRequest::new_set_continuation(vec![0xFF], 64, Some((vec![0xBB; 16], 2)));
142+
let bytes = cbor::to_vec(&req).expect("serialize");
143+
let value: cbor::Value = cbor::from_slice(&bytes).expect("deserialize");
144+
let cbor::Value::Map(map) = value else {
145+
panic!("expected map");
146+
};
147+
let pairs: std::collections::BTreeMap<_, _> = map
148+
.into_iter()
149+
.filter_map(|(k, v)| match k {
150+
cbor::Value::Integer(i) => Some((i, v)),
151+
_ => None,
152+
})
153+
.collect();
154+
assert_eq!(pairs.get(&0x03), Some(&cbor::Value::Integer(64)));
155+
assert!(!pairs.contains_key(&0x04), "length must be absent");
156+
assert!(matches!(pairs.get(&0x02), Some(cbor::Value::Bytes(_))));
157+
}
158+
159+
#[test]
160+
fn set_first_unauthenticated_omits_auth_params() {
161+
// CTAP 2.2 §6.10.2: unprotected authenticators skip the pinUvAuth verification block,
162+
// so the platform omits both pinUvAuthParam and pinUvAuthProtocol.
163+
let req = Ctap2LargeBlobsRequest::new_set_first(vec![0x01, 0x02], 17, None);
164+
let bytes = cbor::to_vec(&req).expect("serialize");
165+
let value: cbor::Value = cbor::from_slice(&bytes).expect("deserialize");
166+
let cbor::Value::Map(map) = value else {
167+
panic!("expected map");
168+
};
169+
let pairs: std::collections::BTreeMap<_, _> = map
170+
.into_iter()
171+
.filter_map(|(k, v)| match k {
172+
cbor::Value::Integer(i) => Some((i, v)),
173+
_ => None,
174+
})
175+
.collect();
176+
assert!(!pairs.contains_key(&0x05));
177+
assert!(!pairs.contains_key(&0x06));
178+
}
71179
}

0 commit comments

Comments
 (0)