Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add basic NFC support #114

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ addons:
packages:
- build-essential
- libudev-dev
- libpcsclite-dev

install:
- rustup component add rustfmt
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions examples/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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::<u64>("timeout", 15) {
Ok(timeout_s) => {
println!("Using {}s as the timeout", &timeout_s);
Expand Down
12 changes: 7 additions & 5 deletions fuzz/fuzz_targets/u2f_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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]| {
Expand Down
12 changes: 7 additions & 5 deletions fuzz/fuzz_targets/u2f_read_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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]| {
Expand Down
200 changes: 200 additions & 0 deletions src/apdu.rs
Original file line number Diff line number Diff line change
@@ -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>, [u8; 2])>;
}

////////////////////////////////////////////////////////////////////////
// Device Commands
////////////////////////////////////////////////////////////////////////

pub fn apdu_register<T>(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result<Vec<u8>>
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, &register_data)?;
apdu_status_to_result(status, resp)
}

pub fn apdu_sign<T>(
dev: &mut T,
challenge: &[u8],
application: &[u8],
key_handle: &[u8],
) -> io::Result<Vec<u8>>
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)?;
apdu_status_to_result(status, resp)
}

pub fn apdu_is_keyhandle_valid<T>(
dev: &mut T,
challenge: &[u8],
application: &[u8],
key_handle: &[u8],
) -> io::Result<bool>
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 apdu_is_v2_device<T>(dev: &mut T) -> io::Result<bool>
where
T: APDUDevice,
{
let (data, status) = dev.send_apdu(U2F_VERSION, 0x00, &[])?;
let actual = CString::new(data)?;
let expected = CString::new("U2F_V2")?;
apdu_status_to_result(status, actual == expected)
}

////////////////////////////////////////////////////////////////////////
// Error Handling
////////////////////////////////////////////////////////////////////////

pub fn apdu_status_to_result<T>(status: [u8; 2], val: T) -> io::Result<T> {
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<Vec<u8>> {
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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe construct this the same as you do size down in serialize_short?

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;
Copy link
Contributor

@micolous micolous Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be skipped when data.len() = 0.

Also, Lc should be entirely skipped when there is no data, not even a prefixed zero byte.

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);
Copy link
Contributor

@micolous micolous Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong - if Nc is zero (no command data length), then it should be omitted.

In extended mode:

  • If Ne is non-zero, then the last three (if Nc=0) or two (if Nc>0) bytes are the Ne (which haven't been included here).

  • If Ne is zero, then there should be no trailing bytes.

At the moment I think you're trying to go for maximum length (65536 bytes), which should have a message ending in 00 00 00.

(Edit: this previously incorrectly stated that Ne is always 3 bytes in extended mode, when it can sometimes be 2... also the U2F spec confusingly is "based" on ISO7816-4 but then is actually incompatible... except some revisions of the spec (not the newest) actually correct this 😢 )

}

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<Vec<u8>> {
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be 4: the response length bytes (Le) are omitted when the desired response length (Ne) is zero.

if !data.is_empty() {
size += 1 + data.len(); // data size field and data itself
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a bit more description, because it appears that the data-size field is bytes[4], accounted for in the initial value of 5. This is actually because of the overflow zeroes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already correct: when there is no command data payload (Ne=0), you omit the command length bytes (Lc).

}
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be skipped when data.len() == 0


bytes[5..5 + data.len()].copy_from_slice(data);

Ok(bytes)
}

pub fn deserialize(mut data: Vec<u8>) -> io::Result<(Vec<u8>, [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]]))
}
}
8 changes: 8 additions & 0 deletions src/authenticatorservice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn AuthenticatorTransport + Send>) {
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
12 changes: 7 additions & 5 deletions src/freebsd/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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);
}
}
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -61,6 +68,7 @@ extern crate runloop;
#[macro_use]
extern crate bitflags;

mod apdu;
pub mod authenticatorservice;
mod consts;
mod statemachine;
Expand Down
Loading