diff --git a/Cargo.toml b/Cargo.toml index ce7a1c2..1618660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ctap_hmac" description = "A Rust implementation of the FIDO2 CTAP protocol, including the HMAC extension" -version = "0.4.0" +version = "0.4.2" license = "Apache-2.0/MIT" homepage = "https://github.com/shimunn/ctap" repository = "https://github.com/shimunn/ctap" diff --git a/examples/hmac.rs b/examples/hmac.rs index ac8834e..4f55e60 100644 --- a/examples/hmac.rs +++ b/examples/hmac.rs @@ -2,7 +2,7 @@ extern crate ctap_hmac as ctap; use crypto::digest::Digest; use crypto::sha2::Sha256; -use ctap::extensions::hmac::HmacExtension; +use ctap::extensions::{self, FidoExtensionResponseParserExt}; use ctap::{FidoAssertionRequestBuilder, FidoCredential, FidoCredentialRequestBuilder}; use hex; use std::env::args; @@ -16,14 +16,13 @@ fn main() -> ctap::FidoResult<()> { let mut devices = ctap::get_devices()?; let device_info = &mut devices.next().expect("No authenticator found"); let mut device = ctap::FidoDevice::new(device_info)?; - - let credential = match args().skip(1).next().map(|h| FidoCredential { + let credential = match args().nth(1).map(|h| FidoCredential { id: hex::decode(&h).expect("Invalid credential"), public_key: None, }) { Some(cred) => cred, _ => { - let req = FidoCredentialRequestBuilder::default() + let mut req = FidoCredentialRequestBuilder::default() .rp_id(RP_ID) .rp_name("ctap_hmac crate") .user_name("example") @@ -31,9 +30,16 @@ fn main() -> ctap::FidoResult<()> { .build() .unwrap(); + assert!( + &device.supports_extension::(), + "Your device does not support the hmac extension" + ); + let hmac = extensions::HmacSecret::new(); + req.with_extension(&hmac)?; + dbg!(&req); println!("Authorize using your device"); - let cred = device - .make_hmac_credential(&req) + let cred = req + .make_credential(&mut device) .expect("Failed to request credential"); println!("Credential: {}\nNote: You can pass this credential as first argument in order to reproduce results", hex::encode(&cred.id)); cred @@ -52,12 +58,15 @@ fn main() -> ctap::FidoResult<()> { digest.input(&message.as_bytes()); digest.result(&mut salt); let credential = &&credential; - let request = FidoAssertionRequestBuilder::default() + let hmac = extensions::HmacSecret::new().for_device(&mut device, &salt, None)?; + let mut request = FidoAssertionRequestBuilder::default() .rp_id(RP_ID) .credential(credential) .build() .unwrap(); - let (_cred, (hash1, _hash2)) = device.get_hmac_assertion(&request, &salt, None)?; + request.with_extension(&hmac)?; + let (_cred, auth_data) = device.get_assertion(&request)?; + let (hash1, _hash2) = auth_data.parse_extension_data(&hmac)?; println!("Hash: {}", hex::encode(&hash1)); Ok(()) } diff --git a/src/cbor.rs b/src/cbor.rs index 7cdb27a..f74f4d6 100644 --- a/src/cbor.rs +++ b/src/cbor.rs @@ -592,7 +592,7 @@ impl P256Key { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct CoseKey { key_type: u16, algorithm: i32, diff --git a/src/crypto.rs b/src/crypto.rs index 1fa4902..31a0d66 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -15,7 +15,7 @@ use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer}; use rust_crypto::symmetriccipher::{Decryptor, Encryptor}; use untrusted::Input; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SharedSecret { pub public_key: CoseKey, pub shared_secret: [u8; 32], diff --git a/src/extensions/cred_protect.rs b/src/extensions/cred_protect.rs new file mode 100644 index 0000000..de39c0c --- /dev/null +++ b/src/extensions/cred_protect.rs @@ -0,0 +1,57 @@ +use crate::extensions::FidoExtension; +use crate::FidoResult; +use crate::{FidoAssertionRequest, FidoCredentialRequest}; +use cbor_codec::value::Value; +use num_traits::ToPrimitive; + +#[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, PartialEq)] +pub enum CredProtectLevel { + UvOptional = 0x01, + UvOptionalWithCredentialIDList = 0x02, + UvRequired = 0x03, +} + +impl CredProtectLevel { + fn extension_input(self) -> Value { + Value::U8(self.to_u8().unwrap()) + } +} + +pub struct CredProtect(Value); + +impl CredProtect { + pub fn level(level: CredProtectLevel) -> Self { + Self(level.extension_input()) + } + + fn extension_name() -> &'static str { + "credProtect" + } + + fn extension_input(&self) -> &Value { + &self.0 + } +} + +impl FidoExtension for CredProtect { + fn extension_name() -> &'static str { + CredProtect::extension_name() + } + + fn patch_assertion_request<'a, 'b>( + &'b self, + _request: &mut FidoAssertionRequest<'a, 'b>, + ) -> FidoResult<()> { + Ok(()) + } + + fn patch_credential_request<'a>( + &'a self, + request: &mut FidoCredentialRequest<'a>, + ) -> FidoResult<()> { + request + .extension_data + .insert(CredProtect::extension_name(), self.extension_input()); + Ok(()) + } +} diff --git a/src/extensions/hmac.rs b/src/extensions/hmac.rs index cd8d900..27da0f4 100644 --- a/src/extensions/hmac.rs +++ b/src/extensions/hmac.rs @@ -1,8 +1,14 @@ +use crate::cbor::AuthenticatorData; +use crate::crypto::SharedSecret; +use crate::extensions::{ + FidoExtension, FidoExtensionResponseParser, FidoExtensionResponseParserExt, +}; use crate::{FidoAssertionRequest, FidoAssertionRequestBuilder, FidoCredentialRequest}; use crate::{FidoCredential, FidoDevice, FidoErrorKind, FidoResult}; use cbor_codec::value::{Bytes, Int, Key, Text, Value}; use cbor_codec::Encoder; use cbor_codec::{Config, GenericDecoder}; +use failure::ResultExt; use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer}; use rust_crypto::digest::Digest; use rust_crypto::hmac::Hmac; @@ -11,6 +17,7 @@ use rust_crypto::sha2::Sha256; use std::collections::BTreeMap; use std::io::Cursor; +//#[deprecated] pub trait HmacExtension { fn extension_name() -> &'static str { "hmac-secret" @@ -85,7 +92,67 @@ pub trait HmacExtension { impl HmacExtension for FidoDevice { fn get_data(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult { - let shared_secret = self.shared_secret.as_ref().unwrap(); + Ok(HmacSecret::extension_input(self, salt, salt2)?) + } + + fn make_hmac_credential( + &mut self, + request: &FidoCredentialRequest, + ) -> FidoResult { + let mut request = request.clone(); + request.rk = true; + request.extension_data.insert( + ::extension_name(), + ::extension_input(), + ); + self.make_credential(&request) + } + + fn get_hmac_assertion<'a: 'b, 'b>( + &mut self, + request: &FidoAssertionRequest<'a, 'b>, + salt: &[u8; 32], + salt2: Option<&[u8; 32]>, + ) -> FidoResult<(&'a FidoCredential, ([u8; 32], Option<[u8; 32]>))> { + while self.shared_secret.is_none() { + self.init_shared_secret()?; + } + let mut request = request.clone(); + let ext = HmacSecret::new().for_device(self, salt, salt2)?; + request.with_extension(&ext)?; + + let (cred, auth_data) = self.get_assertion(&request)?; + + let cred = request + .credentials + .iter() + .find(|c| c.id == cred.id) + .unwrap(); + Ok((cred, auth_data.parse_extension_data(&ext)?)) + } +} + +#[derive(Debug, Clone)] +pub enum HmacSecret { + Assertion { + extension_data: Value, + shared_secret: SharedSecret, + salt2: bool, + }, + Credential, +} + +impl HmacSecret { + pub fn new() -> Self { + Self::Credential + } + + fn extension_input( + device: &mut FidoDevice, + salt: &[u8; 32], + salt2: Option<&[u8; 32]>, + ) -> FidoResult { + let shared_secret = device.shared_secret.as_ref().unwrap(); let mut encryptor = shared_secret.encryptor(); let mut salt_enc = [0u8; 64]; let mut output = RefWriteBuffer::new(&mut salt_enc); @@ -113,7 +180,7 @@ impl HmacExtension for FidoDevice { let mut map = BTreeMap::new(); map.insert( Key::Int(Int::from_i64(0x01)), - key_agreement().map_err(|_| FidoErrorKind::Io)?, + key_agreement().context(FidoErrorKind::Io)?, ); map.insert( Key::Int(Int::from_i64(0x02)), @@ -136,42 +203,67 @@ impl HmacExtension for FidoDevice { Ok(Value::Map(map)) } - fn make_hmac_credential( + pub fn for_device( &mut self, - request: &FidoCredentialRequest, - ) -> FidoResult { - let mut request = request.clone(); - request.rk = true; - request.extension_data.insert( - ::extension_name(), - ::extension_input(), - ); - self.make_credential(&request) - } - - fn get_hmac_assertion<'a: 'b, 'b>( - &mut self, - request: &FidoAssertionRequest<'a, 'b>, + device: &mut FidoDevice, salt: &[u8; 32], salt2: Option<&[u8; 32]>, - ) -> FidoResult<(&'a FidoCredential, ([u8; 32], Option<[u8; 32]>))> { - while self.shared_secret.is_none() { - self.init_shared_secret()?; - } - let ext_data: Value = self.get_data(salt, salt2)?; - let mut request = request.clone(); + ) -> FidoResult { + Ok(Self::Assertion { + extension_data: Self::extension_input(device, salt, salt2)?, + shared_secret: device.shared_secret.as_ref().unwrap().clone(), + salt2: salt2.is_some(), + }) + } +} + +impl FidoExtension for HmacSecret { + fn extension_name() -> &'static str { + "hmac-secret" + } + + fn patch_assertion_request<'a, 'b>( + &'b self, + request: &mut FidoAssertionRequest<'a, 'b>, + ) -> FidoResult<()> { + match self { + Self::Assertion { extension_data, .. } => request + .extension_data + .insert(Self::extension_name(), extension_data), + _ => return Err(FidoErrorKind::DeviceUnsupported.into()), + }; + Ok(()) + } + + fn patch_credential_request<'a>( + &'a self, + request: &mut FidoCredentialRequest<'a>, + ) -> FidoResult<()> { request .extension_data - .insert(::extension_name(), &ext_data); + .insert(Self::extension_name(), &Value::Bool(true)); + Ok(()) + } +} - let (cred, auth_data) = self.get_assertion(&request)?; - let shared_secret = self.shared_secret.as_ref().unwrap(); +impl FidoExtensionResponseParser for HmacSecret { + type Output = ([u8; 32], Option<[u8; 32]>); + + fn parse_response(&self, response: &AuthenticatorData) -> FidoResult { + let (shared_secret, salt2) = match self { + Self::Assertion { + shared_secret, + salt2, + .. + } => (shared_secret, salt2), + _ => return Err(FidoErrorKind::DeviceUnsupported.into()), + }; let mut decryptor = shared_secret.decryptor(); let mut hmac_secret_combined = [0u8; 64]; let _output = RefWriteBuffer::new(&mut hmac_secret_combined); - let hmac_secret_enc = match auth_data + let hmac_secret_enc = match response .extensions - .get(::extension_name()) + .get(Self::extension_name()) .ok_or(FidoErrorKind::CborDecode)? { Value::Bytes(hmac_ciphered) => Ok(match hmac_ciphered { @@ -196,11 +288,6 @@ impl HmacExtension for FidoDevice { let mut hmac_secret_1 = [0u8; 32]; hmac_secret_0.copy_from_slice(&hmac_secret[0..32]); hmac_secret_1.copy_from_slice(&hmac_secret[32..]); - let cred = request - .credentials - .into_iter() - .find(|c| c.id == cred.id) - .unwrap(); - Ok((cred, (hmac_secret_0, salt2.and(Some(hmac_secret_1))))) + Ok((hmac_secret_0, Some(hmac_secret_1).filter(|_| *salt2))) } } diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 07d0006..7f6fffc 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -1,2 +1,36 @@ pub mod hmac; pub use hmac::*; +mod cred_protect; +use crate::cbor::AuthenticatorData; +use crate::{FidoAssertionRequest, FidoCredentialRequest, FidoResult}; +pub use cred_protect::*; + +pub trait FidoExtension { + fn extension_name() -> &'static str; + fn patch_assertion_request<'a, 'b>( + &'b self, + request: &mut FidoAssertionRequest<'a, 'b>, + ) -> FidoResult<()>; + fn patch_credential_request<'a>( + &'a self, + request: &mut FidoCredentialRequest<'a>, + ) -> FidoResult<()>; +} + +pub trait FidoExtensionResponseParser { + type Output; + fn parse_response(&self, response: &AuthenticatorData) -> FidoResult; +} + +pub trait FidoExtensionResponseParserExt { + fn parse_extension_data(&self, extension: &Ext) -> FidoResult; +} + +impl FidoExtensionResponseParserExt for AuthenticatorData { + fn parse_extension_data( + &self, + extension: &Ext, + ) -> FidoResult<::Output> { + extension.parse_response(self) + } +} diff --git a/src/lib.rs b/src/lib.rs index 61739f6..9cba98f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,8 +74,9 @@ pub use self::error::*; use self::hid_linux as hid; use self::packet::CtapCommand; pub use self::util::*; -use crate::cbor::{AuthenticatorData, GetAssertionRequest}; +use crate::cbor::{AuthenticatorData, GetAssertionRequest, GetInfoResponse}; use crate::packet::CtapStatus; +use crate::extensions::FidoExtension; use failure::{Fail, ResultExt}; use num_traits::FromPrimitive; use rand::prelude::*; @@ -108,6 +109,7 @@ pub struct FidoDevice { shared_secret: Option, pin_token: Option, aaguid: [u8; 16], + info: GetInfoResponse, } pub struct FidoCancelHandle { @@ -205,6 +207,10 @@ impl<'a> FidoCredentialRequest<'a> { pub fn make_credential(&self, device: &mut FidoDevice) -> FidoResult { device.make_credential(&self) } + + pub fn with_extension(&mut self, extension: &'a Ext) -> FidoResult<()> { + extension.patch_credential_request(self) + } } /// Request an assertion from the authenticator for a given credential. @@ -240,6 +246,10 @@ impl<'a, 'b> FidoAssertionRequest<'a, 'b> { pub fn get_assertion(&self, device: &mut FidoDevice) -> FidoResult<&'a FidoCredential> { device.get_assertion(self).map(|res| res.0) } + + pub fn with_extension(&mut self, extension: &'b Ext) -> FidoResult<()> { + extension.patch_assertion_request(self) + } } impl<'a, 'b> FidoAssertionRequestBuilder<'a, 'b> { @@ -267,6 +277,7 @@ impl FidoDevice { shared_secret: None, pin_token: None, aaguid: [0; 16], + info: GetInfoResponse::default(), }; dev.init()?; Ok(dev) @@ -301,6 +312,7 @@ impl FidoDevice { } self.needs_pin = response.options.client_pin == Some(true); self.aaguid = response.aaguid; + self.info = response; Ok(()) } @@ -370,6 +382,10 @@ impl FidoDevice { .context(FidoErrorKind::Io)?) } + pub fn supports_extension(&self) -> bool { + self.info.extensions.iter().any(|ext| ext == Ext::extension_name()) + } + pub fn make_credential( &mut self, request: &FidoCredentialRequest<'_>,