diff --git a/Cargo.toml b/Cargo.toml index 8566637..1bfca3c 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.1.1" +version = "0.2.1" license = "Apache-2.0/MIT" homepage = "https://github.com/ArdaXi/ctap/pull/2" repository = "https://github.com/shimunn/ctap" @@ -19,3 +19,5 @@ cbor-codec = "0.7" ring = "0.13" untrusted = "0.6" rust-crypto = "0.2" +hex = "0.4.0" +csv-core = "0.1.6" diff --git a/examples/hmac.rs b/examples/hmac.rs new file mode 100644 index 0000000..c649510 --- /dev/null +++ b/examples/hmac.rs @@ -0,0 +1,62 @@ +extern crate ctap_hmac as ctap; + +use crypto::digest::Digest; +use crypto::sha2::Sha256; +use ctap::extensions::hmac::{FidoHmacCredential, HmacExtension}; +use ctap_hmac::{AuthenticatorOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; +use hex; +use std::env::args; +use std::io::prelude::*; +use std::io::stdin; +use std::io::stdout; + +fn main() -> ctap::FidoResult<()> { + let mut devices = ctap::get_devices()?; + let device_info = &mut devices.next().expect("No authenicator found"); + let mut device = ctap::FidoDevice::new(device_info)?; + let options = || Some(AuthenticatorOptions { uv: true, rk: true }); + let mut credential = match args().skip(1).next().map(|h| FidoHmacCredential { + id: hex::decode(&h).expect("Invalid credential"), + rp_id: "ctap_demo".into(), + }) { + Some(cred) => cred, + _ => { + let rp = PublicKeyCredentialRpEntity { + id: "ctap_demo", + name: Some("ctap_hmac crate"), + icon: None, + }; + let user = PublicKeyCredentialUserEntity { + id: &[0u8], + name: "commandline", + icon: None, + display_name: None, + }; + + println!("Authorize using your device"); + let credential: FidoHmacCredential = device + .make_hmac_credential_full(rp, user, &[0u8; 32], &[], options()) + .map(|cred| cred.into())?; + println!("Credential: {}\nNote: You can pass this credential as first argument in order to reproduce results", hex::encode(&credential.id)); + credential + } + }; + let credential = credential; + print!("Type in your message: "); + stdout().flush(); + let mut message = String::new(); + stdin() + .read_line(&mut message) + .expect("Couldn't get your message\nNote: this demo does not accept binary data"); + println!("Authorize using your device"); + + let mut salt = [0u8; 32]; + let mut digest = Sha256::new(); + digest.input(&message.as_bytes()); + digest.result(&mut salt); + let hash = device + .get_hmac_assertion(&credential, &salt, None, options())? + .0; + println!("Hash: {}", hex::encode(&hash)); + Ok(()) +} diff --git a/src/cbor.rs b/src/cbor.rs index 38d966f..368a5ae 100644 --- a/src/cbor.rs +++ b/src/cbor.rs @@ -82,7 +82,11 @@ impl<'a> MakeCredentialRequest<'a> { let mut length = 4; length += !self.exclude_list.is_empty() as usize; length += !self.extensions.is_empty() as usize; - length += self.options.is_some() as usize; + length += self + .options + .as_ref() + .map(|opt| opt.encoded()) + .unwrap_or(false) as usize; length += self.pin_auth.is_some() as usize; length += self.pin_protocol.is_some() as usize; encoder.object(length)?; @@ -145,7 +149,7 @@ impl MakeCredentialResponse { pub fn decode(mut reader: R) -> FidoResult { let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; if status != 0 { - Err(FidoErrorKind::CborError(status))? + Err(FidoErrorKind::CborError(CborErrorCode::from(status)))? } let mut decoder = Decoder::new(Config::default(), reader); let mut response = MakeCredentialResponse::default(); @@ -182,7 +186,11 @@ impl<'a> GetAssertionRequest<'a> { let mut length = 2; length += !self.allow_list.is_empty() as usize; length += !self.extensions.is_empty() as usize; - length += self.options.is_some() as usize; + length += self + .options + .as_ref() + .map(|opt| opt.encoded()) + .unwrap_or(false) as usize; length += self.pin_auth.is_some() as usize; length += self.pin_protocol.is_some() as usize; encoder.object(length)?; @@ -236,7 +244,7 @@ impl GetAssertionResponse { pub fn decode(mut reader: R) -> FidoResult { let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; if status != 0 { - Err(FidoErrorKind::CborError(status))? + Err(FidoErrorKind::CborError(CborErrorCode::from(status)))? } let mut decoder = Decoder::new(Config::default(), reader); let mut response = GetAssertionResponse::default(); @@ -272,7 +280,7 @@ impl GetInfoResponse { pub fn decode(mut reader: R) -> FidoResult { let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; if status != 0 { - Err(FidoErrorKind::CborError(status))? + Err(FidoErrorKind::CborError(CborErrorCode::from(status)))? } let mut decoder = Decoder::new(Config::default(), reader); let mut response = GetInfoResponse::default(); @@ -360,7 +368,7 @@ impl ClientPinResponse { pub fn decode(mut reader: R) -> FidoResult { let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; if status != 0 { - Err(FidoErrorKind::CborError(status))? + Err(FidoErrorKind::CborError(CborErrorCode::from(status)))? } let mut decoder = Decoder::new(Config::default(), reader); let mut response = ClientPinResponse::default(); diff --git a/src/ctap_error_codes.csv b/src/ctap_error_codes.csv new file mode 100644 index 0000000..9c8bd9e --- /dev/null +++ b/src/ctap_error_codes.csv @@ -0,0 +1,49 @@ +Code,Name,Description +0x00,"CTAP1_ERR_SUCCESS, CTAP2_OK",Indicates successful response. +0x01,CTAP1_ERR_INVALID_COMMAND,The command is not a valid CTAP command. +0x02,CTAP1_ERR_INVALID_PARAMETER,The command included an invalid parameter. +0x03,CTAP1_ERR_INVALID_LENGTH,Invalid message or item length. +0x04,CTAP1_ERR_INVALID_SEQ,Invalid message sequencing. +0x05,CTAP1_ERR_TIMEOUT,Message timed out. +0x06,CTAP1_ERR_CHANNEL_BUSY,Channel busy. +0x0A,CTAP1_ERR_LOCK_REQUIRED,Command requires channel lock. +0x0B,CTAP1_ERR_INVALID_CHANNEL,Command not allowed on this cid. +0x11,CTAP2_ERR_CBOR_UNEXPECTED_TYPE,Invalid/unexpected CBOR error. +0x12,CTAP2_ERR_INVALID_CBOR,Error when parsing CBOR. +0x14,CTAP2_ERR_MISSING_PARAMETER,Missing non-optional parameter. +0x15,CTAP2_ERR_LIMIT_EXCEEDED,Limit for number of items exceeded. +0x16,CTAP2_ERR_UNSUPPORTED_EXTENSION,Unsupported extension. +0x19,CTAP2_ERR_CREDENTIAL_EXCLUDED,Valid credential found in the exclude list. +0x21,CTAP2_ERR_PROCESSING,Processing (Lengthy operation is in progress). +0x22,CTAP2_ERR_INVALID_CREDENTIAL,Credential not valid for the authenticator. +0x23,CTAP2_ERR_USER_ACTION_PENDING,Authentication is waiting for user interaction. +0x24,CTAP2_ERR_OPERATION_PENDING,"Processing, lengthy operation is in progress." +0x25,CTAP2_ERR_NO_OPERATIONS,No request is pending. +0x26,CTAP2_ERR_UNSUPPORTED_ALGORITHM,Authenticator does not support requested algorithm. +0x27,CTAP2_ERR_OPERATION_DENIED,Not authorized for requested operation. +0x28,CTAP2_ERR_KEY_STORE_FULL,Internal key storage is full. +0x29,CTAP2_ERR_NOT_BUSY,Authenticator cannot cancel as it is not busy. +0x2A,CTAP2_ERR_NO_OPERATION_PENDING,No outstanding operations. +0x2B,CTAP2_ERR_UNSUPPORTED_OPTION,Unsupported option. +0x2C,CTAP2_ERR_INVALID_OPTION,Not a valid option for current operation. +0x2D,CTAP2_ERR_KEEPALIVE_CANCEL,Pending keep alive was cancelled. +0x2E,CTAP2_ERR_NO_CREDENTIALS,No valid credentials provided. +0x2F,CTAP2_ERR_USER_ACTION_TIMEOUT,Timeout waiting for user interaction. +0x30,CTAP2_ERR_NOT_ALLOWED,"Continuation command, such as, authenticatorGetNextAssertion not allowed." +0x31,CTAP2_ERR_PIN_INVALID,PIN Invalid. +0x32,CTAP2_ERR_PIN_BLOCKED,PIN Blocked. +0x33,CTAP2_ERR_PIN_AUTH_INVALID,"PIN authentication, pinAuth, verification failed." +0x34,CTAP2_ERR_PIN_AUTH_BLOCKED,"PIN authentication,pinAuth, blocked. Requires power recycle to reset." +0x35,CTAP2_ERR_PIN_NOT_SET,No PIN has been set. +0x36,CTAP2_ERR_PIN_REQUIRED,PIN is required for the selected operation. +0x37,CTAP2_ERR_PIN_POLICY_VIOLATION,PIN policy violation. Currently only enforces minimum length. +0x38,CTAP2_ERR_PIN_TOKEN_EXPIRED,pinToken expired on authenticator. +0x39,CTAP2_ERR_REQUEST_TOO_LARGE,Authenticator cannot handle this request due to memory constraints. +0x3A,CTAP2_ERR_ACTION_TIMEOUT,The current operation has timed out. +0x3B,CTAP2_ERR_UP_REQUIRED,User presence is required for the requested operation. +0x7F,CTAP1_ERR_OTHER,Other unspecified error. +0xDF,CTAP2_ERR_SPEC_LAST,CTAP 2 spec last error. +0xE0,CTAP2_ERR_EXTENSION_FIRST,Extension specific error. +0xEF,CTAP2_ERR_EXTENSION_LAST,Extension specific error. +0xF0,CTAP2_ERR_VENDOR_FIRST,Vendor specific error. +0xFF,CTAP2_ERR_VENDOR_LAST,Vendor specific error. diff --git a/src/error.rs b/src/error.rs index c1f5c74..37d7652 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,8 @@ // http://opensource.org/licenses/MIT>, at your option. This file may not be // copied, modified, or distributed except according to those terms. use cbor_codec::{DecodeError, EncodeError}; - +use csv_core::{ReadFieldResult, Reader}; +use failure::_core::fmt::{Error, Formatter}; use failure::{Backtrace, Context, Fail}; use std::fmt; use std::fmt::Display; @@ -15,6 +16,9 @@ pub type FidoResult = Result; #[derive(Debug)] pub struct FidoError(Context); +#[derive(Debug, Copy, Clone, Fail, Eq, PartialEq)] +pub struct CborErrorCode(u8); + #[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] pub enum FidoErrorKind { #[fail(display = "Read/write error with device.")] @@ -43,8 +47,8 @@ pub enum FidoErrorKind { DecryptPin, #[fail(display = "Supplied key has incorrect type.")] KeyType, - #[fail(display = "Device returned error: 0x{:x}", _0)] - CborError(u8), + #[fail(display = "Device returned error: {}", _0)] + CborError(CborErrorCode), #[fail(display = "Device does not support FIDO2")] DeviceUnsupported, #[fail(display = "This operating requires a PIN but none was provided.")] @@ -99,3 +103,68 @@ impl From for FidoError { FidoError(err.context(FidoErrorKind::CborDecode)) } } + +impl From for CborErrorCode { + fn from(code: u8) -> Self { + Self(code) + } +} + +impl Display for CborErrorCode { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + let messages = include_str!("ctap_error_codes.csv"); + let mut rdr = Reader::new(); + let mut bytes = messages.as_bytes(); + let mut col: usize = 0; + let mut row: usize = 0; + let mut correct_row: bool = false; + let mut field = [0u8; 1024]; + let hex = format!("{:x?}", self.0); + let mut name: Option = None; + let mut desc: Option = None; + loop { + let (result, nin, read) = rdr.read_field(&bytes, &mut field); + bytes = &bytes[nin..]; + match result { + ReadFieldResult::InputEmpty => {} + ReadFieldResult::OutputFull => panic!("field too large"), + ReadFieldResult::Field { record_end } => { + let text = String::from_utf8(field[..read].iter().cloned().collect()).unwrap(); + if row > 0 { + match col { + 0 if i64::from_str_radix(&text[2..], 16) + .expect("malformed ctap_error_codes.csv") + == self.0 as i64 => + { + correct_row = true + } + 1 | 2 if correct_row => { + if let Some(_) = name { + desc = Some(text); + break; + } else { + name = Some(text); + } + } + _ => (), + } + } + col += 1; + if record_end { + col = 0; + row += 1; + } + } + ReadFieldResult::End => break, + } + } + if let Some((code, name, desc)) = + name.and_then(|name| desc.map(|desc| (self.0, name, desc))) + { + write!(f, "CborError: 0x{:x?}: {}", code, desc); + } else { + write!(f, "CborError: 0x{:x?}", self.0); + } + Ok(()) + } +} diff --git a/src/extensions/hmac.rs b/src/extensions/hmac.rs index 78e4bdd..2318781 100644 --- a/src/extensions/hmac.rs +++ b/src/extensions/hmac.rs @@ -1,4 +1,6 @@ -use crate::cbor; +use crate::{ + cbor, AuthenticatorOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, +}; use crate::{FidoCredential, FidoDevice, FidoErrorKind, FidoResult}; use cbor_codec::value::{Bytes, Int, Key, Text, Value}; use cbor_codec::Encoder; @@ -31,23 +33,37 @@ pub trait HmacExtension { "hmac-secret" } + fn extension_input() -> &'static Value { + &Value::Bool(true) + } + /// Generates data for the extension field as part of the assertion request 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())), + Key::Text(Text::Text( + ::extension_name().to_owned(), + )), self.get_data(salt, salt2)?, ); Ok(Value::Map(map)) } - /// Wraps [`get_dict`] fn get_data(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult; /// Convenience function to create an credential with default rp_id and user_name /// Use `FidoDevice::make_credential` if you need more control fn make_hmac_credential(&mut self) -> FidoResult; + fn make_hmac_credential_full( + &mut self, + rp: cbor::PublicKeyCredentialRpEntity, + user: cbor::PublicKeyCredentialUserEntity, + client_data_hash: &[u8], + exclude_list: &[cbor::PublicKeyCredentialDescriptor], + options: Option, + ) -> FidoResult; + /// Request an assertion from the authenticator for a given credential and salt(s). /// at least one `salt` must be provided, consider using a hashing function like SHA256 /// to ensure that your salt will fit 32 bytes. @@ -63,6 +79,7 @@ pub trait HmacExtension { credential: &FidoHmacCredential, salt: &[u8; 32], salt2: Option<&[u8; 32]>, + options: Option, ) -> FidoResult<([u8; 32], Option<[u8; 32]>)>; /// Convenience function for `get_hmac_assertion` that will accept arbitrary @@ -76,8 +93,13 @@ pub trait HmacExtension { let mut digest = Sha256::new(); digest.input(input); digest.result(&mut salt); - self.get_hmac_assertion(credential, &salt, None) - .map(|secret| secret.0) + self.get_hmac_assertion( + credential, + &salt, + None, + Some(AuthenticatorOptions { uv: true, rk: true }), + ) + .map(|secret| secret.0) } } @@ -135,15 +157,53 @@ impl HmacExtension for FidoDevice { } fn make_hmac_credential(&mut self) -> FidoResult { - self.make_credential("hmac", &[0u8], "commandline", &[0u8; 32]) + let rp = PublicKeyCredentialRpEntity { + id: "hmac", + name: None, + icon: None, + }; + let user = PublicKeyCredentialUserEntity { + id: &[0u8], + name: "commandline", + icon: None, + display_name: None, + }; + let options = Some(AuthenticatorOptions { + uv: true, + rk: false, + }); + + self.make_hmac_credential_full(rp, user, &[0u8; 32], &[], options) .map(|cred| cred.into()) } + fn make_hmac_credential_full( + &mut self, + rp: cbor::PublicKeyCredentialRpEntity, + user: cbor::PublicKeyCredentialUserEntity, + client_data_hash: &[u8], + exclude_list: &[cbor::PublicKeyCredentialDescriptor], + options: Option, + ) -> FidoResult { + self.make_credential_full( + rp, + user, + client_data_hash, + exclude_list, + &[( + ::extension_name(), + ::extension_input(), + )], + options, + ) + } + fn get_hmac_assertion( &mut self, credential: &FidoHmacCredential, salt: &[u8; 32], salt2: Option<&[u8; 32]>, + options: Option, ) -> FidoResult<([u8; 32], Option<[u8; 32]>)> { let client_data_hash = [0u8; 32]; while self.shared_secret.is_none() { @@ -170,10 +230,7 @@ impl HmacExtension for FidoDevice { client_data_hash: &client_data_hash, allow_list: &allow_list, extensions: &[(::extension_name(), &ext_data)], - options: Some(cbor::AuthenticatorOptions { - rk: false, - uv: true, - }), + options: options, pin_auth, pin_protocol: pin_auth.and(Some(0x01)), }; diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index c0f9333..07d0006 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -1 +1,2 @@ pub mod hmac; +pub use hmac::*; diff --git a/src/lib.rs b/src/lib.rs index 038709e..5173c38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,10 @@ use std::io::{Cursor, Write}; use std::u16; use std::u8; +pub use self::cbor::{ + AuthenticatorOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, +}; pub use self::error::*; use self::hid_linux as hid; use self::packet::CtapCommand; @@ -232,16 +236,7 @@ impl FidoDevice { user_name: &str, client_data_hash: &[u8], ) -> FidoResult { - 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)); + //TODO: implement all options: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential let rp = cbor::PublicKeyCredentialRpEntity { id: rp_id, name: None, @@ -253,21 +248,60 @@ impl FidoDevice { icon: None, display_name: None, }; + + let options = Some(AuthenticatorOptions { + uv: true, + rk: false, + }); + self.make_credential_full(rp, user, client_data_hash, &[], &[], options) + } + + /// Request a new credential from the authenticator. The `rp_id` should be + /// a stable string used to identify the party for whom the credential is + /// created, for convenience it will be returned with the credential. + /// `user_id` and `user_name` are not required when requesting attestations + /// but they MAY be displayed to the user and MAY be stored on the device + /// to be returned with an attestation if the device supports this. + /// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`, + /// this is only used to verify the attestation provided by the + /// authenticator. When not implementing WebAuthN this can be any random + /// 32-byte array. + /// + /// This method will fail if a PIN is required but the device is not + /// unlocked or if the device returns malformed data. + pub fn make_credential_full( + &mut self, + rp: cbor::PublicKeyCredentialRpEntity, + user: cbor::PublicKeyCredentialUserEntity, + client_data_hash: &[u8], + exclude_list: &[cbor::PublicKeyCredentialDescriptor], + extensions: &[(&str, &cbor_codec::value::Value)], + options: Option, + ) -> FidoResult { + if self.needs_pin && self.pin_token.is_none() { + Err(FidoErrorKind::PinRequired)? + } + if client_data_hash.len() != 32 { + Err(FidoErrorKind::CborEncode)? + } let pub_key_cred_params = [("public-key", -7)]; + let pin_auth = self + .pin_token + .as_ref() + .map(|token| token.auth(&client_data_hash)); + let rp_id = rp.id.to_owned(); let request = cbor::MakeCredentialRequest { client_data_hash, rp, user, pub_key_cred_params: &pub_key_cred_params, - exclude_list: Default::default(), - extensions: Default::default(), - options: Some(cbor::AuthenticatorOptions { - rk: false, - uv: true, - }), + exclude_list: exclude_list, + extensions: extensions, + options: options, pin_auth, pin_protocol: pin_auth.and(Some(0x01)), }; + let response = match self.cbor(cbor::Request::MakeCredential(request))? { cbor::Response::MakeCredential(resp) => resp, _ => Err(FidoErrorKind::CborDecode)?, @@ -281,7 +315,7 @@ impl FidoDevice { .bytes(); Ok(FidoCredential { id: response.auth_data.attested_credential_data.credential_id, - rp_id: String::from(rp_id), + rp_id: rp_id, public_key: Vec::from(&public_key[..]), }) }