Skip to content

Commit c6110f5

Browse files
feat(ctap2): add authenticatorLargeBlobs command (0x0C)
Implements the wire-level model and protocol method for CTAP 2.1 `authenticatorLargeBlobs` (command code 0x0C, spec §6.10). This is the device-side primitive the platform uses to fetch and update the authenticator's serialized largeBlobArray. Includes only the `get` request shape so far; `set` is reserved for a follow-up that will also handle the pinUvAuthParam binding required for writes. Refs: CTAP 2.2 §6.10.
1 parent 8c71a77 commit c6110f5

5 files changed

Lines changed: 118 additions & 2 deletions

File tree

libwebauthn/src/proto/ctap2/cbor/request.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::proto::ctap2::model::Ctap2MakeCredentialRequest;
88
use crate::proto::ctap2::Ctap2AuthenticatorConfigRequest;
99
use crate::proto::ctap2::Ctap2BioEnrollmentRequest;
1010
use crate::proto::ctap2::Ctap2CredentialManagementRequest;
11+
use crate::proto::ctap2::Ctap2LargeBlobsRequest;
1112
use crate::webauthn::Error;
1213

1314
#[derive(Debug, Clone, PartialEq)]
@@ -106,3 +107,13 @@ impl TryFrom<&Ctap2CredentialManagementRequest> for CborRequest {
106107
})
107108
}
108109
}
110+
111+
impl TryFrom<&Ctap2LargeBlobsRequest> for CborRequest {
112+
type Error = Error;
113+
fn try_from(request: &Ctap2LargeBlobsRequest) -> Result<CborRequest, Error> {
114+
Ok(CborRequest {
115+
command: Ctap2CommandCode::AuthenticatorLargeBlobs,
116+
encoded_data: cbor::to_vec(&request)?,
117+
})
118+
}
119+
}

libwebauthn/src/proto/ctap2/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub use model::{
3232
pub use model::{
3333
Ctap2GetAssertionRequest, Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions,
3434
};
35+
pub use model::{Ctap2LargeBlobsRequest, Ctap2LargeBlobsResponse};
3536
pub use model::{
3637
Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse, Ctap2MakeCredentialsResponseExtensions,
3738
};

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub use credential_management::{
4444
Ctap2CredentialData, Ctap2CredentialManagementMetadata, Ctap2CredentialManagementRequest,
4545
Ctap2CredentialManagementResponse, Ctap2RPData,
4646
};
47+
mod large_blobs;
48+
pub use large_blobs::{Ctap2LargeBlobsRequest, Ctap2LargeBlobsResponse};
4749

4850
#[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq, Serialize_repr)]
4951
#[repr(u8)]
@@ -58,6 +60,7 @@ pub enum Ctap2CommandCode {
5860
AuthenticatorCredentialManagement = 0x0A,
5961
AuthenticatorCredentialManagementPreview = 0x41,
6062
AuthenticatorSelection = 0x0B,
63+
AuthenticatorLargeBlobs = 0x0C,
6164
AuthenticatorConfig = 0x0D,
6265
}
6366

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! CTAP 2.1 `authenticatorLargeBlobs` command (`0x0C`). Wire-level model only;
2+
//! see [`crate::ops::webauthn::large_blob`] for the high-level read pipeline.
3+
4+
use serde_bytes::ByteBuf;
5+
use serde_indexed::{DeserializeIndexed, SerializeIndexed};
6+
7+
/// Request parameters. `get` (read) and `set` (write) are mutually exclusive.
8+
#[derive(Debug, Clone, SerializeIndexed)]
9+
pub struct Ctap2LargeBlobsRequest {
10+
#[serde(skip_serializing_if = "Option::is_none")]
11+
#[serde(index = 0x01)]
12+
pub get: Option<u32>,
13+
14+
#[serde(skip_serializing_if = "Option::is_none")]
15+
#[serde(index = 0x02)]
16+
pub set: Option<ByteBuf>,
17+
18+
#[serde(index = 0x03)]
19+
pub offset: u32,
20+
21+
#[serde(skip_serializing_if = "Option::is_none")]
22+
#[serde(index = 0x04)]
23+
pub length: Option<u32>,
24+
25+
#[serde(skip_serializing_if = "Option::is_none")]
26+
#[serde(index = 0x05)]
27+
pub pin_uv_auth_param: Option<ByteBuf>,
28+
29+
#[serde(skip_serializing_if = "Option::is_none")]
30+
#[serde(index = 0x06)]
31+
pub pin_uv_auth_protocol: Option<u32>,
32+
}
33+
34+
impl Ctap2LargeBlobsRequest {
35+
pub fn new_get(offset: u32, length: u32) -> Self {
36+
Self {
37+
get: Some(length),
38+
set: None,
39+
offset,
40+
length: None,
41+
pin_uv_auth_param: None,
42+
pin_uv_auth_protocol: None,
43+
}
44+
}
45+
}
46+
47+
#[cfg_attr(test, derive(SerializeIndexed))]
48+
#[derive(Debug, Default, Clone, DeserializeIndexed)]
49+
pub struct Ctap2LargeBlobsResponse {
50+
#[serde(skip_serializing_if = "Option::is_none")]
51+
#[serde(index = 0x01)]
52+
pub config: Option<ByteBuf>,
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use super::*;
58+
use crate::proto::ctap2::cbor;
59+
60+
#[test]
61+
fn get_request_round_trips_through_cbor() {
62+
let req = Ctap2LargeBlobsRequest::new_get(0, 1024);
63+
let bytes = cbor::to_vec(&req).expect("serialize");
64+
assert_eq!(bytes[0], 0xa2, "expected CBOR map of two items");
65+
let value: cbor::Value = cbor::from_slice(&bytes).expect("deserialize");
66+
let cbor::Value::Map(map) = value else {
67+
panic!("expected map");
68+
};
69+
assert_eq!(map.len(), 2);
70+
}
71+
}

libwebauthn/src/proto/ctap2/protocol.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use super::model::Ctap2ClientPinResponse;
1515
use super::{
1616
Ctap2AuthenticatorConfigRequest, Ctap2BioEnrollmentRequest, Ctap2ClientPinRequest,
1717
Ctap2CredentialManagementRequest, Ctap2CredentialManagementResponse, Ctap2GetAssertionRequest,
18-
Ctap2GetAssertionResponse, Ctap2GetInfoResponse, Ctap2MakeCredentialRequest,
19-
Ctap2MakeCredentialResponse,
18+
Ctap2GetAssertionResponse, Ctap2GetInfoResponse, Ctap2LargeBlobsRequest,
19+
Ctap2LargeBlobsResponse, Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse,
2020
};
2121

2222
const TIMEOUT_GET_INFO: Duration = Duration::from_millis(250);
@@ -90,6 +90,11 @@ pub trait Ctap2 {
9090
request: &Ctap2CredentialManagementRequest,
9191
timeout: Duration,
9292
) -> Result<Ctap2CredentialManagementResponse, Error>;
93+
async fn ctap2_large_blobs(
94+
&mut self,
95+
request: &Ctap2LargeBlobsRequest,
96+
timeout: Duration,
97+
) -> Result<Ctap2LargeBlobsResponse, Error>;
9398
}
9499

95100
#[async_trait]
@@ -284,6 +289,31 @@ where
284289
Ok(Ctap2CredentialManagementResponse::default())
285290
}
286291
}
292+
293+
#[instrument(skip_all)]
294+
async fn ctap2_large_blobs(
295+
&mut self,
296+
request: &Ctap2LargeBlobsRequest,
297+
timeout: Duration,
298+
) -> Result<Ctap2LargeBlobsResponse, Error> {
299+
trace!(?request);
300+
self.cbor_send(&request.try_into()?, timeout).await?;
301+
let cbor_response = self.cbor_recv(timeout).await?;
302+
match cbor_response.status_code {
303+
CtapError::Ok => (),
304+
error => return Err(Error::Ctap(error)),
305+
};
306+
if let Some(data) = cbor_response.data {
307+
let ctap_response = parse_cbor!(Ctap2LargeBlobsResponse, &data);
308+
debug!("CTAP2 LargeBlobs successful");
309+
trace!(?ctap_response);
310+
Ok(ctap_response)
311+
} else {
312+
// Write responses carry no body; same serde_indexed workaround as
313+
// credential_management above.
314+
Ok(Ctap2LargeBlobsResponse::default())
315+
}
316+
}
287317
}
288318

289319
#[cfg(test)]

0 commit comments

Comments
 (0)