diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index d19ccc6f..5976004d 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}"); @@ -170,6 +176,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, @@ -216,10 +225,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..]) { @@ -249,7 +256,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) } } @@ -341,7 +348,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, }; @@ -368,7 +380,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 af33b159..01e5a300 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; use crate::ctap2::server::{CredentialProtectionPolicy, RpIdHash}; @@ -74,11 +75,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 54d7d491..0bad7387 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -14,8 +14,8 @@ use crate::ctap2::commands::get_next_assertion::GetNextAssertion; use crate::ctap2::commands::make_credentials::UserVerification; use crate::ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, - AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, - RelyingParty, RpIdHash, UserVerificationRequirement, + AuthenticatorAttachment, AuthenticatorExtensionsCredBlob, PublicKeyCredentialDescriptor, + PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_be_u32, read_byte}; use crate::errors::AuthenticatorError; @@ -140,12 +140,18 @@ pub struct GetAssertionExtensions { pub app_id: Option, #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, + #[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, } impl From for GetAssertionExtensions { fn from(input: AuthenticationExtensionsClientInputs) -> Self { Self { app_id: input.app_id, + cred_blob: match input.cred_blob { + Some(AuthenticatorExtensionsCredBlob::AsBool(x)) => Some(x), + _ => None, + }, ..Default::default() } } @@ -153,7 +159,7 @@ impl From for GetAssertionExtensions { impl GetAssertionExtensions { fn has_content(&self) -> bool { - self.hmac_secret.is_some() + self.hmac_secret.is_some() || self.cred_blob.is_some() } } @@ -205,6 +211,11 @@ impl GetAssertion { result.extensions.app_id = Some(result.assertion.auth_data.rp_id_hash == RelyingParty::from(app_id).hash()); } + + // 2. 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(); } } diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 1de10484..ac0e4667 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, - AuthenticatorAttachment, CredentialProtectionPolicy, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, - UserVerificationRequirement, + AuthenticatorAttachment, AuthenticatorExtensionsCredBlob, CredentialProtectionPolicy, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, UserVerificationRequirement, }; use crate::ctap2::utils::{read_byte, serde_parse_err}; use crate::errors::AuthenticatorError; @@ -243,11 +243,16 @@ 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, } 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() } } @@ -258,6 +263,7 @@ impl From for MakeCredentialsExtensions { cred_protect: input.credential_protection_policy, hmac_secret: input.hmac_create_secret, min_pin_length: input.min_pin_length, + cred_blob: input.cred_blob, } } } @@ -350,6 +356,11 @@ impl MakeCredentials { result.extensions.hmac_create_secret = Some(flag); } } + + // 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(); } } diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index fdf26374..507d9a43 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -357,6 +357,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, @@ -365,6 +394,9 @@ pub struct AuthenticationExtensionsClientInputs { pub enforce_credential_protection_policy: Option, pub hmac_create_secret: Option, pub min_pin_length: Option, + /// MakeCredential-requests use AsBytes + /// GetAssertion-requests use AsBool + pub cred_blob: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -377,6 +409,9 @@ pub struct AuthenticationExtensionsClientOutputs { pub app_id: Option, pub cred_props: Option, pub hmac_create_secret: Option, + /// MakeCredential-responses use AsBool + /// GetAssertion-responses use AsBytes + pub cred_blob: Option, } #[derive(Clone, Debug, PartialEq, Eq)]