ctap2.1: implement credBlob extension (RK only, max 32 bytes)

This commit is contained in:
Emanuele Cesena
2026-05-22 16:53:22 +02:00
committed by Robin Krahl
parent d97e566631
commit fe3744990a
3 changed files with 60 additions and 3 deletions
+1
View File
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Update to `ctap-types` v0.6.0-rc.1.
- Set `algorithms`, `firmware_version` and `remaining_discoverable_credentials` in `get_info` and add `firmware_version` to `Config`.
- Implement the `credBlob` extension.
## [v0.3.0](https://github.com/trussed-dev/fido-authenticator/releases/tag/v0.3.0) (2026-03-25)
+19
View File
@@ -2,6 +2,7 @@
use core::cmp::Ordering;
use ctap_types::sizes::MAX_CRED_BLOB_LENGTH;
use serde::Serialize;
use serde_bytes::ByteArray;
use trussed_core::{
@@ -197,6 +198,13 @@ impl Credential {
Self::Stripped(credential) => credential.third_party_payment,
}
}
pub fn cred_blob(&self) -> Option<&Bytes<MAX_CRED_BLOB_LENGTH>> {
match self {
Self::Full(credential) => credential.data.cred_blob.as_ref(),
Self::Stripped(_) => None,
}
}
}
fn deserialize_bytes<E: serde::de::Error, const N: usize>(
@@ -518,6 +526,11 @@ pub struct CredentialData {
#[serde(skip_serializing_if = "Option::is_none")]
pub third_party_payment: Option<bool>,
/// `credBlob` extension (CTAP 2.1 §11.1) — platform-supplied bytes
/// associated with this credential, up to `MAX_CRED_BLOB_LENGTH` bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub cred_blob: Option<Bytes<MAX_CRED_BLOB_LENGTH>>,
}
// TODO: figure out sizes
@@ -612,6 +625,7 @@ impl FullCredential {
cred_protect: Option<CredentialProtectionPolicy>,
large_blob_key: Option<ByteArray<32>>,
third_party_payment: Option<bool>,
cred_blob: Option<Bytes<MAX_CRED_BLOB_LENGTH>>,
nonce: [u8; 12],
) -> Self {
info!("credential for algorithm {}", algorithm);
@@ -628,6 +642,7 @@ impl FullCredential {
cred_protect,
large_blob_key,
third_party_payment,
cred_blob,
use_short_id: Some(true),
};
@@ -814,6 +829,7 @@ mod test {
use_short_id: Some(true),
large_blob_key: Some(ByteArray::new([0xff; 32])),
third_party_payment: Some(true),
cred_blob: None,
}
}
@@ -845,6 +861,7 @@ mod test {
use_short_id: None,
large_blob_key: None,
third_party_payment: None,
cred_blob: None,
}
}
@@ -932,6 +949,7 @@ mod test {
use_short_id: Some(true),
large_blob_key: Some(random_byte_array()),
third_party_payment: Some(false),
cred_blob: None,
}
}
@@ -1333,6 +1351,7 @@ mod test {
use_short_id: Some(true),
large_blob_key: None,
third_party_payment: None,
cred_blob: None,
},
);
}
+40 -3
View File
@@ -9,7 +9,7 @@ use ctap_types::{
},
heapless::{String, Vec},
heapless_bytes::Bytes,
sizes,
sizes::{self, MAX_CRED_BLOB_LENGTH},
webauthn::{
FilteredPublicKeyCredentialParameters, KnownPublicKeyCredentialParameters,
PublicKeyCredentialUserEntity, ED_DSA, ES256,
@@ -56,6 +56,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
let mut extensions = Vec::new();
extensions.push(Extension::CredProtect).unwrap();
extensions.push(Extension::CredBlob).unwrap();
extensions.push(Extension::HmacSecret).unwrap();
if self.config.supports_large_blobs() {
extensions.push(Extension::LargeBlobKey).unwrap();
@@ -122,6 +123,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
response.firmware_version = self.config.firmware_version;
response.remaining_discoverable_credentials =
remaining_discoverable_credentials.map(|count| count as usize);
response.max_cred_blob_length = Some(MAX_CRED_BLOB_LENGTH);
response.attestation_formats = Some(attestation_formats);
response
}
@@ -253,6 +255,8 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
let mut cred_protect_requested = None;
let mut large_blob_key_requested = false;
let mut third_party_payment_requested = false;
let mut cred_blob_to_store: Option<Bytes<MAX_CRED_BLOB_LENGTH>> = None;
let mut cred_blob_requested = false;
if let Some(extensions) = &parameters.extensions {
hmac_secret_requested = extensions.hmac_secret;
@@ -277,6 +281,20 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
}
third_party_payment_requested = extensions.third_party_payment.unwrap_or_default();
if let Some(blob) = extensions.cred_blob {
cred_blob_requested = true;
// Spec (CTAP 2.1 §11.1): authenticator MAY refuse to store. We
// refuse when (a) the blob exceeds `MAX_CRED_BLOB_LENGTH`, or
// (b) the credential is non-discoverable — encoding `credBlob`
// into a non-RK credential ID would push it past
// `MAX_CREDENTIAL_ID_LENGTH = 255`. In either case, leave
// `cred_blob_to_store = None` and emit `credBlob: false` in the
// MC output extensions.
if rk_requested && blob.len() <= MAX_CRED_BLOB_LENGTH {
cred_blob_to_store = Some(Bytes::try_from(&**blob).expect("len bounded above"));
}
}
}
// debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested);
@@ -348,6 +366,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
cred_protect_requested,
large_blob_key,
third_party_payment_requested.then_some(true),
cred_blob_to_store.clone(),
nonce,
);
@@ -408,7 +427,10 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
if true {
flags |= Flags::ATTESTED_CREDENTIAL_DATA;
}
if hmac_secret_requested.is_some() || cred_protect_requested.is_some() {
if hmac_secret_requested.is_some()
|| cred_protect_requested.is_some()
|| cred_blob_requested
{
flags |= Flags::EXTENSION_DATA;
}
flags
@@ -428,10 +450,19 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
},
extensions: {
if hmac_secret_requested.is_some() || cred_protect_requested.is_some() {
if hmac_secret_requested.is_some()
|| cred_protect_requested.is_some()
|| cred_blob_requested
{
let mut extensions = ctap2::make_credential::ExtensionsOutput::default();
extensions.cred_protect = parameters.extensions.as_ref().unwrap().cred_protect;
extensions.hmac_secret = parameters.extensions.as_ref().unwrap().hmac_secret;
if cred_blob_requested {
// `Some(true)` if the platform-supplied blob fit in
// `MAX_CRED_BLOB_LENGTH` and was stored, `Some(false)`
// otherwise (CTAP 2.1 §11.1).
extensions.cred_blob = Some(cred_blob_to_store.is_some());
}
Some(extensions)
} else {
None
@@ -1546,6 +1577,12 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
output.third_party_payment = Some(credential.third_party_payment().unwrap_or_default());
}
if extensions.cred_blob.unwrap_or(false) {
// Spec: if the extension was requested but no blob is associated
// with the credential, return an empty byte string (not absent).
output.cred_blob = Some(credential.cred_blob().cloned().unwrap_or_else(Bytes::new));
}
Ok(output.is_set().then_some(output))
}