You've already forked ctap-types
mirror of
https://github.com/trussed-dev/ctap-types.git
synced 2026-06-20 04:16:17 -07:00
ctap2.1: add wire types for credBlob, minPinLength, authenticatorConfig
This commit is contained in:
committed by
Robin Krahl
parent
94c5b5586e
commit
e5bd6cabc9
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.5.0...HEAD
|
||||
|
||||
- `ctap2::get_info`: Fix field order of the `CtapOptions` and `Certifications` structs to produce canonical CBOR
|
||||
- Add support for missing CTAP 2.1 features:
|
||||
- Add `AuthenticatorConfig` command.
|
||||
- Add `credBlob` extension and split `make_credential::Extensions` into `ExtensionsInput` and `ExtensionsOutput`.
|
||||
- Add `minPinLength` extension.
|
||||
|
||||
## [0.5.0] 2026-03-23
|
||||
|
||||
|
||||
@@ -294,6 +294,66 @@ impl<'a> Arbitrary<'a> for webauthn::PublicKeyCredentialUserEntity {
|
||||
}
|
||||
}
|
||||
|
||||
// cannot be derived because of missing impl for &'a serde_bytes::Bytes (cred_blob).
|
||||
// Mirrors the make_credential::Request handling of `pin_auth: Option<&Bytes>`.
|
||||
impl<'a> Arbitrary<'a> for ctap2::make_credential::ExtensionsInput<'a> {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
|
||||
let cred_protect = u.arbitrary()?;
|
||||
let hmac_secret = u.arbitrary()?;
|
||||
let large_blob_key = u.arbitrary()?;
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
let third_party_payment = u.arbitrary()?;
|
||||
let cred_blob = if bool::arbitrary(u)? {
|
||||
Some(serde_bytes::Bytes::new(u.arbitrary()?))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Self {
|
||||
cred_protect,
|
||||
hmac_secret,
|
||||
large_blob_key,
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
third_party_payment,
|
||||
cred_blob,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cannot be derived because of missing impl for Vec<&'a str, N> (rp ID list).
|
||||
impl<'a> Arbitrary<'a> for ctap2::authenticator_config::SubcommandParameters<'a> {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
|
||||
let new_min_pin_length = u.arbitrary()?;
|
||||
let min_pin_length_rp_ids = arbitrary_option(u, arbitrary_vec)?;
|
||||
let force_change_pin = u.arbitrary()?;
|
||||
Ok(Self {
|
||||
new_min_pin_length,
|
||||
min_pin_length_rp_ids,
|
||||
force_change_pin,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cannot be derived because of missing impl for &'a serde_bytes::Bytes (pin_auth)
|
||||
// + Vec<_> in SubcommandParameters.
|
||||
impl<'a> Arbitrary<'a> for ctap2::authenticator_config::Request<'a> {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
|
||||
let sub_command = u.arbitrary()?;
|
||||
let sub_command_params = u.arbitrary()?;
|
||||
let pin_protocol = u.arbitrary()?;
|
||||
let pin_auth = if bool::arbitrary(u)? {
|
||||
Some(serde_bytes::Bytes::new(u.arbitrary()?))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Self {
|
||||
sub_command,
|
||||
sub_command_params,
|
||||
pin_protocol,
|
||||
pin_auth,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_byte_array<'a, const N: usize>(u: &mut Unstructured<'_>) -> Result<&'a ByteArray<N>> {
|
||||
let bytes: &[u8; N] = u.bytes(N)?.try_into().unwrap();
|
||||
// TODO: conversion should be provided by serde_bytes
|
||||
|
||||
+24
-2
@@ -11,6 +11,7 @@ use crate::{sizes::*, Bytes, TryFromStrError};
|
||||
|
||||
pub use crate::operation::{Operation, VendorOperation};
|
||||
|
||||
pub mod authenticator_config;
|
||||
pub mod client_pin;
|
||||
pub mod credential_management;
|
||||
pub mod get_assertion;
|
||||
@@ -45,6 +46,8 @@ pub enum Request<'a> {
|
||||
Selection,
|
||||
// 0xC
|
||||
LargeBlobs(large_blobs::Request<'a>),
|
||||
// 0xD
|
||||
AuthenticatorConfig(authenticator_config::Request<'a>),
|
||||
// vendor, to be embellished
|
||||
// Q: how to handle the associated CBOR structures
|
||||
Vendor(crate::operation::VendorOperation),
|
||||
@@ -118,10 +121,14 @@ impl<'a> Request<'a> {
|
||||
Request::LargeBlobs(cbor_deserialize(data).map_err(CtapMappingError::ParsingError)?)
|
||||
}
|
||||
|
||||
Operation::Config => Request::AuthenticatorConfig(
|
||||
cbor_deserialize(data).map_err(CtapMappingError::ParsingError)?,
|
||||
),
|
||||
|
||||
// NB: FIDO Alliance "stole" 0x40 and 0x41, so these are not available
|
||||
Operation::Vendor(vendor_operation) => Request::Vendor(vendor_operation),
|
||||
|
||||
Operation::BioEnrollment | Operation::PreviewBioEnrollment | Operation::Config => {
|
||||
Operation::BioEnrollment | Operation::PreviewBioEnrollment => {
|
||||
debug_now!("unhandled CBOR operation {:?}", operation);
|
||||
return Err(CtapMappingError::InvalidCommand(op).into());
|
||||
}
|
||||
@@ -143,6 +150,7 @@ pub enum Response {
|
||||
Selection,
|
||||
CredentialManagement(credential_management::Response),
|
||||
LargeBlobs(large_blobs::Response),
|
||||
AuthenticatorConfig,
|
||||
// Q: how to handle the associated CBOR structures
|
||||
Vendor,
|
||||
}
|
||||
@@ -161,7 +169,7 @@ impl Response {
|
||||
GetAssertion(response) | GetNextAssertion(response) => cbor_serialize(response, data),
|
||||
CredentialManagement(response) => cbor_serialize(response, data),
|
||||
LargeBlobs(response) => cbor_serialize(response, data),
|
||||
Reset | Selection | Vendor => Ok([].as_slice()),
|
||||
Reset | Selection | AuthenticatorConfig | Vendor => Ok([].as_slice()),
|
||||
};
|
||||
if let Ok(slice) = outcome {
|
||||
*status = 0;
|
||||
@@ -445,6 +453,11 @@ pub trait Authenticator {
|
||||
Err(Error::InvalidCommand)
|
||||
}
|
||||
|
||||
fn authenticator_config(&mut self, request: &authenticator_config::Request) -> Result<()> {
|
||||
let _ = request;
|
||||
Err(Error::InvalidCommand)
|
||||
}
|
||||
|
||||
/// Dispatches the enum of possible requests into the appropriate trait method.
|
||||
#[inline(never)]
|
||||
fn call_ctap2(&mut self, request: &Request) -> Result<Response> {
|
||||
@@ -533,6 +546,15 @@ pub trait Authenticator {
|
||||
))
|
||||
}
|
||||
|
||||
// 0xD
|
||||
Request::AuthenticatorConfig(request) => {
|
||||
debug_now!("CTAP2.CFG");
|
||||
self.authenticator_config(request).inspect_err(|_e| {
|
||||
debug!("error: {:?}", _e);
|
||||
})?;
|
||||
Ok(Response::AuthenticatorConfig)
|
||||
}
|
||||
|
||||
// Not stable
|
||||
Request::Vendor(op) => {
|
||||
debug_now!("CTAP2.V");
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! `authenticatorConfig` (CTAP 2.1 §6.11), command `0x0D`.
|
||||
//!
|
||||
//! Carries `setMinPINLength`, `toggleAlwaysUv`, and (CTAP 2.3) `enableLongTouchForReset`
|
||||
//! sub-commands, plus the spec-required `enableEnterpriseAttestation` and
|
||||
//! `vendorPrototype` placeholders.
|
||||
|
||||
use serde_indexed::{DeserializeIndexed, SerializeIndexed};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
use crate::Vec;
|
||||
|
||||
pub const MAX_MIN_PIN_LENGTH_RP_IDS: usize = 4;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize_repr, Deserialize_repr)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
pub enum Subcommand {
|
||||
EnableEnterpriseAttestation = 0x01,
|
||||
ToggleAlwaysUv = 0x02,
|
||||
SetMinPINLength = 0x03,
|
||||
VendorPrototype = 0xff,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)]
|
||||
#[non_exhaustive]
|
||||
#[serde_indexed(offset = 1)]
|
||||
pub struct SubcommandParameters<'a> {
|
||||
// 0x01
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub new_min_pin_length: Option<u8>,
|
||||
// 0x02
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_pin_length_rp_ids: Option<Vec<&'a str, MAX_MIN_PIN_LENGTH_RP_IDS>>,
|
||||
// 0x03
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub force_change_pin: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)]
|
||||
#[non_exhaustive]
|
||||
#[serde_indexed(offset = 1)]
|
||||
pub struct Request<'a> {
|
||||
// 0x01
|
||||
pub sub_command: Subcommand,
|
||||
// 0x02
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sub_command_params: Option<SubcommandParameters<'a>>,
|
||||
// 0x03
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pin_protocol: Option<u8>,
|
||||
// 0x04
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pin_auth: Option<&'a serde_bytes::Bytes>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_test::{assert_de_tokens, assert_ser_tokens, assert_tokens, Token};
|
||||
|
||||
#[test]
|
||||
fn test_serde_subcommand() {
|
||||
for (sub, byte) in [
|
||||
(Subcommand::EnableEnterpriseAttestation, 0x01),
|
||||
(Subcommand::ToggleAlwaysUv, 0x02),
|
||||
(Subcommand::SetMinPINLength, 0x03),
|
||||
(Subcommand::VendorPrototype, 0xff),
|
||||
] {
|
||||
assert_tokens(&sub, &[Token::U8(byte)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_de_request_toggle_always_uv() {
|
||||
// A bare ToggleAlwaysUv request has no params and (typically) no pinAuth.
|
||||
let req = Request {
|
||||
sub_command: Subcommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_protocol: None,
|
||||
pin_auth: None,
|
||||
};
|
||||
assert_de_tokens(
|
||||
&req,
|
||||
&[
|
||||
Token::Map { len: Some(1) },
|
||||
Token::U64(1),
|
||||
Token::U8(0x02),
|
||||
Token::MapEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_de_request_set_min_pin_length() {
|
||||
let mut rp_ids = Vec::new();
|
||||
rp_ids.push("login.example.com").unwrap();
|
||||
let req = Request {
|
||||
sub_command: Subcommand::SetMinPINLength,
|
||||
sub_command_params: Some(SubcommandParameters {
|
||||
new_min_pin_length: Some(6),
|
||||
min_pin_length_rp_ids: Some(rp_ids),
|
||||
force_change_pin: Some(true),
|
||||
}),
|
||||
pin_protocol: Some(2),
|
||||
pin_auth: None,
|
||||
};
|
||||
// Deserialization side: serde_indexed reads each present key directly,
|
||||
// without `Token::Some` wrappers (presence of the key encodes Some).
|
||||
assert_de_tokens(
|
||||
&req,
|
||||
&[
|
||||
Token::Map { len: Some(3) },
|
||||
Token::U64(1),
|
||||
Token::U8(0x03),
|
||||
Token::U64(2),
|
||||
Token::Map { len: Some(3) },
|
||||
Token::U64(1),
|
||||
Token::U8(6),
|
||||
Token::U64(2),
|
||||
Token::Seq { len: Some(1) },
|
||||
Token::BorrowedStr("login.example.com"),
|
||||
Token::SeqEnd,
|
||||
Token::U64(3),
|
||||
Token::Bool(true),
|
||||
Token::MapEnd,
|
||||
Token::U64(3),
|
||||
Token::U8(2),
|
||||
Token::MapEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ser_request_set_min_pin_length() {
|
||||
let mut rp_ids = Vec::new();
|
||||
rp_ids.push("login.example.com").unwrap();
|
||||
let req = Request {
|
||||
sub_command: Subcommand::SetMinPINLength,
|
||||
sub_command_params: Some(SubcommandParameters {
|
||||
new_min_pin_length: Some(6),
|
||||
min_pin_length_rp_ids: Some(rp_ids),
|
||||
force_change_pin: Some(true),
|
||||
}),
|
||||
pin_protocol: Some(2),
|
||||
pin_auth: None,
|
||||
};
|
||||
// Serialization side: `Some` is emitted for each present optional.
|
||||
assert_ser_tokens(
|
||||
&req,
|
||||
&[
|
||||
Token::Map { len: Some(3) },
|
||||
Token::U64(1),
|
||||
Token::U8(0x03),
|
||||
Token::U64(2),
|
||||
Token::Some,
|
||||
Token::Map { len: Some(3) },
|
||||
Token::U64(1),
|
||||
Token::Some,
|
||||
Token::U8(6),
|
||||
Token::U64(2),
|
||||
Token::Some,
|
||||
Token::Seq { len: Some(1) },
|
||||
Token::BorrowedStr("login.example.com"),
|
||||
Token::SeqEnd,
|
||||
Token::U64(3),
|
||||
Token::Some,
|
||||
Token::Bool(true),
|
||||
Token::MapEnd,
|
||||
Token::U64(3),
|
||||
Token::Some,
|
||||
Token::U8(2),
|
||||
Token::MapEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ pub struct HmacSecretInput {
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[non_exhaustive]
|
||||
pub struct ExtensionsInput {
|
||||
/// `credBlob` (CTAP 2.1 §11.1) retrieval flag. `Some(true)` asks the
|
||||
/// authenticator to return the blob stored at MakeCredential time.
|
||||
#[serde(rename = "credBlob")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cred_blob: Option<bool>,
|
||||
|
||||
#[serde(rename = "hmac-secret")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hmac_secret: Option<HmacSecretInput>,
|
||||
@@ -42,6 +48,13 @@ pub struct ExtensionsInput {
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct ExtensionsOutput {
|
||||
/// `credBlob` retrieval result: the bytes stored at MakeCredential time, up
|
||||
/// to `maxCredBlobLength` (≥ 32). Absent if the platform did not request
|
||||
/// `credBlob` or if no blob is associated with the credential.
|
||||
#[serde(rename = "credBlob")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cred_blob: Option<Bytes<MAX_CRED_BLOB_LENGTH>>,
|
||||
|
||||
#[serde(rename = "hmac-secret")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
// *either* enc(output1) *or* enc(output1 || output2)
|
||||
@@ -57,6 +70,7 @@ impl ExtensionsOutput {
|
||||
#[inline]
|
||||
pub fn is_set(&self) -> bool {
|
||||
let Self {
|
||||
cred_blob,
|
||||
hmac_secret,
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
third_party_payment,
|
||||
@@ -64,6 +78,9 @@ impl ExtensionsOutput {
|
||||
if hmac_secret.is_some() {
|
||||
return true;
|
||||
}
|
||||
if cred_blob.is_some() {
|
||||
return true;
|
||||
}
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
if third_party_payment.is_some() {
|
||||
return true;
|
||||
@@ -181,6 +198,7 @@ mod tests {
|
||||
pin_protocol: Some(1),
|
||||
}),
|
||||
large_blob_key: Some(true),
|
||||
cred_blob: Some(true),
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
third_party_payment: Some(true),
|
||||
};
|
||||
@@ -191,6 +209,7 @@ mod tests {
|
||||
fn test_extensions_output_canonical() {
|
||||
let output = ExtensionsOutput {
|
||||
hmac_secret: Some([0xff; 80].try_into().unwrap()),
|
||||
cred_blob: Some([0xff; 32].try_into().unwrap()),
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
third_party_payment: Some(true),
|
||||
};
|
||||
|
||||
+13
-2
@@ -14,7 +14,7 @@ pub struct Response {
|
||||
|
||||
// 0x02
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extensions: Option<Vec<Extension, 4>>,
|
||||
pub extensions: Option<Vec<Extension, 6>>,
|
||||
|
||||
// 0x03
|
||||
pub aaguid: Bytes<16>,
|
||||
@@ -66,7 +66,7 @@ pub struct Response {
|
||||
// FIDO_2_1
|
||||
#[cfg(feature = "get-info-full")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_pin_length: Option<usize>,
|
||||
pub min_pin_length: Option<u8>,
|
||||
|
||||
// 0x0E
|
||||
// FIDO_2_1
|
||||
@@ -250,15 +250,19 @@ impl TryFrom<&str> for Version {
|
||||
#[serde(into = "&str", try_from = "&str")]
|
||||
pub enum Extension {
|
||||
CredProtect,
|
||||
CredBlob,
|
||||
HmacSecret,
|
||||
LargeBlobKey,
|
||||
MinPinLength,
|
||||
ThirdPartyPayment,
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
const CRED_PROTECT: &'static str = "credProtect";
|
||||
const CRED_BLOB: &'static str = "credBlob";
|
||||
const HMAC_SECRET: &'static str = "hmac-secret";
|
||||
const LARGE_BLOB_KEY: &'static str = "largeBlobKey";
|
||||
const MIN_PIN_LENGTH: &'static str = "minPinLength";
|
||||
const THIRD_PARTY_PAYMENT: &'static str = "thirdPartyPayment";
|
||||
}
|
||||
|
||||
@@ -266,8 +270,10 @@ impl From<Extension> for &str {
|
||||
fn from(extension: Extension) -> Self {
|
||||
match extension {
|
||||
Extension::CredProtect => Extension::CRED_PROTECT,
|
||||
Extension::CredBlob => Extension::CRED_BLOB,
|
||||
Extension::HmacSecret => Extension::HMAC_SECRET,
|
||||
Extension::LargeBlobKey => Extension::LARGE_BLOB_KEY,
|
||||
Extension::MinPinLength => Extension::MIN_PIN_LENGTH,
|
||||
Extension::ThirdPartyPayment => Extension::THIRD_PARTY_PAYMENT,
|
||||
}
|
||||
}
|
||||
@@ -279,8 +285,10 @@ impl TryFrom<&str> for Extension {
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
match s {
|
||||
Self::CRED_PROTECT => Ok(Self::CredProtect),
|
||||
Self::CRED_BLOB => Ok(Self::CredBlob),
|
||||
Self::HMAC_SECRET => Ok(Self::HmacSecret),
|
||||
Self::LARGE_BLOB_KEY => Ok(Self::LargeBlobKey),
|
||||
Self::MIN_PIN_LENGTH => Ok(Self::MinPinLength),
|
||||
Self::THIRD_PARTY_PAYMENT => Ok(Self::ThirdPartyPayment),
|
||||
_ => Err(TryFromStrError),
|
||||
}
|
||||
@@ -465,8 +473,11 @@ mod tests {
|
||||
fn test_serde_extension() {
|
||||
let extensions = [
|
||||
(Extension::CredProtect, "credProtect"),
|
||||
(Extension::CredBlob, "credBlob"),
|
||||
(Extension::HmacSecret, "hmac-secret"),
|
||||
(Extension::LargeBlobKey, "largeBlobKey"),
|
||||
(Extension::MinPinLength, "minPinLength"),
|
||||
(Extension::ThirdPartyPayment, "thirdPartyPayment"),
|
||||
];
|
||||
for (extension, s) in extensions {
|
||||
assert_tokens(&extension, &[Token::BorrowedStr(s)]);
|
||||
|
||||
@@ -24,10 +24,20 @@ impl TryFrom<u8> for CredentialProtectionPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions input to `authenticatorMakeCredential`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[non_exhaustive]
|
||||
pub struct Extensions {
|
||||
// `Arbitrary` impl lives in `crate::arbitrary` because `&'a serde_bytes::Bytes`
|
||||
// (cred_blob) doesn't satisfy `Arbitrary<'_>` and the derive macro can't
|
||||
// special-case it. Same pattern as `make_credential::Request<'a>`.
|
||||
pub struct ExtensionsInput<'a> {
|
||||
/// `credBlob` (CTAP 2.1 §11.1): platform-supplied bytes to associate with
|
||||
/// the credential. Up to `maxCredBlobLength` (≥ 32) bytes per credential.
|
||||
#[serde(rename = "credBlob")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(borrow)]
|
||||
pub cred_blob: Option<&'a serde_bytes::Bytes>,
|
||||
|
||||
#[serde(rename = "credProtect")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cred_protect: Option<u8>,
|
||||
@@ -47,6 +57,32 @@ pub struct Extensions {
|
||||
pub third_party_payment: Option<bool>,
|
||||
}
|
||||
|
||||
/// Extensions output emitted in `authenticatorData.extensions` after
|
||||
/// `authenticatorMakeCredential`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct ExtensionsOutput {
|
||||
/// `credBlob` storage acknowledgement: `Some(true)` if the platform-supplied
|
||||
/// blob was stored, `Some(false)` if not stored, absent if the platform did
|
||||
/// not request the extension.
|
||||
#[serde(rename = "credBlob")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cred_blob: Option<bool>,
|
||||
|
||||
#[serde(rename = "credProtect")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cred_protect: Option<u8>,
|
||||
|
||||
#[serde(rename = "hmac-secret")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hmac_secret: Option<bool>,
|
||||
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
#[serde(rename = "thirdPartyPayment")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub third_party_payment: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)]
|
||||
#[non_exhaustive]
|
||||
#[serde_indexed(offset = 1)]
|
||||
@@ -58,7 +94,7 @@ pub struct Request<'a> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclude_list: Option<Vec<PublicKeyCredentialDescriptorRef<'a>, 16>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extensions: Option<Extensions>,
|
||||
pub extensions: Option<ExtensionsInput<'a>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub options: Option<AuthenticatorOptions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -74,7 +110,7 @@ pub struct Request<'a> {
|
||||
pub type AttestationObject = Response;
|
||||
|
||||
pub type AuthenticatorData<'a> =
|
||||
super::AuthenticatorData<'a, AttestedCredentialData<'a>, Extensions>;
|
||||
super::AuthenticatorData<'a, AttestedCredentialData<'a>, ExtensionsOutput>;
|
||||
|
||||
// NOTE: This is not CBOR, it has a custom encoding...
|
||||
// https://www.w3.org/TR/webauthn/#sec-attested-credential-data
|
||||
@@ -179,13 +215,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extensions_canonical() {
|
||||
let extensions = Extensions {
|
||||
fn test_extensions_input_canonical() {
|
||||
let extensions = ExtensionsInput {
|
||||
cred_protect: Some(1),
|
||||
hmac_secret: Some(true),
|
||||
large_blob_key: Some(true),
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
third_party_payment: Some(true),
|
||||
cred_blob: Some(serde_bytes::Bytes::new(b"1234")),
|
||||
};
|
||||
crate::test::assert_canonical_cbor(&extensions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extensions_output_canonical() {
|
||||
let extensions = ExtensionsOutput {
|
||||
cred_protect: Some(1),
|
||||
hmac_secret: Some(true),
|
||||
#[cfg(feature = "third-party-payment")]
|
||||
third_party_payment: Some(true),
|
||||
cred_blob: Some(true),
|
||||
};
|
||||
crate::test::assert_canonical_cbor(&extensions);
|
||||
}
|
||||
|
||||
@@ -30,3 +30,5 @@ pub const THEORETICAL_MAX_MESSAGE_SIZE: usize = PACKET_SIZE - 7 + 128 * (PACKET_
|
||||
pub const LARGE_BLOB_MAX_FRAGMENT_LENGTH: usize = 0;
|
||||
#[cfg(feature = "large-blobs")]
|
||||
pub const LARGE_BLOB_MAX_FRAGMENT_LENGTH: usize = 3008;
|
||||
|
||||
pub const MAX_CRED_BLOB_LENGTH: usize = 32;
|
||||
|
||||
Reference in New Issue
Block a user