From c2adc07c90e43e7a72ec1adc340056e90282b9b6 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Thu, 1 Feb 2024 08:10:01 +0100 Subject: [PATCH] Add credBlob-extension --- examples/ctap2_discoverable_creds.rs | 44 ++++++++++++++++++-------- src/ctap2/attestation.rs | 8 ++++- src/ctap2/commands/get_assertion.rs | 22 +++++++++++-- src/ctap2/commands/make_credentials.rs | 20 +++++++++--- src/ctap2/server.rs | 35 ++++++++++++++++++++ 5 files changed, 108 insertions(+), 21 deletions(-) diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index 4667dcf5..09089fa8 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -6,18 +6,19 @@ use authenticator::{ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, crypto::COSEAlgorithm, ctap2::server::{ - AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, - ResidentKeyRequirement, Transport, UserVerificationRequirement, + AuthenticationExtensionsClientInputs, AuthenticatorExtensionsCredBlob, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, + UserVerificationRequirement, }, statecallback::StateCallback, Pin, StatusPinUv, StatusUpdate, }; -use getopts::Options; +use getopts::{Matches, Options}; use sha2::{Digest, Sha256}; +use std::io::Write; use std::sync::mpsc::{channel, RecvError}; use std::{env, io, thread}; -use std::io::Write; fn print_usage(program: &str, opts: Options) { println!("------------------------------------------------------------------------"); @@ -60,7 +61,12 @@ fn ask_user_choice(choices: &[PublicKeyCredentialUserEntity]) -> Option { } } -fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) { +fn register_user( + manager: &mut AuthenticatorService, + username: &str, + timeout_ms: u64, + matches: &Matches, +) { println!(); println!("*********************************************************************"); println!("Asking a security key to register now with user: {username}"); @@ -168,6 +174,9 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: resident_key_req: ResidentKeyRequirement::Required, extensions: AuthenticationExtensionsClientInputs { cred_props: Some(true), + cred_blob: matches.opt_present("cred_blob").then(|| { + AuthenticatorExtensionsCredBlob::AsBytes("My short credBlob".as_bytes().to_vec()) + }), ..Default::default() }, pin: None, @@ -214,10 +223,8 @@ fn main() { "timeout in seconds", "SEC", ); - opts.optflag( - "s", - "skip_reg", - "Skip registration"); + opts.optflag("s", "skip_reg", "Skip registration"); + opts.optflag("b", "cred_blob", "With credBlob"); opts.optflag("h", "help", "print this help menu"); let matches = match opts.parse(&args[1..]) { @@ -247,7 +254,7 @@ fn main() { if !matches.opt_present("skip_reg") { for username in &["A. User", "A. Nother", "Dr. Who"] { - register_user(&mut manager, username, timeout_ms) + register_user(&mut manager, username, timeout_ms, &matches) } } @@ -337,7 +344,12 @@ fn main() { allow_list, user_verification_req: UserVerificationRequirement::Required, user_presence_req: true, - extensions: Default::default(), + extensions: AuthenticationExtensionsClientInputs { + cred_blob: matches + .opt_present("cred_blob") + .then_some(AuthenticatorExtensionsCredBlob::AsBool(true)), + ..Default::default() + }, pin: None, use_ctap1_fallback: false, }; @@ -364,7 +376,13 @@ fn main() { println!("Found credentials:"); println!( "{:?}", - assertion_object.assertion.user.clone().unwrap().name.unwrap() // Unwrapping here, as these shouldn't fail + assertion_object + .assertion + .user + .clone() + .unwrap() + .name + .unwrap() // Unwrapping here, as these shouldn't fail ); println!("-----------------------------------------------------------------"); println!("Done."); diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index ad5da1b0..7626420d 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1,3 +1,4 @@ +use super::server::AuthenticatorExtensionsCredBlob; use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte}; use crate::crypto::{COSEAlgorithm, CryptoError, SharedSecret}; use crate::ctap2::server::{CredentialProtectionPolicy, HMACGetSecretOutput, RpIdHash}; @@ -119,11 +120,16 @@ pub struct Extension { pub hmac_secret: Option, #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, + #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, } impl Extension { pub fn has_some(&self) -> bool { - self.min_pin_length.is_some() || self.hmac_secret.is_some() || self.cred_protect.is_some() + self.min_pin_length.is_some() + || self.hmac_secret.is_some() + || self.cred_protect.is_some() + || self.cred_blob.is_some() } } diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 890febae..4ae062ec 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -15,8 +15,8 @@ use crate::ctap2::commands::make_credentials::UserVerification; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, AuthenticationExtensionsPRFInputs, AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, - PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, - UserVerificationRequirement, + AuthenticatorExtensionsCredBlob, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_be_u32, read_byte}; use crate::errors::AuthenticatorError; @@ -253,6 +253,8 @@ pub struct GetAssertionExtensions { skip_serializing_if = "HmacGetSecretOrPrf::skip_serializing" )] pub hmac_secret: Option, + #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, } impl From for GetAssertionExtensions { @@ -271,13 +273,17 @@ impl From for GetAssertionExtensions { .or_else( || prf.map(HmacGetSecretOrPrf::PrfUninitialized), // Cannot calculate hmac-secret inputs here because we don't yet know which eval or evalByCredential entry to use ), + cred_blob: match input.cred_blob { + Some(AuthenticatorExtensionsCredBlob::AsBool(x)) => Some(x), + _ => None, + }, } } } impl GetAssertionExtensions { fn has_content(&self) -> bool { - self.hmac_secret.is_some() + self.hmac_secret.is_some() || self.cred_blob.is_some() } } @@ -432,6 +438,11 @@ impl GetAssertion { } None => {} } + + // 3. credBlob + // The extension returns a flag in the authenticator data which we need to mirror as a + // client output. + result.extensions.cred_blob = result.assertion.auth_data.extensions.cred_blob.clone(); } } @@ -1048,6 +1059,7 @@ pub mod test { None, ), )), + cred_blob: None, }, options: GetAssertionOptions { user_presence: Some(true), @@ -1105,6 +1117,7 @@ pub mod test { Some(2), ), )), + cred_blob: None, }, options: GetAssertionOptions { user_presence: None, @@ -1150,6 +1163,7 @@ pub mod test { eval_by_credential: None, }, )), + cred_blob: None, }, options: GetAssertionOptions { user_presence: None, @@ -1171,6 +1185,7 @@ pub mod test { extensions: GetAssertionExtensions { app_id: None, hmac_secret: Some(HmacGetSecretOrPrf::PrfUnmatched), + cred_blob: None, }, options: GetAssertionOptions { user_presence: None, @@ -2828,6 +2843,7 @@ pub mod test { cred_protect: None, hmac_secret: hmac_secret_response, min_pin_length: None, + cred_blob: None, }, }, signature: vec![], diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index c64d4076..f393a622 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -15,9 +15,9 @@ use crate::ctap2::attestation::{ use crate::ctap2::client_data::ClientDataHash; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, - AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, CredentialProtectionPolicy, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, - RelyingParty, RpIdHash, UserVerificationRequirement, + AuthenticationExtensionsPRFOutputs, AuthenticatorAttachment, AuthenticatorExtensionsCredBlob, + CredentialProtectionPolicy, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_byte, serde_parse_err}; use crate::errors::AuthenticatorError; @@ -242,6 +242,8 @@ pub struct MakeCredentialsExtensions { pub hmac_secret: Option, #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] pub min_pin_length: Option, + #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, } #[derive(Debug, Clone)] @@ -264,7 +266,10 @@ impl Serialize for HmacCreateSecretOrPrf { impl MakeCredentialsExtensions { fn has_content(&self) -> bool { - self.cred_protect.is_some() || self.hmac_secret.is_some() || self.min_pin_length.is_some() + self.cred_protect.is_some() + || self.hmac_secret.is_some() + || self.min_pin_length.is_some() + || self.cred_blob.is_some() } } @@ -281,6 +286,7 @@ impl From for MakeCredentialsExtensions { } }, min_pin_length: input.min_pin_length, + cred_blob: input.cred_blob, } } } @@ -409,6 +415,11 @@ impl MakeCredentials { } None | Some(HmacCreateSecretOrPrf::HmacCreateSecret(false)) => {} } + + // 3. credBlob + // The extension returns a flag in the authenticator data which we need to mirror as a + // client output. + result.extensions.cred_blob = result.att_obj.auth_data.extensions.cred_blob.clone(); } } @@ -755,6 +766,7 @@ pub mod test { ), hmac_secret: Some(HmacCreateSecretOrPrf::HmacCreateSecret(true)), min_pin_length: Some(true), + cred_blob: None, }, options: MakeCredentialsOptions { resident_key: Some(true), diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 29ed7c1b..b9ce8cbc 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -354,6 +354,35 @@ impl<'de> Deserialize<'de> for CredentialProtectionPolicy { } } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AuthenticatorExtensionsCredBlob { + /// Used in GetAssertion-requests to request the stored blob, + /// and in MakeCredential-responses to signify if the + /// storing worked. + AsBool(bool), + /// Used in MakeCredential-requests to store a new credBlob, + /// and in GetAssertion-responses when retrieving the + /// stored blob. + #[serde(serialize_with = "vec_to_bytebuf", deserialize_with = "bytebuf_to_vec")] + AsBytes(Vec), +} + +fn vec_to_bytebuf(data: &[u8], s: S) -> Result +where + S: Serializer, +{ + ByteBuf::from(data).serialize(s) +} + +fn bytebuf_to_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let bytes = ::deserialize(deserializer)?; + Ok(bytes.to_vec()) +} + #[derive(Clone, Debug, Default)] pub struct AuthenticationExtensionsClientInputs { pub app_id: Option, @@ -364,6 +393,9 @@ pub struct AuthenticationExtensionsClientInputs { pub hmac_get_secret: Option, pub min_pin_length: Option, pub prf: Option, + /// MakeCredential-requests use AsBytes + /// GetAssertion-requests use AsBool + pub cred_blob: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -495,6 +527,9 @@ pub struct AuthenticationExtensionsClientOutputs { pub hmac_create_secret: Option, pub hmac_get_secret: Option, pub prf: Option, + /// MakeCredential-responses use AsBool + /// GetAssertion-responses use AsBytes + pub cred_blob: Option, } #[derive(Clone, Debug, PartialEq, Eq)]