Skip to content

Commit 84d9628

Browse files
committed
Add NFC support
1 parent 24f909e commit 84d9628

File tree

7 files changed

+314
-1
lines changed

7 files changed

+314
-1
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ maintenance = { status = "actively-developed" }
1717
binding-recompile = ["bindgen"]
1818
webdriver = ["base64", "bytes", "warp", "tokio", "serde", "serde_json"]
1919

20+
nfc = ["pcsc"]
21+
2022
[target.'cfg(target_os = "linux")'.dependencies]
2123
libudev = "^0.2"
2224

@@ -51,6 +53,7 @@ serde = { version = "1.0", optional = true, features = ["derive"] }
5153
serde_json = { version = "1.0", optional = true }
5254
bytes = { version = "0.5", optional = true, features = ["serde"] }
5355
base64 = { version = "^0.10", optional = true }
56+
pcsc = { version = "2", optional = true }
5457

5558
[dev-dependencies]
5659
sha2 = "^0.8.2"

examples/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ fn main() {
4242
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
4343
#[cfg(feature = "webdriver")]
4444
opts.optflag("w", "webdriver", "enable WebDriver virtual bus");
45+
#[cfg(feature = "nfc")]
46+
opts.optflag("n", "no-nfc", "do not enable u2f-nfc platform");
4547

4648
opts.optflag("h", "help", "print this help menu").optopt(
4749
"t",
@@ -74,6 +76,13 @@ fn main() {
7476
}
7577
}
7678

79+
#[cfg(feature = "nfc")]
80+
{
81+
if !matches.opt_present("no-nfc") {
82+
manager.add_u2f_nfc_transports();
83+
}
84+
}
85+
7786
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 15) {
7887
Ok(timeout_s) => {
7988
println!("Using {}s as the timeout", &timeout_s);

src/authenticatorservice.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ impl AuthenticatorService {
7171
/// Add any detected platform transports
7272
pub fn add_detected_transports(&mut self) {
7373
self.add_u2f_usb_hid_platform_transports();
74+
75+
#[cfg(feature = "nfc")]
76+
self.add_u2f_nfc_transports();
7477
}
7578

7679
fn add_transport(&mut self, boxed_token: Box<dyn AuthenticatorTransport + Send>) {
@@ -95,6 +98,11 @@ impl AuthenticatorService {
9598
}
9699
}
97100

101+
#[cfg(feature = "nfc")]
102+
pub fn add_u2f_nfc_transports(&mut self) {
103+
self.add_transport(Box::new(crate::NFCManager::new()));
104+
}
105+
98106
pub fn register(
99107
&mut self,
100108
flags: crate::RegisterFlags,

src/consts.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,12 @@ pub const SW_NO_ERROR: [u8; 2] = [0x90, 0x00];
7878
pub const SW_CONDITIONS_NOT_SATISFIED: [u8; 2] = [0x69, 0x85];
7979
pub const SW_WRONG_DATA: [u8; 2] = [0x6A, 0x80];
8080
pub const SW_WRONG_LENGTH: [u8; 2] = [0x67, 0x00];
81+
82+
// U2F-over-NFC constants
83+
// TODO: naming, some are also ISO 7816-4 ??
84+
// The're mostly APDU-specific?
85+
pub const U2F_AID: [u8; 8] = [0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01];
86+
pub const U2F_SELECT_FILE: u8 = 0xA4;
87+
pub const U2F_SELECT_DIRECT: u8 = 0x04;
88+
pub const U2F_GET_RESPONSE: u8 = 0xC0;
89+
pub const U2F_MORE_DATA: u8 = 0x61;

src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ pub mod platform;
5252
#[path = "stub/mod.rs"]
5353
pub mod platform;
5454

55+
#[cfg(feature = "nfc")]
56+
extern crate pcsc;
57+
#[cfg(feature = "nfc")]
58+
mod nfc;
59+
#[cfg(feature = "nfc")]
60+
pub use crate::nfc::NFCManager;
61+
5562
extern crate libc;
5663
#[macro_use]
5764
extern crate log;

src/nfc.rs

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use std::io;
6+
use std::sync::mpsc::Sender;
7+
use pcsc::*;
8+
use std::ffi::CString;
9+
use runloop::RunLoop;
10+
use std::collections::HashMap;
11+
use std::option::Option;
12+
use std::thread;
13+
use std::time::Duration;
14+
use std::sync::{Mutex, Arc};
15+
16+
use crate::consts::*;
17+
use crate::apdu::*;
18+
use crate::util::{io_err, trace_hex};
19+
use crate::errors;
20+
21+
use crate::authenticatorservice::AuthenticatorTransport;
22+
use crate::statecallback::StateCallback;
23+
use crate::statemachine::StateMachine;
24+
use crate::u2ftypes::{U2FInfoQueryable, U2FDeviceInfo};
25+
26+
fn sendrecv(card: &mut Card, send: &[u8]) -> io::Result<Vec<u8>>
27+
{
28+
trace_hex("NFC send", send);
29+
let mut rapdu_buf = [0; MAX_BUFFER_SIZE];
30+
match card.transmit(send, &mut rapdu_buf) {
31+
Ok(rapdu) => {
32+
trace_hex("NFC recv", rapdu);
33+
Ok(rapdu.to_vec())
34+
},
35+
Err(err) => {
36+
trace!("NFC error: {}", err);
37+
let s = format!("{}", err);
38+
Err(io_err(&s))
39+
}
40+
}
41+
}
42+
43+
impl APDUDevice for Card {
44+
fn init_apdu(&mut self) -> io::Result<()> {
45+
let out = APDU::serialize_short(U2F_SELECT_FILE, U2F_SELECT_DIRECT, &U2F_AID)?;
46+
let ret = sendrecv(self, &out)?;
47+
let (_, status) = APDU::deserialize(ret)?;
48+
apdu_status_to_result(status, ())
49+
}
50+
51+
fn send_apdu(&mut self, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec<u8>, [u8; 2])> {
52+
// Some devices, such as the Yubikey 4, freak out if an APDU which _would_ fit a short command
53+
// is sent as an extended command. This means we must use short, even though that means chaining
54+
// the responses together.
55+
// The whole response would have fit in an extended reply, but it seems we can't have nice things.
56+
let mut data: Vec<u8> = Vec::new();
57+
58+
let out = APDU::serialize_short(cmd, p1, send)?;
59+
let ret = sendrecv(self, &out)?;
60+
let (mut more, [s1, s2]) = APDU::deserialize(ret)?;
61+
data.append(&mut more);
62+
63+
if s1 != U2F_MORE_DATA {
64+
return Ok((data, [s1, s2]));
65+
}
66+
67+
loop {
68+
let out = APDU::serialize_short(U2F_GET_RESPONSE, 0x00, &[])?;
69+
let ret = sendrecv(self, &out)?;
70+
let (mut more, [s1, s2]) = APDU::deserialize(ret)?;
71+
data.append(&mut more);
72+
if s1 != U2F_MORE_DATA {
73+
return Ok((data, [s1, s2]));
74+
}
75+
}
76+
}
77+
}
78+
79+
impl U2FInfoQueryable for Card {
80+
fn get_device_info(&self) -> U2FDeviceInfo {
81+
// TODO: actuall return something sane here!
82+
let vendor = String::from("Unknown Vendor");
83+
let product = String::from("Unknown Device");
84+
85+
U2FDeviceInfo {
86+
vendor_name: vendor.as_bytes().to_vec(),
87+
device_name: product.as_bytes().to_vec(),
88+
version_interface: 0,
89+
version_major: 0,
90+
version_minor: 0,
91+
version_build: 0,
92+
cap_flags: 0,
93+
}
94+
}
95+
}
96+
97+
pub struct NFCManager {
98+
run_loop: Option<RunLoop>,
99+
}
100+
101+
impl NFCManager {
102+
fn run<E, F>(&mut self, timeout: u64, fatal_error: E, f: F) -> crate::Result<()>
103+
where
104+
E: Fn() + Sync + Send + 'static,
105+
F: Fn(&mut Card, &dyn Fn() -> bool) + Sync + Send + Clone + 'static,
106+
{
107+
if self.run_loop.is_some() {
108+
return Err(errors::AuthenticatorError::InternalError(String::from("nfc run loop is already in use")));
109+
}
110+
111+
let ctx = Context::establish(Scope::User)
112+
.map_err(|_| errors::AuthenticatorError::Platform)?;
113+
114+
let rl = RunLoop::new_with_timeout(move |alive| {
115+
let mut child_loops: HashMap<CString, RunLoop> = HashMap::new();
116+
117+
let mut readers_buf = [0; 2048];
118+
// We _could_ insert `ReaderState::new(PNP_NOTIFICATION(), State::UNAWARE)`
119+
// to be reminded of reader insertion/removal,
120+
// but this is not guaranteed to be supported
121+
// and we need to poll anyways.
122+
let mut reader_states: Vec<ReaderState> = Vec::new();
123+
124+
while alive() {
125+
// Add new readers.
126+
let names = match ctx.list_readers(&mut readers_buf) {
127+
Ok(n) => n,
128+
Err(_) => {
129+
fatal_error();
130+
break; },
131+
};
132+
for name in names {
133+
if !reader_states.iter().any(|reader| reader.name() == name) {
134+
debug!("Adding reader {:?}", name);
135+
reader_states.push(ReaderState::new(name, State::UNAWARE));
136+
}
137+
}
138+
139+
// Remove dead readers.
140+
fn is_dead(reader: &ReaderState) -> bool {
141+
reader.event_state().intersects(State::UNKNOWN | State::IGNORE)
142+
}
143+
for reader in &reader_states {
144+
if is_dead(reader) {
145+
debug!("Removing reader {:?}", reader.name());
146+
}
147+
}
148+
reader_states.retain(|reader| !is_dead(reader));
149+
150+
// Let backend know that we know about the reader state.
151+
// Otherwise it will keep trying to update us.
152+
for rs in &mut reader_states {
153+
rs.sync_current_state();
154+
}
155+
156+
if reader_states.len() == 0 {
157+
// No readers available. This means that `get_status_change` will return
158+
// immediately without any work, causing a busy-loop.
159+
// Let's wait for a bit and look for a new reader.
160+
thread::sleep(Duration::from_millis(500));
161+
continue;
162+
}
163+
164+
// This call is blocking, so we must give it _some_ timeout in order for
165+
// the `alive()` check to work.
166+
let timeout = Duration::from_millis(100);
167+
if let Err(e) = ctx.get_status_change(timeout, &mut reader_states) {
168+
if e == Error::Timeout {
169+
continue;
170+
}
171+
fatal_error();
172+
break;
173+
}
174+
175+
for reader in &mut reader_states {
176+
let state = reader.event_state();
177+
let name = CString::from(reader.name());
178+
179+
trace!("Reader {:?}: state {:?}", name, state);
180+
181+
// TODO: this will keep spamming yubikeys with usb auth attempts???
182+
// probably not harmful, but is there a way to avoid it?
183+
if state.contains(State::PRESENT) && !state.contains(State::EXCLUSIVE) {
184+
let mut card = match ctx.connect(&name, ShareMode::Shared, Protocols::ANY) {
185+
Ok(card) => card,
186+
_ => continue,
187+
};
188+
189+
let my_f = f.clone();
190+
let cl = RunLoop::new(move |alive| {
191+
if alive() {
192+
my_f(&mut card, alive);
193+
}
194+
});
195+
196+
if let Ok(x) = cl {
197+
child_loops.insert(name, x);
198+
}
199+
} else {
200+
if let Some(cl) = child_loops.remove(&name) {
201+
cl.cancel();
202+
}
203+
}
204+
}
205+
}
206+
207+
for (_, child) in child_loops {
208+
child.cancel();
209+
}
210+
}, timeout)
211+
.map_err(|_| errors::AuthenticatorError::Platform)?;
212+
self.run_loop = Some(rl);
213+
Ok(())
214+
}
215+
216+
pub fn new() -> Self {
217+
Self {
218+
run_loop: None,
219+
}
220+
}
221+
222+
fn stop(&mut self) {
223+
if let Some(rl) = &self.run_loop {
224+
rl.cancel();
225+
self.run_loop = None;
226+
}
227+
}
228+
}
229+
230+
impl Drop for NFCManager {
231+
fn drop(&mut self) {
232+
self.stop();
233+
}
234+
}
235+
236+
impl AuthenticatorTransport for NFCManager {
237+
fn register(
238+
&mut self,
239+
flags: crate::RegisterFlags,
240+
timeout: u64,
241+
challenge: Vec<u8>,
242+
application: crate::AppId,
243+
key_handles: Vec<crate::KeyHandle>,
244+
status: Sender<crate::StatusUpdate>,
245+
callback: StateCallback<crate::Result<crate::RegisterResult>>,
246+
) -> crate::Result<()> {
247+
let status_mutex = Arc::new(Mutex::new(status));
248+
let cbc = callback.clone();
249+
let err = move || { cbc.call(Err(errors::AuthenticatorError::Platform)) };
250+
self.run(timeout, err, move |card, alive| {
251+
StateMachine::register(card, flags, &challenge, &application, &key_handles, &status_mutex, &callback, alive);
252+
})
253+
}
254+
255+
fn sign(
256+
&mut self,
257+
flags: crate::SignFlags,
258+
timeout: u64,
259+
challenge: Vec<u8>,
260+
app_ids: Vec<crate::AppId>,
261+
key_handles: Vec<crate::KeyHandle>,
262+
status: Sender<crate::StatusUpdate>,
263+
callback: StateCallback<crate::Result<crate::SignResult>>,
264+
) -> crate::Result<()> {
265+
let status_mutex = Arc::new(Mutex::new(status));
266+
let cbc = callback.clone();
267+
let err = move || { cbc.call(Err(errors::AuthenticatorError::Platform)) };
268+
self.run(timeout, err, move |card, alive| {
269+
StateMachine::sign(card, flags, &challenge, &app_ids, &key_handles, &status_mutex, &callback, alive);
270+
})
271+
}
272+
273+
fn cancel(&mut self) -> crate::Result<()> {
274+
self.stop();
275+
Ok(())
276+
}
277+
}

src/statemachine.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::thread;
1616
use std::time::Duration;
1717

1818
fn is_valid_transport(transports: crate::AuthenticatorTransports) -> bool {
19-
transports.is_empty() || transports.contains(crate::AuthenticatorTransports::USB)
19+
transports.is_empty() || transports.intersects(crate::AuthenticatorTransports::USB | crate::AuthenticatorTransports::NFC)
2020
}
2121

2222
fn find_valid_key_handles<'a, F>(

0 commit comments

Comments
 (0)