From a07f32e0327f5b61338b0770a7f9ee0a31bfc7c2 Mon Sep 17 00:00:00 2001 From: shimunn Date: Sat, 14 Sep 2019 01:04:56 +0200 Subject: [PATCH] implement hmac extension --- src/cbor.rs | 102 ++++++++++++---------- src/crypto.rs | 63 +++++++------ src/extensions/hmac.rs | 194 +++++++++++++++++++++++++++++++++++++++++ src/extensions/mod.rs | 1 + src/lib.rs | 1 + 5 files changed, 286 insertions(+), 75 deletions(-) create mode 100644 src/extensions/hmac.rs create mode 100644 src/extensions/mod.rs diff --git a/src/cbor.rs b/src/cbor.rs index 434fadb..38d966f 100644 --- a/src/cbor.rs +++ b/src/cbor.rs @@ -4,11 +4,11 @@ // http://apache.org/licenses/LICENSE-2.0> or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. -use cbor_codec::{Config, Encoder, Decoder, GenericDecoder, GenericEncoder}; -use cbor_codec::value::Value; use cbor_codec::value; +use cbor_codec::value::Value; +use cbor_codec::{Config, Decoder, Encoder, GenericDecoder, GenericEncoder}; -use byteorder::{WriteBytesExt, ReadBytesExt, BigEndian, ByteOrder}; +use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt}; use failure::ResultExt; use std::collections::HashMap; @@ -29,25 +29,23 @@ impl<'a> Request<'a> { match self { Request::MakeCredential(req) => req.encode(&mut encoder), Request::GetAssertion(req) => req.encode(&mut encoder), - Request::GetInfo => { - encoder - .writer() - .write_u8(0x04) - .context(FidoErrorKind::CborEncode) - .map_err(From::from) - } + Request::GetInfo => encoder + .writer() + .write_u8(0x04) + .context(FidoErrorKind::CborEncode) + .map_err(From::from), Request::ClientPin(req) => req.encode(&mut encoder), } } pub fn decode(&self, reader: R) -> FidoResult { Ok(match self { - Request::MakeCredential(_) => Response::MakeCredential( - MakeCredentialResponse::decode(reader)?, - ), - Request::GetAssertion(_) => Response::GetAssertion( - GetAssertionResponse::decode(reader)?, - ), + Request::MakeCredential(_) => { + Response::MakeCredential(MakeCredentialResponse::decode(reader)?) + } + Request::GetAssertion(_) => { + Response::GetAssertion(GetAssertionResponse::decode(reader)?) + } Request::GetInfo => Response::GetInfo(GetInfoResponse::decode(reader)?), Request::ClientPin(_) => Response::ClientPin(ClientPinResponse::decode(reader)?), }) @@ -77,9 +75,10 @@ pub struct MakeCredentialRequest<'a> { impl<'a> MakeCredentialRequest<'a> { pub fn encode(&self, mut encoder: &mut Encoder) -> FidoResult<()> { - encoder.writer().write_u8(0x01).context( - FidoErrorKind::CborEncode, - )?; // authenticatorMakeCredential + encoder + .writer() + .write_u8(0x01) + .context(FidoErrorKind::CborEncode)?; // authenticatorMakeCredential let mut length = 4; length += !self.exclude_list.is_empty() as usize; length += !self.extensions.is_empty() as usize; @@ -176,9 +175,10 @@ pub struct GetAssertionRequest<'a> { impl<'a> GetAssertionRequest<'a> { pub fn encode(&self, mut encoder: &mut Encoder) -> FidoResult<()> { - encoder.writer().write_u8(0x02).context( - FidoErrorKind::CborEncode, - )?; // authenticatorGetAssertion + encoder + .writer() + .write_u8(0x02) + .context(FidoErrorKind::CborEncode)?; // authenticatorGetAssertion let mut length = 2; length += !self.allow_list.is_empty() as usize; length += !self.extensions.is_empty() as usize; @@ -315,9 +315,10 @@ pub struct ClientPinRequest<'a> { impl<'a> ClientPinRequest<'a> { pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { - encoder.writer().write_u8(0x06).context( - FidoErrorKind::CborEncode, - )?; // authenticatorClientPIN + encoder + .writer() + .write_u8(0x06) + .context(FidoErrorKind::CborEncode)?; // authenticatorClientPIN let mut length = 2; length += self.key_agreement.is_some() as usize; length += self.pin_auth.is_some() as usize; @@ -383,7 +384,6 @@ impl ClientPinResponse { } } - #[derive(Debug)] pub struct OptionsInfo { pub plat: bool, @@ -439,21 +439,28 @@ impl AuthenticatorData { let flags = bytes[32]; data.up = (flags & 0x01) == 0x01; data.uv = (flags & 0x02) == 0x02; + let is_attested = (flags & 0x40) == 0x40; + let has_extension_data = (flags & 0x80) == 0x80; data.sign_count = BigEndian::read_u32(&bytes[33..37]); if bytes.len() < 38 { return Ok(data); } + let mut cur = Cursor::new(&bytes[37..]); - let attested_credential_data = AttestedCredentialData::from_bytes(&mut cur)?; - data.attested_credential_data = attested_credential_data; - if cur.position() >= (bytes.len() - 37) as u64 { - return Ok(data); + if is_attested { + let attested_credential_data = AttestedCredentialData::from_bytes(&mut cur)?; + data.attested_credential_data = attested_credential_data; + if cur.position() >= (bytes.len() - 37) as u64 { + return Ok(data); + } } - let mut decoder = GenericDecoder::new(Config::default(), cur); - for _ in 0..decoder.borrow_mut().object()? { - let key = decoder.borrow_mut().text()?; - let value = decoder.value()?; - data.extensions.insert(key.to_string(), value); + if has_extension_data { + let mut decoder = GenericDecoder::new(Config::default(), cur); + for _ in 0..decoder.borrow_mut().object()? { + let key = decoder.borrow_mut().text()?; + let value = decoder.value()?; + data.extensions.insert(key.to_string(), value); + } } Ok(data) } @@ -494,15 +501,15 @@ impl P256Key { if cose.key_type != 2 || cose.algorithm != -7 { Err(FidoErrorKind::KeyType)? } - if let (Some(Value::U8(curve)), - Some(Value::Bytes(value::Bytes::Bytes(x))), - Some(Value::Bytes(value::Bytes::Bytes(y)))) = - ( - cose.parameters.get(&-1), - cose.parameters.get(&-2), - cose.parameters.get(&-3), - ) - { + if let ( + Some(Value::U8(curve)), + Some(Value::Bytes(value::Bytes::Bytes(x))), + Some(Value::Bytes(value::Bytes::Bytes(y))), + ) = ( + cose.parameters.get(&-1), + cose.parameters.get(&-2), + cose.parameters.get(&-3), + ) { if *curve != 1 { Err(FidoErrorKind::KeyType)? } @@ -532,9 +539,10 @@ impl P256Key { (-1, Value::U8(1)), (-2, Value::Bytes(value::Bytes::Bytes(self.x.to_vec()))), (-3, Value::Bytes(value::Bytes::Bytes(self.y.to_vec()))), - ].iter() - .cloned() - .collect(), + ] + .iter() + .cloned() + .collect(), } } diff --git a/src/crypto.rs b/src/crypto.rs index 0e7ced1..1fa4902 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -4,15 +4,16 @@ // http://apache.org/licenses/LICENSE-2.0> or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. -use ring::{agreement, rand, digest, hmac, signature}; -use ring::error::Unspecified; -use untrusted::Input; -use rust_crypto::blockmodes::NoPadding; -use rust_crypto::aes; -use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer}; -use failure::ResultExt; use super::cbor::{CoseKey, P256Key}; use super::error::*; +use failure::ResultExt; +use ring::error::Unspecified; +use ring::{agreement, digest, hmac, rand, signature}; +use rust_crypto::aes; +use rust_crypto::blockmodes::NoPadding; +use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer}; +use rust_crypto::symmetriccipher::{Decryptor, Encryptor}; +use untrusted::Input; #[derive(Debug)] pub struct SharedSecret { @@ -26,9 +27,9 @@ impl SharedSecret { let private = agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng) .context(FidoErrorKind::GenerateKey)?; let public = &mut [0u8; agreement::PUBLIC_KEY_MAX_LEN][..private.public_key_len()]; - private.compute_public_key(public).context( - FidoErrorKind::GenerateKey, - )?; + private + .compute_public_key(public) + .context(FidoErrorKind::GenerateKey)?; let peer = P256Key::from_cose(peer_key) .context(FidoErrorKind::ParsePublic)? .bytes(); @@ -39,7 +40,8 @@ impl SharedSecret { peer, Unspecified, |material| Ok(digest::digest(&digest::SHA256, material)), - ).context(FidoErrorKind::GenerateSecret)?; + ) + .context(FidoErrorKind::GenerateSecret)?; let mut res = SharedSecret { public_key: P256Key::from_bytes(&public) .context(FidoErrorKind::ParsePublic)? @@ -50,42 +52,46 @@ impl SharedSecret { Ok(res) } - pub fn encrypt_pin(&self, pin: &str) -> FidoResult<[u8; 16]> { - let mut encryptor = aes::cbc_encryptor( + pub fn encryptor(&self) -> Box { + aes::cbc_encryptor( aes::KeySize::KeySize256, &self.shared_secret, &[0u8; 16], NoPadding, - ); + ) + } + + pub fn encrypt_pin(&self, pin: &str) -> FidoResult<[u8; 16]> { + let mut encryptor = self.encryptor(); let pin_bytes = pin.as_bytes(); let hash = digest::digest(&digest::SHA256, &pin_bytes); let in_bytes = &hash.as_ref()[0..16]; let mut input = RefReadBuffer::new(&in_bytes); let mut out_bytes = [0; 16]; let mut output = RefWriteBuffer::new(&mut out_bytes); - encryptor.encrypt(&mut input, &mut output, true).map_err( - |_| { - FidoErrorKind::EncryptPin - }, - )?; + encryptor + .encrypt(&mut input, &mut output, true) + .map_err(|_| FidoErrorKind::EncryptPin)?; Ok(out_bytes) } - pub fn decrypt_token(&self, data: &mut [u8]) -> FidoResult { - let mut decryptor = aes::cbc_decryptor( + pub fn decryptor(&self) -> Box { + aes::cbc_decryptor( aes::KeySize::KeySize256, &self.shared_secret, &[0u8; 16], NoPadding, - ); + ) + } + + pub fn decrypt_token(&self, data: &mut [u8]) -> FidoResult { + let mut decryptor = self.decryptor(); let mut input = RefReadBuffer::new(data); let mut out_bytes = [0; 16]; let mut output = RefWriteBuffer::new(&mut out_bytes); - decryptor.decrypt(&mut input, &mut output, true).map_err( - |_| { - FidoErrorKind::DecryptPin - }, - )?; + decryptor + .decrypt(&mut input, &mut output, true) + .map_err(|_| FidoErrorKind::DecryptPin)?; Ok(PinToken(hmac::SigningKey::new(&digest::SHA256, &out_bytes))) } } @@ -119,5 +125,6 @@ pub fn verify_signature( public_key, msg, signature, - ).is_ok() + ) + .is_ok() } diff --git a/src/extensions/hmac.rs b/src/extensions/hmac.rs new file mode 100644 index 0000000..58ec619 --- /dev/null +++ b/src/extensions/hmac.rs @@ -0,0 +1,194 @@ +use crate::cbor; +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 rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer}; +use rust_crypto::digest::Digest; +use rust_crypto::hmac::Hmac; +use rust_crypto::mac::Mac; +use rust_crypto::sha2::Sha256; +use std::collections::BTreeMap; +use std::io::Cursor; + +#[derive(Debug, Clone)] +pub struct FidoHmacCredential { + pub id: Vec, + pub rp_id: String, +} + +impl From for FidoHmacCredential { + fn from(cred: FidoCredential) -> Self { + FidoHmacCredential { + id: cred.id, + rp_id: cred.rp_id, + } + } +} + +pub trait HmacExtension { + fn extension_name() -> &'static str { + "hmac-secret" + } + + fn get_dict(&mut self, salt: [u8; 32], salt2: Option<[u8; 32]>) -> FidoResult { + let mut map = BTreeMap::new(); + map.insert( + Key::Text(Text::Text(Self::extension_name().to_owned())), + self.get_data(salt, salt2)?, + ); + Ok(Value::Map(map)) + } + + fn get_data(&mut self, salt: [u8; 32], salt2: Option<[u8; 32]>) -> FidoResult; + + fn make_hmac_credential(&mut self) -> FidoResult; + + fn get_hmac_assertion( + &mut self, + credential: &FidoHmacCredential, + salt: [u8; 32], + salt2: Option<[u8; 32]>, + ) -> FidoResult<([u8; 32], Option<[u8; 32]>)>; + + fn hmac_challange( + &mut self, + credential: &FidoHmacCredential, + input: &[u8], + ) -> FidoResult<[u8; 32]> { + let mut salt = [0u8; 32]; + let mut digest = Sha256::new(); + digest.input(input); + digest.result(&mut salt); + self.get_hmac_assertion(credential, salt, None).map(|secret| secret.0) + } +} + +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(); + let mut encryptor = shared_secret.encryptor(); + let mut salt_enc = [0u8; 64]; + let mut output = RefWriteBuffer::new(&mut salt_enc); + let mut encrypt = || { + encryptor.encrypt(&mut RefReadBuffer::new(&salt), &mut output, salt2.is_none())?; + if let Some(salt2) = salt2 { + encryptor + .encrypt(&mut RefReadBuffer::new(&salt2), &mut output, true) + .map(|_| ()) + } else { + Ok(()) + } + }; + encrypt().map_err(|_| FidoErrorKind::Io)?; + + let key_agreement = || { + let mut cur = Cursor::new(Vec::new()); + let mut encoder = Encoder::new(&mut cur); + shared_secret + .public_key + .encode(&mut encoder).unwrap(); + cur.set_position(0); + let mut dec = GenericDecoder::new(Config::default(), cur); + dec.value() + }; + + let mut map = BTreeMap::new(); + map.insert(Key::Int(Int::from_i64(0x01)), key_agreement().map_err(|_| FidoErrorKind::Io)?); + map.insert( + Key::Int(Int::from_i64(0x02)), + Value::Bytes(Bytes::Bytes( + salt_enc[0..((salt2.is_some() as usize + 1) * 32)].to_vec(), + )), + ); + + let mut salt_hmac = Hmac::new(Sha256::new(), &shared_secret.shared_secret); + salt_hmac.input(&salt_enc[0..((salt2.is_some() as usize + 1) * 32)]); + + let mut authed_salt_enc = [0u8; 32]; + authed_salt_enc.copy_from_slice(salt_hmac.result().code()); + + map.insert( + Key::Int(Int::from_i64(0x03)), + Value::Bytes(Bytes::Bytes(authed_salt_enc[0..16].to_vec())), + ); + + Ok(Value::Map(map)) + } + + fn make_hmac_credential(&mut self) -> FidoResult { + self.make_credential("hmac", &[0u8], "commandline", &[0u8; 32]) + .map(|cred| cred.into()) + } + + fn get_hmac_assertion( + &mut self, + credential: &FidoHmacCredential, + salt: [u8; 32], + salt2: Option<[u8; 32]>, + ) -> FidoResult<([u8; 32], Option<[u8; 32]>)> { + let client_data_hash = [0u8; 32]; + while self.shared_secret.is_none() { + self.init_shared_secret()?; + } + if self.needs_pin && self.pin_token.is_none() { + Err(FidoErrorKind::PinRequired)? + } + + if client_data_hash.len() != 32 { + Err(FidoErrorKind::CborEncode)? + } + let pin_auth = self + .pin_token + .as_ref() + .map(|token| token.auth(&client_data_hash)); + let ext_data: Value = self.get_data(salt, salt2)?; + let allow_list = [cbor::PublicKeyCredentialDescriptor { + cred_type: String::from("public-key"), + id: credential.id.clone(), + }]; + let request = cbor::GetAssertionRequest { + rp_id: &credential.rp_id, + client_data_hash: &client_data_hash, + allow_list: &allow_list, + extensions: &[(::extension_name(), &ext_data)], + options: Some(cbor::AuthenticatorOptions { + rk: false, + uv: true, + }), + pin_auth, + pin_protocol: pin_auth.and(Some(0x01)), + }; + let response = match self.cbor(cbor::Request::GetAssertion(request))? { + cbor::Response::GetAssertion(resp) => resp, + _ => Err(FidoErrorKind::CborDecode)?, + }; + let shared_secret = self.shared_secret.as_ref().unwrap(); + let mut decryptor = shared_secret.decryptor(); + let mut hmac_secret_combined = [0u8; 64]; + let mut output = RefWriteBuffer::new(&mut hmac_secret_combined); + let hmac_secret_enc = match response + .auth_data + .extensions + .get(::extension_name()) + .ok_or(FidoErrorKind::CborDecode)? + { + Value::Bytes(hmac_ciphered) => Ok(match hmac_ciphered { + Bytes::Bytes(hmac_ciphered) => hmac_ciphered.to_vec(), + Bytes::Chunks(hmac_ciphered) => hmac_ciphered.iter().fold(Vec::new(), |s, i| { + let mut s = s; + s.extend_from_slice(&i); + s + }), + }), + _ => Err(FidoErrorKind::CborDecode), + }?; + let mut hmac_secret = ([0u8; 32], [0u8; 32]); + decryptor.decrypt( + &mut RefReadBuffer::new(&hmac_secret_enc), + &mut RefWriteBuffer::new(unsafe { std::mem::transmute::<_ ,&mut [u8; 64]>(&mut hmac_secret) }), + true, + ); + Ok((hmac_secret.0, salt2.map(|_| hmac_secret.1))) + } +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs new file mode 100644 index 0000000..c0f9333 --- /dev/null +++ b/src/extensions/mod.rs @@ -0,0 +1 @@ +pub mod hmac; diff --git a/src/lib.rs b/src/lib.rs index 7be3529..7d0ebe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,7 @@ mod hid_linux; mod error; mod crypto; mod cbor; +pub mod extensions; use std::cmp; use std::u8;