mirror of
https://github.com/trussed-dev/fido-authenticator.git
synced 2026-06-20 04:16:16 -07:00
ctap2.1: implement credBlob extension (RK only, max 32 bytes)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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) = ¶meters.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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user