From d4c9dd913fa2ea72b202c1752ed70fccaa2a56ee Mon Sep 17 00:00:00 2001 From: shimun Date: Mon, 30 Mar 2020 00:00:55 +0200 Subject: [PATCH] use builder pattern to expose all possible options --- Cargo.toml | 7 +- README.md | 28 +-- examples/hmac.rs | 48 ++--- examples/multiple.rs | 55 ++++++ src/cbor.rs | 9 +- src/error.rs | 2 + src/extensions/hmac.rs | 151 +++++---------- src/lib.rs | 419 +++++++++++++++++++++++++++-------------- 8 files changed, 420 insertions(+), 299 deletions(-) create mode 100644 examples/multiple.rs diff --git a/Cargo.toml b/Cargo.toml index 1bfca3c..392d84e 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.2.1" +version = "0.3.0" license = "Apache-2.0/MIT" homepage = "https://github.com/ArdaXi/ctap/pull/2" repository = "https://github.com/shimunn/ctap" @@ -19,5 +19,8 @@ cbor-codec = "0.7" ring = "0.13" untrusted = "0.6" rust-crypto = "0.2" -hex = "0.4.0" csv-core = "0.1.6" +derive_builder = "0.9.0" +[dev-dependencies] +crossbeam = "0.7.3" +hex = "0.4.0" diff --git a/README.md b/README.md index f44efd4..cb3c5a1 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,28 @@ ctap is a library implementing the [FIDO2 CTAP](https://fidoalliance.org/specs/f ## Usage example ```rust -let devices = ctap::get_devices()?; -let device_info = &devices[0]; -let mut device = ctap::FidoDevice::new(device_info)?; +use ctap_hmac::*; +let device_info = get_devices()?.next().expect("no device connected"); +let mut device = FidoDevice::new(&device_info)?; // This can be omitted if the FIDO device is not configured with a PIN. let pin = "test"; device.unlock(pin)?; // In a real application these values would come from the requesting app. -let rp_id = "rp_id"; -let user_id = [0]; -let user_name = "user_name"; -let client_data_hash = [0; 32]; -let cred = device.make_credential( - rp_id, - &user_id, - user_name, - &client_data_hash -)?; +let cred_request = FidoCredentialRequestBuilder::default() + .rp_id("rp_id") + .user_name("user_name") + .build().unwrap(); +let cred = device.make_credential(&cred_request)?; +let cred = &&cred; +let assertion_request = FidoAssertionRequestBuilder::default() + .rp_id("rp_id") + .credential(&&cred) + .build().unwrap(); // In a real application the credential would be stored and used later. -let result = device.get_assertion(&cred, &client_data_hash); +let result = device.get_assertion(&assertion_request); ``` ## Limitations diff --git a/examples/hmac.rs b/examples/hmac.rs index 00c78d1..b2dc792 100644 --- a/examples/hmac.rs +++ b/examples/hmac.rs @@ -2,8 +2,8 @@ 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 ctap::extensions::hmac::HmacExtension; +use ctap::{FidoCredential, FidoCredentialRequestBuilder, AuthenticatorOptions}; use hex; use std::env::args; use std::io::prelude::*; @@ -14,38 +14,21 @@ const RP_ID: &str = "ctap_demo"; fn main() -> ctap::FidoResult<()> { let mut devices = ctap::get_devices()?; - let device_info = &mut devices.next().expect("No authenicator found"); + let device_info = &mut devices.next().expect("No authenticator found"); let mut device = ctap::FidoDevice::new(device_info)?; - let options = || { - Some(AuthenticatorOptions { - uv: false, - rk: true, - }) - }; - let mut credential = match args().skip(1).next().map(|h| FidoHmacCredential { + + let mut credential = match args().skip(1).next().map(|h| FidoCredential { id: hex::decode(&h).expect("Invalid credential"), - rp_id: RP_ID.into(), + public_key: None, }) { Some(cred) => cred, _ => { - let rp = PublicKeyCredentialRpEntity { - id: RP_ID, - name: Some("ctap_hmac crate"), - icon: None, - }; - let user = PublicKeyCredentialUserEntity { - id: &[0u8], - name: "commandline", - icon: None, - display_name: None, - }; + let req = FidoCredentialRequestBuilder::default().rp_id(RP_ID).rp_name("ctap_hmac crate").user_name("example").uv(false).build().unwrap(); 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 cred = device.make_hmac_credential(req).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 } }; let credential = credential; @@ -61,14 +44,7 @@ fn main() -> ctap::FidoResult<()> { let mut digest = Sha256::new(); digest.input(&message.as_bytes()); digest.result(&mut salt); - let hash = device - .get_hmac_assertion( - &credential, - &salt, - None, - None, - )? - .0; - println!("Hash: {}", hex::encode(&hash)); + let (cred, (hash1, _hash2)) = device.get_hmac_assertion(RP_ID, &[&credential], &salt, None, None)?; + println!("Hash: {}", hex::encode(&hash1)); Ok(()) } diff --git a/examples/multiple.rs b/examples/multiple.rs new file mode 100644 index 0000000..f428f0b --- /dev/null +++ b/examples/multiple.rs @@ -0,0 +1,55 @@ +extern crate ctap_hmac as ctap; + +use crypto::digest::Digest; +use crypto::sha2::Sha256; +use ctap::{FidoCredential, FidoCredentialRequestBuilder, FidoAssertionRequestBuilder, AuthenticatorOptions, FidoDevice, FidoError, FidoResult}; +use failure::_core::time::Duration; +use hex; +use std::env::args; +use std::io::prelude::*; +use std::io::stdin; +use std::io::stdout; +use std::sync::mpsc::channel; +use std::sync::Mutex; +use crossbeam::thread; + +const RP_ID: &str = "ctap_demo"; + +fn run() -> ctap::FidoResult<()> { + let mut credentials = args().skip(1).map(|id| FidoCredential { + id: hex::decode(&id).expect("Invalid credential"), + public_key: None, + }).collect::>(); + if credentials.len() == 0 { + credentials = ctap::get_devices()?.map(|h| FidoDevice::new(&h).and_then(|mut dev| FidoCredentialRequestBuilder::default() + .rp_id(RP_ID).build().unwrap().make_credential(&mut dev))).collect::>>()?; + } + let credentials = credentials.iter().collect::>(); + let (s, r) = channel(); + thread::scope(|scope| { + let handles = ctap::get_devices()?.map(|h| { + let req = FidoAssertionRequestBuilder::default().rp_id(RP_ID).credentials(&credentials[..]).build().unwrap(); + let s = s.clone(); + scope.spawn(move |_| { + FidoDevice::new(&h).and_then(|mut dev| { + req.get_assertion(&mut dev).map(|res| { + s.send(res.clone()); + res + }) + }) + }) + }).collect::>(); + for h in handles { + h.join(); + } + Ok::<(), FidoError>(()) + }).unwrap(); + for res in r.iter().take(credentials.len()) { + dbg!(res); + } + Ok(()) +} + +fn main() { + dbg!(run()); +} diff --git a/src/cbor.rs b/src/cbor.rs index 368a5ae..173f610 100644 --- a/src/cbor.rs +++ b/src/cbor.rs @@ -700,15 +700,16 @@ impl PublicKeyCredentialDescriptor { pub struct AuthenticatorOptions { pub rk: bool, pub uv: bool, + pub up: bool, } impl AuthenticatorOptions { pub fn encoded(&self) -> bool { - self.rk || self.uv + self.rk || self.uv || self.up } pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { - let length = (self.rk as usize) + (self.uv as usize); + let length = (self.rk as usize) + (self.uv as usize) + (self.up as usize); encoder.object(length)?; if self.rk { encoder.text("rk")?; @@ -718,6 +719,10 @@ impl AuthenticatorOptions { encoder.text("uv")?; encoder.bool(true)?; } + if self.up { + encoder.text("up")?; + encoder.bool(true)?; + } Ok(()) } } diff --git a/src/error.rs b/src/error.rs index c2331c2..3cd5ef6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,8 @@ pub enum FidoErrorKind { #[fail(display = "Failed to decrypt PIN.")] DecryptPin, #[fail(display = "Supplied key has incorrect type.")] + VerifySignature, + #[fail(display = "Failed to verify response signature.")] KeyType, #[fail(display = "Device returned error: {}", _0)] CborError(CborErrorCode), diff --git a/src/extensions/hmac.rs b/src/extensions/hmac.rs index 2318781..d3026a6 100644 --- a/src/extensions/hmac.rs +++ b/src/extensions/hmac.rs @@ -1,5 +1,6 @@ use crate::{ - cbor, AuthenticatorOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + AuthenticatorOptions, FidoAssertionRequestBuilder, + FidoCredentialRequest, }; use crate::{FidoCredential, FidoDevice, FidoErrorKind, FidoResult}; use cbor_codec::value::{Bytes, Int, Key, Text, Value}; @@ -12,21 +13,7 @@ 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, - } - } -} +use std::iter::FromIterator; pub trait HmacExtension { fn extension_name() -> &'static str { @@ -51,18 +38,10 @@ pub trait HmacExtension { 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 + /// Convenience function to create an credential which includes extension specific data /// Use `FidoDevice::make_credential` if you need more control - fn make_hmac_credential(&mut self) -> FidoResult; + fn make_hmac_credential(&mut self, request: FidoCredentialRequest) -> 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 @@ -74,19 +53,21 @@ pub trait HmacExtension { /// provided, and will fail if a PIN is required but not provided or if the /// device returns malformed data. /// - fn get_hmac_assertion( + fn get_hmac_assertion<'a>( &mut self, - credential: &FidoHmacCredential, + rp_id: &str, + credentials: &'a [&'a FidoCredential], salt: &[u8; 32], salt2: Option<&[u8; 32]>, options: Option, - ) -> FidoResult<([u8; 32], Option<[u8; 32]>)>; + ) -> FidoResult<(&'a FidoCredential, ([u8; 32], Option<[u8; 32]>))>; /// Convenience function for `get_hmac_assertion` that will accept arbitrary /// lenght input which will then be hashed and passed on fn hmac_challange( &mut self, - credential: &FidoHmacCredential, + rp_id: &str, + credential: &FidoCredential, input: &[u8], ) -> FidoResult<[u8; 32]> { let mut salt = [0u8; 32]; @@ -94,12 +75,13 @@ pub trait HmacExtension { digest.input(input); digest.result(&mut salt); self.get_hmac_assertion( - credential, + rp_id, + &[credential], &salt, None, - Some(AuthenticatorOptions { uv: true, rk: true }), + Some(AuthenticatorOptions { uv: true, rk: true, up: false }), ) - .map(|secret| secret.0) + .map(|(_cred, secret)| secret.0) } } @@ -156,94 +138,48 @@ impl HmacExtension for FidoDevice { Ok(Value::Map(map)) } - fn make_hmac_credential(&mut self) -> FidoResult { - 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(&mut self, request: FidoCredentialRequest) -> FidoResult { + let mut request = request; + request.rk = true; + request.extension_data.insert(::extension_name(), ::extension_input()); + self.make_credential(&request) } - fn make_hmac_credential_full( + fn get_hmac_assertion<'a>( &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, + rp_id: &str, + credentials: &'a [&'a FidoCredential], salt: &[u8; 32], salt2: Option<&[u8; 32]>, options: Option, - ) -> FidoResult<([u8; 32], Option<[u8; 32]>)> { - let client_data_hash = [0u8; 32]; + ) -> FidoResult<(&'a FidoCredential, ([u8; 32], Option<[u8; 32]>))> { while self.shared_secret.is_none() { self.init_shared_secret()?; } - if self.needs_pin && self.pin_token.is_none() { - Err(FidoErrorKind::PinRequired)? + let ext_data: Value = self.get_data(salt, salt2)?; + + let ext_data: BTreeMap<&str, &Value> = BTreeMap::from_iter( + [(::extension_name(), &ext_data)] + .iter() + .cloned(), + ); + + let mut builder = FidoAssertionRequestBuilder::default() + .credentials(credentials) + .rp_id(rp_id) + .extension_data(ext_data); + + if let Some(opts) = options { + builder = builder.uv(opts.uv).up(opts.up); } - 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: options, - 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 (cred, auth_data) = + self.get_assertion(&builder.build().unwrap())?; let shared_secret = self.shared_secret.as_ref().unwrap(); 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 response - .auth_data + let hmac_secret_enc = match auth_data .extensions .get(::extension_name()) .ok_or(FidoErrorKind::CborDecode)? @@ -270,6 +206,7 @@ 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..]); - Ok((hmac_secret_0, salt2.map(|_| hmac_secret_1))) + let cred = credentials.into_iter().find(|c| c.id == cred.id).unwrap(); + Ok((cred, (hmac_secret_0, salt2.and(Some(hmac_secret_1))))) } } diff --git a/src/lib.rs b/src/lib.rs index 1f6e475..ec38060 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,29 +9,31 @@ //! # Example //! //! ``` -//! # fn do_fido() -> ctap::FidoResult<()> { -//! let mut devices = ctap::get_devices()?; -//! let device_info = &devices.next().unwrap(); -//! let mut device = ctap::FidoDevice::new(device_info)?; +//! # use ctap_hmac::*; +//! # fn do_fido() -> FidoResult<()> { //! -//! // This can be omitted if the FIDO device is not configured with a PIN. -//! let pin = "test"; -//! device.unlock(pin)?; +//!use ctap_hmac::*; +//!let device_info = get_devices()?.next().expect("no device connected"); +//!let mut device = FidoDevice::new(&device_info)?; //! -//! // In a real application these values would come from the requesting app. -//! let rp_id = "rp_id"; -//! let user_id = [0]; -//! let user_name = "user_name"; -//! let client_data_hash = [0; 32]; -//! let cred = device.make_credential( -//! rp_id, -//! &user_id, -//! user_name, -//! &client_data_hash -//! )?; +//!// This can be omitted if the FIDO device is not configured with a PIN. +//!let pin = "test"; +//!device.unlock(pin)?; +//! +//!// In a real application these values would come from the requesting app. +//!let cred_request = FidoCredentialRequestBuilder::default() +//! .rp_id("rp_id") +//! .user_name("user_name") +//! .build().unwrap(); +//!let cred = device.make_credential(&cred_request)?; +//!let cred = &&cred; +//!let assertion_request = FidoAssertionRequestBuilder::default() +//! .rp_id("rp_id") +//! .credential(cred) +//! .build().unwrap(); +//!// In a real application the credential would be stored and used later. +//!let result = device.get_assertion(&assertion_request); //! -//! // In a real application the credential would be stored and used later. -//! let result = device.get_assertion(&cred, &client_data_hash); //! # Ok(()) //! # } @@ -43,6 +45,8 @@ extern crate rand; extern crate failure_derive; #[macro_use] extern crate num_derive; +#[macro_use] +extern crate derive_builder; extern crate byteorder; extern crate cbor as cbor_codec; extern crate crypto as rust_crypto; @@ -64,16 +68,16 @@ use std::io::{Cursor, Write}; use std::u16; use std::u8; -pub use self::cbor::{ - AuthenticatorOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, -}; +pub use self::cbor::AuthenticatorOptions; +use self::cbor::PublicKeyCredentialDescriptor; pub use self::error::*; use self::hid_linux as hid; use self::packet::CtapCommand; +use crate::cbor::{AuthenticatorData, GetAssertionRequest}; use failure::{Fail, ResultExt}; use num_traits::FromPrimitive; use rand::prelude::*; +use std::collections::BTreeMap; static BROADCAST_CID: [u8; 4] = [0xff, 0xff, 0xff, 0xff]; @@ -86,14 +90,12 @@ pub fn get_devices() -> FidoResult> { } /// A credential created by a FIDO2 authenticator. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FidoCredential { /// The ID provided by the authenticator. pub id: Vec, /// The public key provided by the authenticator, in uncompressed form. - pub public_key: Vec, - /// The Relying Party ID provided by the platform when this key was generated. - pub rp_id: String, + pub public_key: Option>, } /// An opened FIDO authenticator. @@ -107,6 +109,145 @@ pub struct FidoDevice { aaguid: [u8; 16], } +pub struct FidoCancelHandle { + device: fs::File, + packet_size: u16, + channel_id: [u8; 4], +} + +impl FidoCancelHandle { + pub fn cancel(&mut self) -> FidoResult<()> { + let payload = &[1u8]; + let to_send = payload.len() as u16; + let max_payload = (self.packet_size - 7) as usize; + let (frame, payload) = payload.split_at(cmp::min(payload.len(), max_payload)); + packet::write_init_packet( + &mut self.device, + 64, + &self.channel_id, + &CtapCommand::Cancel, + to_send, + frame, + )?; + if payload.is_empty() { + return Ok(()); + } + let max_payload = (self.packet_size - 5) as usize; + for (seq, frame) in (0..u8::MAX).zip(payload.chunks(max_payload)) { + packet::write_cont_packet(&mut self.device, 64, &self.channel_id, seq, frame)?; + } + self.device.flush().context(FidoErrorKind::WritePacket)?; + Ok(()) + } + + pub fn cancel_after(&mut self, body: impl Fn(()) -> T) -> FidoResult { + let res = body(()); + match self.cancel() { + Ok(_) => Ok(res), + Err(e) => Err(e), + } + } +} + +/// 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. +#[derive(Clone, Debug, Builder)] +#[builder(setter(into))] +#[builder(pattern = "owned")] +pub struct FidoCredentialRequest<'a> { + /// create resident key + #[builder(default)] + rk: bool, + /// user verification + #[builder(default)] + uv: bool, + /// relying party id + rp_id: &'a str, + /// relying party id + #[builder(default)] + rp_name: Option<&'a str>, + /// relying party icon url + #[builder(default)] + rp_icon_url: Option<&'a str>, + /// user id + #[builder(default = "&[0u8]")] + user_id: &'a [u8], + /// user name + #[builder(default)] + user_name: Option<&'a str>, + /// user icon url + #[builder(default)] + user_icon_url: Option<&'a str>, + /// user display name + #[builder(default)] + user_display_name: Option<&'a str>, + #[builder(default = "&[]")] + exclude_list: &'a [&'a FidoCredential], + #[builder(default = "&[0u8; 32]")] + client_data_hash: &'a [u8], + #[builder(default)] + extension_data: BTreeMap<&'a str, &'a cbor_codec::value::Value>, +} + +impl<'a> FidoCredentialRequest<'a> { + pub fn make_credential(&self, device: &mut FidoDevice) -> FidoResult { + device.make_credential(&self) + } +} + +/// Request an assertion from the authenticator for a given credential. +/// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`, +/// this is signed and verified as part of the attestation. When not +/// implementing WebAuthN this can be any random 32-byte array. +/// +/// This method will return whether the assertion matches the credential +/// provided, and will fail if a PIN is required but not provided or if the +/// device returns malformed data. +#[derive(Clone, Debug, Builder)] +#[builder(setter(into))] +#[builder(pattern = "owned")] +pub struct FidoAssertionRequest<'a> { + #[builder(default)] + up: bool, + #[builder(default)] + rk: bool, + #[builder(default)] + uv: bool, + /// The Relying Party ID provided by the platform when this key was generated. + rp_id: &'a str, + credentials: &'a [&'a FidoCredential], + #[builder(default = "&[]")] + exclude_list: &'a [&'a FidoCredential], + #[builder(default = "&[0u8; 32]")] + client_data_hash: &'a [u8], + #[builder(default)] + extension_data: BTreeMap<&'a str, &'a cbor_codec::value::Value>, +} + +impl<'a> FidoAssertionRequest<'a> { + pub fn get_assertion(&self, device: &mut FidoDevice) -> FidoResult<&'a FidoCredential> { + device.get_assertion(self).map(|res| res.0) + } +} + +impl<'a> FidoAssertionRequestBuilder<'a> { + pub fn credential(mut self, credential: &'a &'a FidoCredential) -> Self { + self.credentials = Some(std::slice::from_ref(credential)); + self + } +} + impl FidoDevice { /// Open and initialize a given device. DeviceInfo is provided by the `get_devices` /// function. This method will allocate a channel for this application, verify that @@ -216,88 +357,72 @@ impl FidoDevice { } } - /// 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 cancel_handle(&mut self) -> FidoResult { + Ok(self + .device + .try_clone() + .map(|device| FidoCancelHandle { + device, + packet_size: self.packet_size, + channel_id: self.channel_id, + }) + .context(FidoErrorKind::Io)?) + } + pub fn make_credential( &mut self, - rp_id: &str, - user_id: &[u8], - user_name: &str, - client_data_hash: &[u8], + request: &FidoCredentialRequest<'_>, ) -> FidoResult { - //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, - icon: None, + id: request.rp_id, + name: request.rp_name, + icon: request.rp_icon_url, }; let user = cbor::PublicKeyCredentialUserEntity { - id: user_id, - name: user_name, - icon: None, - display_name: None, + id: request.user_id, + name: request.user_name.unwrap_or(""), + icon: request.user_icon_url, + display_name: request.user_display_name, }; let options = Some(AuthenticatorOptions { - uv: true, - rk: false, + up: false, + uv: request.uv, + rk: request.rk, }); - 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 { + if request.client_data_hash.len() != 32 { Err(FidoErrorKind::CborEncode)? } + while self.shared_secret.is_none() { + self.init_shared_secret()?; + } 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(); + .map(|token| token.auth(&request.client_data_hash)); let request = cbor::MakeCredentialRequest { - client_data_hash, + client_data_hash: request.client_data_hash, rp, user, pub_key_cred_params: &pub_key_cred_params, - exclude_list: exclude_list, - extensions: extensions, - options: options, + exclude_list: &request + .exclude_list + .iter() + .map(|cred| PublicKeyCredentialDescriptor { + cred_type: "public-key".into(), + id: cred.id.clone(), + }) + .collect::>()[..], + extensions: &request + .extension_data + .iter() + .map(|(name, data)| (*name, *data)) + .collect::>()[..], + options, pin_auth, pin_protocol: pin_auth.and(Some(0x01)), }; @@ -315,85 +440,103 @@ impl FidoDevice { .bytes(); Ok(FidoCredential { id: response.auth_data.attested_credential_data.credential_id, - rp_id: rp_id, - public_key: Vec::from(&public_key[..]), + public_key: Some(Vec::from(&public_key[..])), }) } - /// Request an assertion from the authenticator for a given credential. + /// 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 signed and verified as part of the attestation. When not - /// implementing WebAuthN this can be any random 32-byte array. + /// 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 return whether the assertion matches the credential - /// provided, and will fail if a PIN is required but not provided or if the - /// device returns malformed data. - pub fn get_assertion( - &mut self, - credential: &FidoCredential, - client_data_hash: &[u8], - ) -> FidoResult { - self.get_assertion_multiple(&[credential], client_data_hash) - } + /// This method will fail if a PIN is required but the device is not + /// unlocked or if the device returns malformed data. - pub fn get_assertion_multiple( + pub fn get_assertion<'a>( &mut self, - credentials: &[&FidoCredential], - client_data_hash: &[u8], - ) -> FidoResult { + assertion: &FidoAssertionRequest<'a>, + ) -> FidoResult<(&'a FidoCredential, AuthenticatorData)> { + 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 { + if assertion.client_data_hash.len() != 32 { Err(FidoErrorKind::CborEncode)? } let pin_auth = self .pin_token .as_ref() - .map(|token| token.auth(&client_data_hash)); - let allow_list = credentials - .iter() - .map(|cred| cbor::PublicKeyCredentialDescriptor { - cred_type: String::from("public-key"), - id: cred.id.clone(), - }) - .collect::>(); - let request = cbor::GetAssertionRequest { - rp_id: &credentials[0].rp_id, - client_data_hash: client_data_hash, - allow_list: &allow_list, - extensions: Default::default(), - options: Some(cbor::AuthenticatorOptions { - rk: false, - uv: true, + .map(|token| token.auth(&assertion.client_data_hash)); + let request = GetAssertionRequest { + rp_id: assertion.rp_id, + client_data_hash: assertion.client_data_hash, + allow_list: &assertion + .credentials + .iter() + .map(|cred| PublicKeyCredentialDescriptor { + cred_type: "public-key".into(), + id: cred.id.clone(), + }) + .collect::>()[..], + extensions: &assertion + .extension_data + .iter() + .map(|(name, data)| (*name, *data)) + .collect::>()[..], + options: Some(AuthenticatorOptions { + rk: assertion.rk, + uv: assertion.uv, + up: assertion.up, }), - pin_auth, + pin_auth: 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)?, }; - Ok(credentials + let credential = assertion + .credentials .iter() - .filter(|cred| { + .flat_map(|cred| { response .credential .as_ref() - .map(|cred2| cred2.id == cred.id) - .unwrap_or(true) + .filter(|rcred| rcred.id == cred.id) + .map(|_| *cred) }) - .map(|cred| { - crypto::verify_signature( - &cred.public_key, - &client_data_hash, - &response.auth_data_bytes, - &response.signature, - ) + .next(); + + credential + .and_then(|cred| { + cred.public_key + .as_ref() + .map(|public_key| { + Some(crypto::verify_signature( + &public_key, + &assertion.client_data_hash, + &response.auth_data_bytes, + &response.signature, + )) + .unwrap_or(true) + }) + .iter() + .filter_map(|valid| match valid { + true => Some(cred), + false => None, + }) + .next() }) - .filter(|pass| *pass) - .next() - .unwrap_or(false)) + .ok_or(FidoError::from(FidoErrorKind::VerifySignature)) + .map(|cred| (cred, response.auth_data)) } fn cbor(&mut self, request: cbor::Request) -> FidoResult {