From f9f1655248ae13ce2da985a1debed0cf25bf81bf Mon Sep 17 00:00:00 2001 From: "L. K. Post" Date: Sun, 23 Aug 2020 21:38:19 +0200 Subject: [PATCH 1/7] Detangle APDU code from HID implementation --- src/apdu.rs | 200 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/statemachine.rs | 6 +- src/u2fprotocol.rs | 157 ++++------------------------------ src/u2ftypes.rs | 26 ------ 5 files changed, 221 insertions(+), 169 deletions(-) create mode 100644 src/apdu.rs diff --git a/src/apdu.rs b/src/apdu.rs new file mode 100644 index 00000000..2aec9bd6 --- /dev/null +++ b/src/apdu.rs @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::ffi::CString; +use std::io; + +use crate::consts::*; +use crate::util::io_err; + +pub trait APDUDevice { + fn init_apdu(&mut self) -> io::Result<()>; + fn send_apdu(&mut self, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec, [u8; 2])>; +} + +//////////////////////////////////////////////////////////////////////// +// Device Commands +//////////////////////////////////////////////////////////////////////// + +pub fn u2f_register(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result> +where + T: APDUDevice, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); + register_data.extend(challenge); + register_data.extend(application); + + let flags = U2F_REQUEST_USER_PRESENCE; + let (resp, status) = dev.send_apdu(U2F_REGISTER, flags, ®ister_data)?; + status_word_to_result(status, resp) +} + +pub fn u2f_sign( + dev: &mut T, + challenge: &[u8], + application: &[u8], + key_handle: &[u8], +) -> io::Result> +where + T: APDUDevice, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + if key_handle.len() > 256 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Key handle too large", + )); + } + + let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); + sign_data.extend(challenge); + sign_data.extend(application); + sign_data.push(key_handle.len() as u8); + sign_data.extend(key_handle); + + let flags = U2F_REQUEST_USER_PRESENCE; + let (resp, status) = dev.send_apdu(U2F_AUTHENTICATE, flags, &sign_data)?; + status_word_to_result(status, resp) +} + +pub fn u2f_is_keyhandle_valid( + dev: &mut T, + challenge: &[u8], + application: &[u8], + key_handle: &[u8], +) -> io::Result +where + T: APDUDevice, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + if key_handle.len() > 256 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Key handle too large", + )); + } + + let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); + sign_data.extend(challenge); + sign_data.extend(application); + sign_data.push(key_handle.len() as u8); + sign_data.extend(key_handle); + + let flags = U2F_CHECK_IS_REGISTERED; + let (_, status) = dev.send_apdu(U2F_AUTHENTICATE, flags, &sign_data)?; + Ok(status == SW_CONDITIONS_NOT_SATISFIED) +} + +pub fn is_v2_device(dev: &mut T) -> io::Result +where + T: APDUDevice, +{ + let (data, status) = dev.send_apdu(U2F_VERSION, 0x00, &[])?; + let actual = CString::new(data)?; + let expected = CString::new("U2F_V2")?; + status_word_to_result(status, actual == expected) +} + +//////////////////////////////////////////////////////////////////////// +// Error Handling +//////////////////////////////////////////////////////////////////////// + +pub fn status_word_to_result(status: [u8; 2], val: T) -> io::Result { + use self::io::ErrorKind::{InvalidData, InvalidInput}; + + match status { + SW_NO_ERROR => Ok(val), + SW_WRONG_DATA => Err(io::Error::new(InvalidData, "wrong data")), + SW_WRONG_LENGTH => Err(io::Error::new(InvalidInput, "wrong length")), + SW_CONDITIONS_NOT_SATISFIED => Err(io_err("conditions not satisfied")), + _ => Err(io_err(&format!("failed with status {:?}", status))), + } +} + +// https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit +// https://fidoalliance.org/specs/fido-u2f-v1. +// 0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#u2f-message-framing +pub struct APDU {} + +impl APDU { + pub fn serialize_long(ins: u8, p1: u8, data: &[u8]) -> io::Result> { + let class: u8 = 0x00; + if data.len() > 0xffff { + return Err(io_err("payload length > 2^16")); + } + + // Size of header + data + 2 zero bytes for maximum return size. + let mut bytes = vec![0u8; U2FAPDUHEADER_SIZE + data.len() + 2]; + bytes[0] = class; + bytes[1] = ins; + bytes[2] = p1; + // p2 is always 0, at least, for our requirements. + // lc[0] should always be 0. + bytes[5] = (data.len() >> 8) as u8; + bytes[6] = data.len() as u8; + bytes[7..7 + data.len()].copy_from_slice(data); + + // When sending zero data, the two data length bytes should be omitted. + // Luckily, all later bytes are zero, so we can just truncate. + if data.is_empty() { + bytes.truncate(bytes.len() - 2); + } + + Ok(bytes) + } + + // This will be used by future NFC code + #[allow(dead_code)] + pub fn serialize_short(ins: u8, p1: u8, data: &[u8]) -> io::Result> { + let class: u8 = 0x00; + if data.len() > 0xff { + return Err(io_err("payload length > 2^8")); + } + + let mut size = 5; // class, ins, p1, p2, response size field + if !data.is_empty() { + size += 1 + data.len(); // data size field and data itself + } + let mut bytes = vec![0u8; size]; + bytes[0] = class; + bytes[1] = ins; + bytes[2] = p1; + // p2 is always 0, at least, for our requirements. + bytes[4] = data.len() as u8; + + bytes[5..5 + data.len()].copy_from_slice(data); + + Ok(bytes) + } + + pub fn deserialize(mut data: Vec) -> io::Result<(Vec, [u8; 2])> { + if data.len() < 2 { + return Err(io_err("unexpected response")); + } + + let split_at = data.len() - 2; + let status = data.split_off(split_at); + + Ok((data, [status[0], status[1]])) + } +} diff --git a/src/lib.rs b/src/lib.rs index cfe82deb..67783431 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ extern crate runloop; #[macro_use] extern crate bitflags; +mod apdu; pub mod authenticatorservice; mod consts; mod statemachine; diff --git a/src/statemachine.rs b/src/statemachine.rs index 32552f8d..e4d3105a 100644 --- a/src/statemachine.rs +++ b/src/statemachine.rs @@ -2,12 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use crate::apdu::{u2f_is_keyhandle_valid, u2f_register, u2f_sign, APDUDevice}; use crate::consts::PARAMETER_SIZE; use crate::errors; use crate::platform::device::Device; use crate::platform::transaction::Transaction; use crate::statecallback::StateCallback; -use crate::u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign}; use crate::u2ftypes::U2FDevice; use std::sync::mpsc::Sender; @@ -90,7 +90,7 @@ impl StateMachine { }; // Try initializing it. - if !dev.is_u2f() || !u2f_init_device(dev) { + if !dev.is_u2f() || dev.init_apdu().is_err() { return; } @@ -182,7 +182,7 @@ impl StateMachine { }; // Try initializing it. - if !dev.is_u2f() || !u2f_init_device(dev) { + if !dev.is_u2f() || dev.init_apdu().is_err() { return; } diff --git a/src/u2fprotocol.rs b/src/u2fprotocol.rs index ca9f7043..aeec8a6c 100644 --- a/src/u2fprotocol.rs +++ b/src/u2fprotocol.rs @@ -7,115 +7,33 @@ extern crate std; use rand::{thread_rng, RngCore}; -use std::ffi::CString; use std::io; use std::io::{Read, Write}; +use crate::apdu::*; use crate::consts::*; use crate::u2ftypes::*; -use crate::util::io_err; -//////////////////////////////////////////////////////////////////////// -// Device Commands -//////////////////////////////////////////////////////////////////////// - -pub fn u2f_init_device(dev: &mut T) -> bool -where - T: U2FDevice + Read + Write, -{ - let mut nonce = [0u8; 8]; - thread_rng().fill_bytes(&mut nonce); - - // Initialize the device and check its version. - init_device(dev, &nonce).is_ok() && is_v2_device(dev).unwrap_or(false) -} - -pub fn u2f_register(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result> -where - T: U2FDevice + Read + Write, -{ - if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid parameter sizes", - )); - } - - let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); - register_data.extend(challenge); - register_data.extend(application); - - let flags = U2F_REQUEST_USER_PRESENCE; - let (resp, status) = send_apdu(dev, U2F_REGISTER, flags, ®ister_data)?; - status_word_to_result(status, resp) -} - -pub fn u2f_sign( - dev: &mut T, - challenge: &[u8], - application: &[u8], - key_handle: &[u8], -) -> io::Result> +impl APDUDevice for T where T: U2FDevice + Read + Write, { - if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid parameter sizes", - )); - } - - if key_handle.len() > 256 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Key handle too large", - )); + fn init_apdu(&mut self) -> io::Result<()> { + let mut nonce = [0u8; 8]; + thread_rng().fill_bytes(&mut nonce); + + // Initialize the device and check its version. + init_device(self, &nonce) + .and(is_v2_device(self)) + .map(|_| ()) } - let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); - sign_data.extend(challenge); - sign_data.extend(application); - sign_data.push(key_handle.len() as u8); - sign_data.extend(key_handle); - - let flags = U2F_REQUEST_USER_PRESENCE; - let (resp, status) = send_apdu(dev, U2F_AUTHENTICATE, flags, &sign_data)?; - status_word_to_result(status, resp) -} - -pub fn u2f_is_keyhandle_valid( - dev: &mut T, - challenge: &[u8], - application: &[u8], - key_handle: &[u8], -) -> io::Result -where - T: U2FDevice + Read + Write, -{ - if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid parameter sizes", - )); - } + fn send_apdu(&mut self, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec, [u8; 2])> { + let apdu = APDU::serialize_long(cmd, p1, send)?; + let data = sendrecv(self, U2FHID_MSG, &apdu)?; - if key_handle.len() > 256 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Key handle too large", - )); + APDU::deserialize(data) } - - let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); - sign_data.extend(challenge); - sign_data.extend(application); - sign_data.push(key_handle.len() as u8); - sign_data.extend(key_handle); - - let flags = U2F_CHECK_IS_REGISTERED; - let (_, status) = send_apdu(dev, U2F_AUTHENTICATE, flags, &sign_data)?; - Ok(status == SW_CONDITIONS_NOT_SATISFIED) } //////////////////////////////////////////////////////////////////////// @@ -151,32 +69,6 @@ where Ok(()) } -fn is_v2_device(dev: &mut T) -> io::Result -where - T: U2FDevice + Read + Write, -{ - let (data, status) = send_apdu(dev, U2F_VERSION, 0x00, &[])?; - let actual = CString::new(data)?; - let expected = CString::new("U2F_V2")?; - status_word_to_result(status, actual == expected) -} - -//////////////////////////////////////////////////////////////////////// -// Error Handling -//////////////////////////////////////////////////////////////////////// - -fn status_word_to_result(status: [u8; 2], val: T) -> io::Result { - use self::io::ErrorKind::{InvalidData, InvalidInput}; - - match status { - SW_NO_ERROR => Ok(val), - SW_WRONG_DATA => Err(io::Error::new(InvalidData, "wrong data")), - SW_WRONG_LENGTH => Err(io::Error::new(InvalidInput, "wrong length")), - SW_CONDITIONS_NOT_SATISFIED => Err(io_err("conditions not satisfied")), - _ => Err(io_err(&format!("failed with status {:?}", status))), - } -} - //////////////////////////////////////////////////////////////////////// // Device Communication Functions //////////////////////////////////////////////////////////////////////// @@ -210,22 +102,6 @@ where Ok(data) } -fn send_apdu(dev: &mut T, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec, [u8; 2])> -where - T: U2FDevice + Read + Write, -{ - let apdu = U2FAPDUHeader::serialize(cmd, p1, send)?; - let mut data = sendrecv(dev, U2FHID_MSG, &apdu)?; - - if data.len() < 2 { - return Err(io_err("unexpected response")); - } - - let split_at = data.len() - 2; - let status = data.split_off(split_at); - Ok((data, [status[0], status[1]])) -} - //////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////// @@ -234,7 +110,8 @@ where mod tests { use rand::{thread_rng, RngCore}; - use super::{init_device, send_apdu, sendrecv, U2FDevice}; + use super::{init_device, sendrecv, U2FDevice}; + use crate::apdu::APDUDevice; use crate::consts::{CID_BROADCAST, SW_NO_ERROR, U2FHID_INIT, U2FHID_MSG, U2FHID_PING}; mod platform { @@ -443,7 +320,7 @@ mod tests { msg.extend_from_slice(&SW_NO_ERROR); device.add_read(&msg, 0); - let (result, status) = send_apdu(&mut device, U2FHID_PING, 0xaa, &data).unwrap(); + let (result, status) = device.send_apdu(U2FHID_PING, 0xaa, &data).unwrap(); assert_eq!(result, &data); assert_eq!(status, SW_NO_ERROR); } diff --git a/src/u2ftypes.rs b/src/u2ftypes.rs index 8360e8ad..be8894f0 100644 --- a/src/u2ftypes.rs +++ b/src/u2ftypes.rs @@ -202,32 +202,6 @@ impl U2FHIDInitResp { } } -// https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit -// https://fidoalliance.org/specs/fido-u2f-v1. -// 0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#u2f-message-framing -pub struct U2FAPDUHeader {} - -impl U2FAPDUHeader { - pub fn serialize(ins: u8, p1: u8, data: &[u8]) -> io::Result> { - if data.len() > 0xffff { - return Err(io_err("payload length > 2^16")); - } - - // Size of header + data + 2 zero bytes for maximum return size. - let mut bytes = vec![0u8; U2FAPDUHEADER_SIZE + data.len() + 2]; - // cla is always 0 for our requirements - bytes[1] = ins; - bytes[2] = p1; - // p2 is always 0, at least, for our requirements. - // lc[0] should always be 0. - bytes[5] = (data.len() >> 8) as u8; - bytes[6] = data.len() as u8; - bytes[7..7 + data.len()].copy_from_slice(data); - - Ok(bytes) - } -} - #[derive(Clone, Debug)] pub struct U2FDeviceInfo { pub vendor_name: Vec, From eb0371f5b56452fd76814501d773b71dbdb13f7b Mon Sep 17 00:00:00 2001 From: "L. K. Post" Date: Tue, 29 Sep 2020 00:26:23 +0200 Subject: [PATCH 2/7] Decouple device info getter from U2FDevice --- fuzz/fuzz_targets/u2f_read.rs | 12 +++++++----- fuzz/fuzz_targets/u2f_read_write.rs | 12 +++++++----- src/freebsd/device.rs | 12 +++++++----- src/linux/device.rs | 12 +++++++----- src/macos/device.rs | 12 +++++++----- src/netbsd/device.rs | 12 +++++++----- src/openbsd/device.rs | 12 +++++++----- src/statemachine.rs | 2 +- src/stub/device.rs | 8 +++++--- src/u2fprotocol.rs | 13 ++++++++----- src/u2ftypes.rs | 5 ++++- src/windows/device.rs | 12 +++++++----- 12 files changed, 74 insertions(+), 50 deletions(-) diff --git a/fuzz/fuzz_targets/u2f_read.rs b/fuzz/fuzz_targets/u2f_read.rs index 8631549e..7ffb25fd 100644 --- a/fuzz/fuzz_targets/u2f_read.rs +++ b/fuzz/fuzz_targets/u2f_read.rs @@ -9,7 +9,7 @@ extern crate authenticator; use std::{cmp, io}; -use authenticator::{sendrecv, U2FDevice, U2FDeviceInfo}; +use authenticator::{sendrecv, U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use authenticator::{CID_BROADCAST, MAX_HID_RPT_SIZE}; struct TestDevice<'a> { @@ -70,15 +70,17 @@ impl<'a> U2FDevice for TestDevice<'a> { Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl<'a> U2FInfoQueryable for TestDevice<'a> { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } fuzz_target!(|data: &[u8]| { diff --git a/fuzz/fuzz_targets/u2f_read_write.rs b/fuzz/fuzz_targets/u2f_read_write.rs index c6277b04..99f70146 100644 --- a/fuzz/fuzz_targets/u2f_read_write.rs +++ b/fuzz/fuzz_targets/u2f_read_write.rs @@ -9,7 +9,7 @@ extern crate authenticator; use std::{cmp, io}; -use authenticator::{sendrecv, U2FDevice, U2FDeviceInfo}; +use authenticator::{sendrecv, U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use authenticator::{CID_BROADCAST, MAX_HID_RPT_SIZE}; struct TestDevice { @@ -71,15 +71,17 @@ impl U2FDevice for TestDevice { Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl U2FInfoQueryable for TestDevice { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } fuzz_target!(|data: &[u8]| { diff --git a/src/freebsd/device.rs b/src/freebsd/device.rs index 32069cd5..235b026a 100644 --- a/src/freebsd/device.rs +++ b/src/freebsd/device.rs @@ -11,7 +11,7 @@ use std::os::unix::prelude::*; use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; use crate::platform::uhid; -use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use crate::util::from_unix_result; #[derive(Debug)] @@ -100,13 +100,15 @@ impl U2FDevice for Device { Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl U2FInfoQueryable for Device { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } diff --git a/src/linux/device.rs b/src/linux/device.rs index 4a0d58d6..224424c1 100644 --- a/src/linux/device.rs +++ b/src/linux/device.rs @@ -11,7 +11,7 @@ use std::os::unix::prelude::*; use crate::consts::CID_BROADCAST; use crate::platform::{hidraw, monitor}; -use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use crate::util::from_unix_result; #[derive(Debug)] @@ -100,13 +100,15 @@ impl U2FDevice for Device { monitor::get_property_linux(&self.path, prop_name) } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl U2FInfoQueryable for Device { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } diff --git a/src/macos/device.rs b/src/macos/device.rs index 425a2795..00a0e672 100644 --- a/src/macos/device.rs +++ b/src/macos/device.rs @@ -6,7 +6,7 @@ extern crate log; use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; use crate::platform::iokit::*; -use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use core_foundation::base::*; use core_foundation::string::*; use std::convert::TryInto; @@ -144,13 +144,15 @@ impl U2FDevice for Device { unsafe { self.get_property_macos(prop_name) } } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl U2FInfoQueryable for Device { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } diff --git a/src/netbsd/device.rs b/src/netbsd/device.rs index 92e7c22e..de46e15e 100644 --- a/src/netbsd/device.rs +++ b/src/netbsd/device.rs @@ -13,7 +13,7 @@ use crate::consts::CID_BROADCAST; use crate::consts::MAX_HID_RPT_SIZE; use crate::platform::fd::Fd; use crate::platform::uhid; -use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use crate::util::io_err; #[derive(Debug)] @@ -147,13 +147,15 @@ impl U2FDevice for Device { Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl U2FInfoQueryable for Device { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } diff --git a/src/openbsd/device.rs b/src/openbsd/device.rs index f6b0901c..9a5f9861 100644 --- a/src/openbsd/device.rs +++ b/src/openbsd/device.rs @@ -10,7 +10,7 @@ use std::mem; use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; use crate::platform::monitor::FidoDev; -use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use crate::util::{from_unix_result, io_err}; #[derive(Debug)] @@ -135,13 +135,15 @@ impl U2FDevice for Device { Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl U2FInfoQueryable for Device { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } diff --git a/src/statemachine.rs b/src/statemachine.rs index e4d3105a..5c4958e6 100644 --- a/src/statemachine.rs +++ b/src/statemachine.rs @@ -8,7 +8,7 @@ use crate::errors; use crate::platform::device::Device; use crate::platform::transaction::Transaction; use crate::statecallback::StateCallback; -use crate::u2ftypes::U2FDevice; +use crate::u2ftypes::U2FInfoQueryable; use std::sync::mpsc::Sender; use std::sync::Mutex; diff --git a/src/stub/device.rs b/src/stub/device.rs index 283da8ed..419c5e62 100644 --- a/src/stub/device.rs +++ b/src/stub/device.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; use std::io; use std::io::{Read, Write}; @@ -55,11 +55,13 @@ impl U2FDevice for Device { panic!("not implemented") } - fn get_device_info(&self) -> U2FDeviceInfo { + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { panic!("not implemented") } +} - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { +impl U2FInfoQueryable for Device { + fn get_device_info(&self) -> U2FDeviceInfo { panic!("not implemented") } } diff --git a/src/u2fprotocol.rs b/src/u2fprotocol.rs index aeec8a6c..8850ad7e 100644 --- a/src/u2fprotocol.rs +++ b/src/u2fprotocol.rs @@ -110,7 +110,7 @@ where mod tests { use rand::{thread_rng, RngCore}; - use super::{init_device, sendrecv, U2FDevice}; + use super::{init_device, sendrecv, U2FDevice, U2FInfoQueryable}; use crate::apdu::APDUDevice; use crate::consts::{CID_BROADCAST, SW_NO_ERROR, U2FHID_INIT, U2FHID_MSG, U2FHID_PING}; @@ -119,7 +119,7 @@ mod tests { use std::io::{Read, Write}; use crate::consts::CID_BROADCAST; - use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; + use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; const IN_HID_RPT_SIZE: usize = 64; const OUT_HID_RPT_SIZE: usize = 64; @@ -212,14 +212,17 @@ mod tests { fn get_property(&self, prop_name: &str) -> io::Result { Ok(format!("{} not implemented", prop_name)) } - fn get_device_info(&self) -> U2FDeviceInfo { - self.dev_info.clone().unwrap() - } fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { self.dev_info = Some(dev_info); } } + + impl U2FInfoQueryable for TestDevice { + fn get_device_info(&self) -> U2FDeviceInfo { + self.dev_info.clone().unwrap() + } + } } #[test] diff --git a/src/u2ftypes.rs b/src/u2ftypes.rs index be8894f0..b8161ed0 100644 --- a/src/u2ftypes.rs +++ b/src/u2ftypes.rs @@ -41,10 +41,13 @@ pub trait U2FDevice { } fn get_property(&self, prop_name: &str) -> io::Result; - fn get_device_info(&self) -> U2FDeviceInfo; fn set_device_info(&mut self, dev_info: U2FDeviceInfo); } +pub trait U2FInfoQueryable { + fn get_device_info(&self) -> U2FDeviceInfo; +} + // Init structure for U2F Communications. Tells the receiver what channel // communication is happening on, what command is running, and how much data to // expect to receive over all. diff --git a/src/windows/device.rs b/src/windows/device.rs index 183ba71f..1c056e8e 100644 --- a/src/windows/device.rs +++ b/src/windows/device.rs @@ -9,7 +9,7 @@ use std::os::windows::io::AsRawHandle; use super::winapi::DeviceCapabilities; use crate::consts::{CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE}; -use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FInfoQueryable}; #[derive(Debug)] pub struct Device { @@ -85,13 +85,15 @@ impl U2FDevice for Device { Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) } + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl U2FInfoQueryable for Device { fn get_device_info(&self) -> U2FDeviceInfo { // unwrap is okay, as dev_info must have already been set, else // a programmer error self.dev_info.clone().unwrap() } - - fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { - self.dev_info = Some(dev_info); - } } From fcd16c360e9c0afdc71218c223bfb6a51518627e Mon Sep 17 00:00:00 2001 From: "L. K. Post" Date: Mon, 28 Sep 2020 23:54:31 +0200 Subject: [PATCH 3/7] Rename device-agnostic APDU methods --- src/apdu.rs | 16 ++++++++-------- src/statemachine.rs | 22 ++++++++++++++-------- src/u2fprotocol.rs | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/apdu.rs b/src/apdu.rs index 2aec9bd6..c8e158af 100644 --- a/src/apdu.rs +++ b/src/apdu.rs @@ -17,7 +17,7 @@ pub trait APDUDevice { // Device Commands //////////////////////////////////////////////////////////////////////// -pub fn u2f_register(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result> +pub fn apdu_register(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result> where T: APDUDevice, { @@ -34,10 +34,10 @@ where let flags = U2F_REQUEST_USER_PRESENCE; let (resp, status) = dev.send_apdu(U2F_REGISTER, flags, ®ister_data)?; - status_word_to_result(status, resp) + apdu_status_to_result(status, resp) } -pub fn u2f_sign( +pub fn apdu_sign( dev: &mut T, challenge: &[u8], application: &[u8], @@ -68,10 +68,10 @@ where let flags = U2F_REQUEST_USER_PRESENCE; let (resp, status) = dev.send_apdu(U2F_AUTHENTICATE, flags, &sign_data)?; - status_word_to_result(status, resp) + apdu_status_to_result(status, resp) } -pub fn u2f_is_keyhandle_valid( +pub fn apdu_is_keyhandle_valid( dev: &mut T, challenge: &[u8], application: &[u8], @@ -105,21 +105,21 @@ where Ok(status == SW_CONDITIONS_NOT_SATISFIED) } -pub fn is_v2_device(dev: &mut T) -> io::Result +pub fn apdu_is_v2_device(dev: &mut T) -> io::Result where T: APDUDevice, { let (data, status) = dev.send_apdu(U2F_VERSION, 0x00, &[])?; let actual = CString::new(data)?; let expected = CString::new("U2F_V2")?; - status_word_to_result(status, actual == expected) + apdu_status_to_result(status, actual == expected) } //////////////////////////////////////////////////////////////////////// // Error Handling //////////////////////////////////////////////////////////////////////// -pub fn status_word_to_result(status: [u8; 2], val: T) -> io::Result { +pub fn apdu_status_to_result(status: [u8; 2], val: T) -> io::Result { use self::io::ErrorKind::{InvalidData, InvalidInput}; match status { diff --git a/src/statemachine.rs b/src/statemachine.rs index 5c4958e6..5f82e2d7 100644 --- a/src/statemachine.rs +++ b/src/statemachine.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::apdu::{u2f_is_keyhandle_valid, u2f_register, u2f_sign, APDUDevice}; +use crate::apdu::{apdu_is_keyhandle_valid, apdu_register, apdu_sign, APDUDevice}; use crate::consts::PARAMETER_SIZE; use crate::errors; use crate::platform::device::Device; @@ -117,20 +117,25 @@ impl StateMachine { // consent, to be consistent with CTAP2 device behavior. let excluded = key_handles.iter().any(|key_handle| { is_valid_transport(key_handle.transports) - && u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential) - .unwrap_or(false) /* no match on failure */ + && apdu_is_keyhandle_valid( + dev, + &challenge, + &application, + &key_handle.credential, + ) + .unwrap_or(false) /* no match on failure */ }); while alive() { if excluded { let blank = vec![0u8; PARAMETER_SIZE]; - if u2f_register(dev, &blank, &blank).is_ok() { + if apdu_register(dev, &blank, &blank).is_ok() { callback.call(Err(errors::AuthenticatorError::U2FToken( errors::U2FTokenError::InvalidState, ))); break; } - } else if let Ok(bytes) = u2f_register(dev, &challenge, &application) { + } else if let Ok(bytes) = apdu_register(dev, &challenge, &application) { let dev_info = dev.get_device_info(); send_status( &status_mutex, @@ -201,7 +206,7 @@ impl StateMachine { // valid key handle for an appId, we'll use that appId below. let (app_id, valid_handles) = find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| { - u2f_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential) + apdu_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential) .unwrap_or(false) /* no match on failure */ }); @@ -230,7 +235,7 @@ impl StateMachine { // then just make it blink with bogus data. if valid_handles.is_empty() { let blank = vec![0u8; PARAMETER_SIZE]; - if u2f_register(dev, &blank, &blank).is_ok() { + if apdu_register(dev, &blank, &blank).is_ok() { callback.call(Err(errors::AuthenticatorError::U2FToken( errors::U2FTokenError::InvalidState, ))); @@ -239,7 +244,8 @@ impl StateMachine { } else { // Otherwise, try to sign. for key_handle in &valid_handles { - if let Ok(bytes) = u2f_sign(dev, &challenge, app_id, &key_handle.credential) + if let Ok(bytes) = + apdu_sign(dev, &challenge, app_id, &key_handle.credential) { let dev_info = dev.get_device_info(); send_status( diff --git a/src/u2fprotocol.rs b/src/u2fprotocol.rs index 8850ad7e..e76f9312 100644 --- a/src/u2fprotocol.rs +++ b/src/u2fprotocol.rs @@ -24,7 +24,7 @@ where // Initialize the device and check its version. init_device(self, &nonce) - .and(is_v2_device(self)) + .and(apdu_is_v2_device(self)) .map(|_| ()) } From 1b3d8fa821abd246e426162573eff17802166e36 Mon Sep 17 00:00:00 2001 From: "L. K. Post" Date: Sun, 23 Aug 2020 22:19:02 +0200 Subject: [PATCH 4/7] Generalize hex tracing --- src/u2ftypes.rs | 17 +++-------------- src/util.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/u2ftypes.rs b/src/u2ftypes.rs index b8161ed0..e0ea2628 100644 --- a/src/u2ftypes.rs +++ b/src/u2ftypes.rs @@ -5,18 +5,7 @@ use std::{cmp, fmt, io, str}; use crate::consts::*; -use crate::util::io_err; - -pub fn to_hex(data: &[u8], joiner: &str) -> String { - let parts: Vec = data.iter().map(|byte| format!("{:02x}", byte)).collect(); - parts.join(joiner) -} - -pub fn trace_hex(data: &[u8]) { - if log_enabled!(log::Level::Trace) { - trace!("USB send: {}", to_hex(data, "")); - } -} +use crate::util::{io_err, to_hex, trace_hex}; // Trait for representing U2F HID Devices. Requires getters/setters for the // channel ID, created during device initialization. @@ -97,7 +86,7 @@ impl U2FHIDInit { let count = cmp::min(data.len(), dev.out_init_data_size()); frame[8..8 + count].copy_from_slice(&data[..count]); - trace_hex(&frame); + trace_hex("USB send", &frame); if dev.write(&frame)? != frame.len() { return Err(io_err("device write failed")); @@ -150,7 +139,7 @@ impl U2FHIDCont { let count = cmp::min(data.len(), dev.out_cont_data_size()); frame[6..6 + count].copy_from_slice(&data[..count]); - trace_hex(&frame); + trace_hex("USB send", &frame); if dev.write(&frame)? != frame.len() { return Err(io_err("device write failed")); diff --git a/src/util.rs b/src/util.rs index 4ccb3c97..c9e599f3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -65,3 +65,14 @@ pub fn from_unix_result(rv: T) -> io::Result { pub fn io_err(msg: &str) -> io::Error { io::Error::new(io::ErrorKind::Other, msg) } + +pub fn to_hex(data: &[u8], joiner: &str) -> String { + let parts: Vec = data.iter().map(|byte| format!("{:02x}", byte)).collect(); + parts.join(joiner) +} + +pub fn trace_hex(label: &str, data: &[u8]) { + if log_enabled!(log::Level::Trace) { + trace!("{}: {}", label, to_hex(data, "")); + } +} From e47a02ac6e1873175defe4b10d71870717916d69 Mon Sep 17 00:00:00 2001 From: "L. K. Post" Date: Tue, 29 Sep 2020 00:02:48 +0200 Subject: [PATCH 5/7] First attempt at decoupling Device/Transaction stuff from StateMachine --- src/manager.rs | 4 +- src/statemachine.rs | 333 +++++++++++++++++++++++++------------------- 2 files changed, 193 insertions(+), 144 deletions(-) diff --git a/src/manager.rs b/src/manager.rs index 06f972db..b2d176de 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -60,7 +60,7 @@ impl U2FManager { callback, }) => { // This must not block, otherwise we can't cancel. - sm.register( + sm._register( flags, timeout, challenge, @@ -80,7 +80,7 @@ impl U2FManager { callback, }) => { // This must not block, otherwise we can't cancel. - sm.sign( + sm._sign( flags, timeout, challenge, diff --git a/src/statemachine.rs b/src/statemachine.rs index 5f82e2d7..060a3056 100644 --- a/src/statemachine.rs +++ b/src/statemachine.rs @@ -66,7 +66,84 @@ impl StateMachine { Default::default() } - pub fn register( + pub fn register( + dev: &mut T, + flags: crate::RegisterFlags, + challenge: &[u8], + application: crate::AppId, + key_handles: &[crate::KeyHandle], + status_mutex: &Mutex>, + callback: &StateCallback>, + alive: &dyn Fn() -> bool, + ) where + T: APDUDevice + U2FInfoQueryable, + { + // Try initializing it. + if dev.init_apdu().is_err() { + return; + } + + // We currently support none of the authenticator selection + // criteria because we can't ask tokens whether they do support + // those features. If flags are set, ignore all tokens for now. + // + // Technically, this is a ConstraintError because we shouldn't talk + // to this authenticator in the first place. But the result is the + // same anyway. + if !flags.is_empty() { + return; + } + + send_status( + &status_mutex, + crate::StatusUpdate::DeviceAvailable { + dev_info: dev.get_device_info(), + }, + ); + + // Iterate the exclude list and see if there are any matches. + // If so, we'll keep polling the device anyway to test for user + // consent, to be consistent with CTAP2 device behavior. + let excluded = key_handles.iter().any(|key_handle| { + is_valid_transport(key_handle.transports) + && apdu_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential) + .unwrap_or(false) /* no match on failure */ + }); + + while alive() { + if excluded { + let blank = vec![0u8; PARAMETER_SIZE]; + if apdu_register(dev, &blank, &blank).is_ok() { + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::InvalidState, + ))); + break; + } + } else if let Ok(bytes) = apdu_register(dev, &challenge, &application) { + let dev_info = dev.get_device_info(); + send_status( + &status_mutex, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + callback.call(Ok((bytes, dev_info))); + break; + } + + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } + + send_status( + &status_mutex, + crate::StatusUpdate::DeviceUnavailable { + dev_info: dev.get_device_info(), + }, + ); + } + + pub fn _register( &mut self, flags: crate::RegisterFlags, timeout: u64, @@ -89,80 +166,127 @@ impl StateMachine { _ => return, }; - // Try initializing it. - if !dev.is_u2f() || dev.init_apdu().is_err() { - return; - } - - // We currently support none of the authenticator selection - // criteria because we can't ask tokens whether they do support - // those features. If flags are set, ignore all tokens for now. - // - // Technically, this is a ConstraintError because we shouldn't talk - // to this authenticator in the first place. But the result is the - // same anyway. - if !flags.is_empty() { + if !dev.is_u2f() { return; } - send_status( + StateMachine::register( + dev, + flags, + &challenge, + application.clone(), + &key_handles, &status_mutex, - crate::StatusUpdate::DeviceAvailable { - dev_info: dev.get_device_info(), - }, + &callback, + alive, ); + }); - // Iterate the exclude list and see if there are any matches. - // If so, we'll keep polling the device anyway to test for user - // consent, to be consistent with CTAP2 device behavior. - let excluded = key_handles.iter().any(|key_handle| { - is_valid_transport(key_handle.transports) - && apdu_is_keyhandle_valid( - dev, - &challenge, - &application, - &key_handle.credential, - ) + self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + } + + pub fn sign( + dev: &mut T, + flags: crate::SignFlags, + challenge: &[u8], + app_ids: &[crate::AppId], + key_handles: &[crate::KeyHandle], + status_mutex: &Mutex>, + callback: &StateCallback>, + alive: &dyn Fn() -> bool, + ) where + T: APDUDevice + U2FInfoQueryable, + { + // Try initializing it. + if dev.init_apdu().is_err() { + return; + } + + // We currently don't support user verification because we can't + // ask tokens whether they do support that. If the flag is set, + // ignore all tokens for now. + // + // Technically, this is a ConstraintError because we shouldn't talk + // to this authenticator in the first place. But the result is the + // same anyway. + if !flags.is_empty() { + return; + } + + // For each appId, try all key handles. If there's at least one + // valid key handle for an appId, we'll use that appId below. + let (app_id, valid_handles) = + find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| { + apdu_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential) .unwrap_or(false) /* no match on failure */ }); - while alive() { - if excluded { - let blank = vec![0u8; PARAMETER_SIZE]; - if apdu_register(dev, &blank, &blank).is_ok() { - callback.call(Err(errors::AuthenticatorError::U2FToken( - errors::U2FTokenError::InvalidState, + // Aggregate distinct transports from all given credentials. + let transports = key_handles + .iter() + .fold(crate::AuthenticatorTransports::empty(), |t, k| { + t | k.transports + }); + + // We currently only support USB. If the RP specifies transports + // and doesn't include USB it's probably lying. + if !is_valid_transport(transports) { + return; + } + + send_status( + &status_mutex, + crate::StatusUpdate::DeviceAvailable { + dev_info: dev.get_device_info(), + }, + ); + + 'outer: while alive() { + // If the device matches none of the given key handles + // then just make it blink with bogus data. + if valid_handles.is_empty() { + let blank = vec![0u8; PARAMETER_SIZE]; + if apdu_register(dev, &blank, &blank).is_ok() { + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::InvalidState, + ))); + break; + } + } else { + // Otherwise, try to sign. + for key_handle in &valid_handles { + if let Ok(bytes) = apdu_sign(dev, &challenge, app_id, &key_handle.credential) { + let dev_info = dev.get_device_info(); + send_status( + &status_mutex, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + callback.call(Ok(( + app_id.clone(), + key_handle.credential.clone(), + bytes, + dev_info, ))); - break; + break 'outer; } - } else if let Ok(bytes) = apdu_register(dev, &challenge, &application) { - let dev_info = dev.get_device_info(); - send_status( - &status_mutex, - crate::StatusUpdate::Success { - dev_info: dev.get_device_info(), - }, - ); - callback.call(Ok((bytes, dev_info))); - break; } - - // Sleep a bit before trying again. - thread::sleep(Duration::from_millis(100)); } - send_status( - &status_mutex, - crate::StatusUpdate::DeviceUnavailable { - dev_info: dev.get_device_info(), - }, - ); - }); + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } - self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + send_status( + &status_mutex, + crate::StatusUpdate::DeviceUnavailable { + dev_info: dev.get_device_info(), + }, + ); } - pub fn sign( + pub fn _sign( &mut self, flags: crate::SignFlags, timeout: u64, @@ -186,94 +310,19 @@ impl StateMachine { _ => return, }; - // Try initializing it. - if !dev.is_u2f() || dev.init_apdu().is_err() { + if !dev.is_u2f() { return; } - // We currently don't support user verification because we can't - // ask tokens whether they do support that. If the flag is set, - // ignore all tokens for now. - // - // Technically, this is a ConstraintError because we shouldn't talk - // to this authenticator in the first place. But the result is the - // same anyway. - if !flags.is_empty() { - return; - } - - // For each appId, try all key handles. If there's at least one - // valid key handle for an appId, we'll use that appId below. - let (app_id, valid_handles) = - find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| { - apdu_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential) - .unwrap_or(false) /* no match on failure */ - }); - - // Aggregate distinct transports from all given credentials. - let transports = key_handles - .iter() - .fold(crate::AuthenticatorTransports::empty(), |t, k| { - t | k.transports - }); - - // We currently only support USB. If the RP specifies transports - // and doesn't include USB it's probably lying. - if !is_valid_transport(transports) { - return; - } - - send_status( - &status_mutex, - crate::StatusUpdate::DeviceAvailable { - dev_info: dev.get_device_info(), - }, - ); - - 'outer: while alive() { - // If the device matches none of the given key handles - // then just make it blink with bogus data. - if valid_handles.is_empty() { - let blank = vec![0u8; PARAMETER_SIZE]; - if apdu_register(dev, &blank, &blank).is_ok() { - callback.call(Err(errors::AuthenticatorError::U2FToken( - errors::U2FTokenError::InvalidState, - ))); - break; - } - } else { - // Otherwise, try to sign. - for key_handle in &valid_handles { - if let Ok(bytes) = - apdu_sign(dev, &challenge, app_id, &key_handle.credential) - { - let dev_info = dev.get_device_info(); - send_status( - &status_mutex, - crate::StatusUpdate::Success { - dev_info: dev.get_device_info(), - }, - ); - callback.call(Ok(( - app_id.clone(), - key_handle.credential.clone(), - bytes, - dev_info, - ))); - break 'outer; - } - } - } - - // Sleep a bit before trying again. - thread::sleep(Duration::from_millis(100)); - } - - send_status( + StateMachine::sign( + dev, + flags, + &challenge, + &app_ids, + &key_handles, &status_mutex, - crate::StatusUpdate::DeviceUnavailable { - dev_info: dev.get_device_info(), - }, + &callback, + alive, ); }); From db71f283dd7ba97c5f410c2cdcb95c9a32b90fdb Mon Sep 17 00:00:00 2001 From: "L. K. Post" Date: Mon, 24 Aug 2020 00:59:34 +0200 Subject: [PATCH 6/7] Add NFC support --- Cargo.toml | 3 + examples/main.rs | 9 ++ src/authenticatorservice.rs | 8 + src/consts.rs | 9 ++ src/lib.rs | 7 + src/nfc.rs | 301 ++++++++++++++++++++++++++++++++++++ src/statemachine.rs | 4 +- 7 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 src/nfc.rs diff --git a/Cargo.toml b/Cargo.toml index f07d5927..ba160a8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ maintenance = { status = "actively-developed" } binding-recompile = ["bindgen"] webdriver = ["base64", "bytes", "warp", "tokio", "serde", "serde_json"] +nfc = ["pcsc"] + [target.'cfg(target_os = "linux")'.dependencies] libudev = "^0.2" @@ -51,6 +53,7 @@ serde = { version = "1.0", optional = true, features = ["derive"] } serde_json = { version = "1.0", optional = true } bytes = { version = "0.5", optional = true, features = ["serde"] } base64 = { version = "^0.10", optional = true } +pcsc = { version = "2", optional = true } [dev-dependencies] sha2 = "^0.8.2" diff --git a/examples/main.rs b/examples/main.rs index 3922a25d..8bee1f16 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -42,6 +42,8 @@ fn main() { opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms"); #[cfg(feature = "webdriver")] opts.optflag("w", "webdriver", "enable WebDriver virtual bus"); + #[cfg(feature = "nfc")] + opts.optflag("n", "no-nfc", "do not enable u2f-nfc platform"); opts.optflag("h", "help", "print this help menu").optopt( "t", @@ -74,6 +76,13 @@ fn main() { } } + #[cfg(feature = "nfc")] + { + if !matches.opt_present("no-nfc") { + manager.add_u2f_nfc_transports(); + } + } + let timeout_ms = match matches.opt_get_default::("timeout", 15) { Ok(timeout_s) => { println!("Using {}s as the timeout", &timeout_s); diff --git a/src/authenticatorservice.rs b/src/authenticatorservice.rs index fcb05dc6..bd818533 100644 --- a/src/authenticatorservice.rs +++ b/src/authenticatorservice.rs @@ -71,6 +71,9 @@ impl AuthenticatorService { /// Add any detected platform transports pub fn add_detected_transports(&mut self) { self.add_u2f_usb_hid_platform_transports(); + + #[cfg(feature = "nfc")] + self.add_u2f_nfc_transports(); } fn add_transport(&mut self, boxed_token: Box) { @@ -95,6 +98,11 @@ impl AuthenticatorService { } } + #[cfg(feature = "nfc")] + pub fn add_u2f_nfc_transports(&mut self) { + self.add_transport(Box::new(crate::NFCManager::new())); + } + pub fn register( &mut self, flags: crate::RegisterFlags, diff --git a/src/consts.rs b/src/consts.rs index 5f27bb93..48e6025d 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -78,3 +78,12 @@ pub const SW_NO_ERROR: [u8; 2] = [0x90, 0x00]; pub const SW_CONDITIONS_NOT_SATISFIED: [u8; 2] = [0x69, 0x85]; pub const SW_WRONG_DATA: [u8; 2] = [0x6A, 0x80]; pub const SW_WRONG_LENGTH: [u8; 2] = [0x67, 0x00]; + +// U2F-over-NFC constants +// TODO: naming, some are also ISO 7816-4 ?? +// The're mostly APDU-specific? +pub const U2F_AID: [u8; 8] = [0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01]; +pub const U2F_SELECT_FILE: u8 = 0xA4; +pub const U2F_SELECT_DIRECT: u8 = 0x04; +pub const U2F_GET_RESPONSE: u8 = 0xC0; +pub const U2F_MORE_DATA: u8 = 0x61; diff --git a/src/lib.rs b/src/lib.rs index 67783431..201ee5f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,13 @@ pub mod platform; #[path = "stub/mod.rs"] pub mod platform; +#[cfg(feature = "nfc")] +extern crate pcsc; +#[cfg(feature = "nfc")] +mod nfc; +#[cfg(feature = "nfc")] +pub use crate::nfc::NFCManager; + extern crate libc; #[macro_use] extern crate log; diff --git a/src/nfc.rs b/src/nfc.rs new file mode 100644 index 00000000..eae6ed1e --- /dev/null +++ b/src/nfc.rs @@ -0,0 +1,301 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use pcsc::*; +use runloop::RunLoop; +use std::collections::HashMap; +use std::ffi::CString; +use std::io; +use std::option::Option; +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use crate::apdu::*; +use crate::consts::*; +use crate::errors; +use crate::util::{io_err, trace_hex}; + +use crate::authenticatorservice::AuthenticatorTransport; +use crate::statecallback::StateCallback; +use crate::statemachine::StateMachine; +use crate::u2ftypes::{U2FDeviceInfo, U2FInfoQueryable}; + +fn sendrecv(card: &mut Card, send: &[u8]) -> io::Result> { + trace_hex("NFC send", send); + let mut rapdu_buf = [0; MAX_BUFFER_SIZE]; + match card.transmit(send, &mut rapdu_buf) { + Ok(rapdu) => { + trace_hex("NFC recv", rapdu); + Ok(rapdu.to_vec()) + } + Err(err) => { + trace!("NFC error: {}", err); + let s = format!("{}", err); + Err(io_err(&s)) + } + } +} + +impl APDUDevice for Card { + fn init_apdu(&mut self) -> io::Result<()> { + let out = APDU::serialize_short(U2F_SELECT_FILE, U2F_SELECT_DIRECT, &U2F_AID)?; + let ret = sendrecv(self, &out)?; + let (_, status) = APDU::deserialize(ret)?; + apdu_status_to_result(status, ()) + } + + fn send_apdu(&mut self, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec, [u8; 2])> { + // Some devices, such as the Yubikey 4, freak out if an APDU which _would_ fit a short + // command is sent as an extended command. This means we must use short, even though + // that means chaining the responses together. + // The whole response would have fit in an extended reply, but it seems we can't have nice + // things. + let mut data: Vec = Vec::new(); + + let out = APDU::serialize_short(cmd, p1, send)?; + let ret = sendrecv(self, &out)?; + let (mut more, [s1, s2]) = APDU::deserialize(ret)?; + data.append(&mut more); + + if s1 != U2F_MORE_DATA { + return Ok((data, [s1, s2])); + } + + loop { + let out = APDU::serialize_short(U2F_GET_RESPONSE, 0x00, &[])?; + let ret = sendrecv(self, &out)?; + let (mut more, [s1, s2]) = APDU::deserialize(ret)?; + data.append(&mut more); + if s1 != U2F_MORE_DATA { + return Ok((data, [s1, s2])); + } + } + } +} + +impl U2FInfoQueryable for Card { + fn get_device_info(&self) -> U2FDeviceInfo { + // TODO: actuall return something sane here! + let vendor = String::from("Unknown Vendor"); + let product = String::from("Unknown Device"); + + U2FDeviceInfo { + vendor_name: vendor.as_bytes().to_vec(), + device_name: product.as_bytes().to_vec(), + version_interface: 0, + version_major: 0, + version_minor: 0, + version_build: 0, + cap_flags: 0, + } + } +} + +#[derive(Default)] +pub struct NFCManager { + run_loop: Option, +} + +impl NFCManager { + fn run(&mut self, timeout: u64, fatal_error: E, f: F) -> crate::Result<()> + where + E: Fn() + Sync + Send + 'static, + F: Fn(&mut Card, &dyn Fn() -> bool) + Sync + Send + Clone + 'static, + { + if self.run_loop.is_some() { + return Err(errors::AuthenticatorError::InternalError(String::from( + "nfc run loop is already in use", + ))); + } + + let ctx = + Context::establish(Scope::User).map_err(|_| errors::AuthenticatorError::Platform)?; + + let rl = RunLoop::new_with_timeout( + move |alive| { + let mut child_loops: HashMap = HashMap::new(); + + let mut readers_buf = [0; 2048]; + // We _could_ insert `ReaderState::new(PNP_NOTIFICATION(), State::UNAWARE)` + // to be reminded of reader insertion/removal, + // but this is not guaranteed to be supported + // and we need to poll anyways. + let mut reader_states: Vec = Vec::new(); + + while alive() { + // Add new readers. + let names = match ctx.list_readers(&mut readers_buf) { + Ok(n) => n, + Err(_) => { + fatal_error(); + break; + } + }; + for name in names { + if !reader_states.iter().any(|reader| reader.name() == name) { + debug!("Adding reader {:?}", name); + reader_states.push(ReaderState::new(name, State::UNAWARE)); + } + } + + // Remove dead readers. + fn is_dead(reader: &ReaderState) -> bool { + reader + .event_state() + .intersects(State::UNKNOWN | State::IGNORE) + } + for reader in &reader_states { + if is_dead(reader) { + debug!("Removing reader {:?}", reader.name()); + } + } + reader_states.retain(|reader| !is_dead(reader)); + + // Let backend know that we know about the reader state. + // Otherwise it will keep trying to update us. + for rs in &mut reader_states { + rs.sync_current_state(); + } + + if reader_states.is_empty() { + // No readers available. This means that `get_status_change` will return + // immediately without any work, causing a busy-loop. + // Let's wait for a bit and look for a new reader. + thread::sleep(Duration::from_millis(500)); + continue; + } + + // This call is blocking, so we must give it _some_ timeout in order for + // the `alive()` check to work. + let timeout = Duration::from_millis(100); + if let Err(e) = ctx.get_status_change(timeout, &mut reader_states) { + if e == Error::Timeout { + continue; + } + fatal_error(); + break; + } + + for reader in &mut reader_states { + let state = reader.event_state(); + let name = CString::from(reader.name()); + + trace!("Reader {:?}: state {:?}", name, state); + + // TODO: this will keep spamming yubikeys with usb auth attempts??? + // probably not harmful, but is there a way to avoid it? + if state.contains(State::PRESENT) && !state.contains(State::EXCLUSIVE) { + let mut card = + match ctx.connect(&name, ShareMode::Shared, Protocols::ANY) { + Ok(card) => card, + _ => continue, + }; + + let my_f = f.clone(); + let cl = RunLoop::new(move |alive| { + if alive() { + my_f(&mut card, alive); + } + }); + + if let Ok(x) = cl { + child_loops.insert(name, x); + } + } else if let Some(cl) = child_loops.remove(&name) { + cl.cancel(); + } + } + } + + for (_, child) in child_loops { + child.cancel(); + } + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + self.run_loop = Some(rl); + Ok(()) + } + + pub fn new() -> Self { + Self { run_loop: None } + } + + fn stop(&mut self) { + if let Some(rl) = &self.run_loop { + rl.cancel(); + self.run_loop = None; + } + } +} + +impl Drop for NFCManager { + fn drop(&mut self) { + self.stop(); + } +} + +impl AuthenticatorTransport for NFCManager { + fn register( + &mut self, + flags: crate::RegisterFlags, + timeout: u64, + challenge: Vec, + application: crate::AppId, + key_handles: Vec, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()> { + let status_mutex = Arc::new(Mutex::new(status)); + let cbc = callback.clone(); + let err = move || cbc.call(Err(errors::AuthenticatorError::Platform)); + self.run(timeout, err, move |card, alive| { + StateMachine::register( + card, + flags, + &challenge, + application.clone(), + &key_handles, + &status_mutex, + &callback, + alive, + ); + }) + } + + fn sign( + &mut self, + flags: crate::SignFlags, + timeout: u64, + challenge: Vec, + app_ids: Vec, + key_handles: Vec, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()> { + let status_mutex = Arc::new(Mutex::new(status)); + let cbc = callback.clone(); + let err = move || cbc.call(Err(errors::AuthenticatorError::Platform)); + self.run(timeout, err, move |card, alive| { + StateMachine::sign( + card, + flags, + &challenge, + &app_ids, + &key_handles, + &status_mutex, + &callback, + alive, + ); + }) + } + + fn cancel(&mut self) -> crate::Result<()> { + self.stop(); + Ok(()) + } +} diff --git a/src/statemachine.rs b/src/statemachine.rs index 060a3056..0eaf4db3 100644 --- a/src/statemachine.rs +++ b/src/statemachine.rs @@ -16,7 +16,9 @@ use std::thread; use std::time::Duration; fn is_valid_transport(transports: crate::AuthenticatorTransports) -> bool { - transports.is_empty() || transports.contains(crate::AuthenticatorTransports::USB) + transports.is_empty() + || transports + .intersects(crate::AuthenticatorTransports::USB | crate::AuthenticatorTransports::NFC) } fn find_valid_key_handles<'a, F>( From 0de730f112e07908b481f38842b1c43682f6724d Mon Sep 17 00:00:00 2001 From: "L. K. Post" Date: Tue, 29 Sep 2020 00:54:32 +0200 Subject: [PATCH 7/7] Add CI pcsc support --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 70ea5c55..c6800d82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ addons: packages: - build-essential - libudev-dev + - libpcsclite-dev install: - rustup component add rustfmt