Skip to content

Commit 15371f7

Browse files
committed
Add NFC support
1 parent a0025ce commit 15371f7

File tree

7 files changed

+313
-3
lines changed

7 files changed

+313
-3
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ maintenance = { status = "actively-developed" }
1616
[features]
1717
binding-recompile = ["bindgen"]
1818

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

@@ -44,6 +46,7 @@ log = "0.4"
4446
libc = "0.2"
4547
runloop = "0.1.0"
4648
bitflags = "1.0"
49+
pcsc = { version = "2", optional = true }
4750

4851
[dev-dependencies]
4952
sha2 = "^0.8.2"

examples/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ fn main() {
3939
let program = args[0].clone();
4040

4141
let mut opts = Options::new();
42-
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
42+
opts.optflag("u", "no-usb", "do not enable u2f-usb-hid platforms");
43+
#[cfg(feature = "nfc")]
44+
opts.optflag("n", "no-nfc", "do not enable u2f-nfc platform");
4345

4446
opts.optflag("h", "help", "print this help menu");
4547
let matches = match opts.parse(&args[1..]) {
@@ -54,10 +56,15 @@ fn main() {
5456
let mut manager =
5557
AuthenticatorService::new().expect("The auth service should initialize safely");
5658

57-
if !matches.opt_present("no-u2f-usb-hid") {
59+
if !matches.opt_present("no-usb") {
5860
manager.add_u2f_usb_hid_platform_transports();
5961
}
6062

63+
#[cfg(feature = "nfc")]
64+
if !matches.opt_present("no-nfc") {
65+
manager.add_u2f_nfc_transports();
66+
}
67+
6168
println!("Asking a security key to register now...");
6269
let challenge_str = format!(
6370
"{}{}",

src/authenticatorservice.rs

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

7578
fn add_transport(&mut self, boxed_token: Box<dyn AuthenticatorTransport + Send>) {
@@ -83,6 +86,11 @@ impl AuthenticatorService {
8386
}
8487
}
8588

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

src/statemachine.rs

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

1717
fn is_valid_transport(transports: crate::AuthenticatorTransports) -> bool {
18-
transports.is_empty() || transports.contains(crate::AuthenticatorTransports::USB)
18+
transports.is_empty() || transports.intersects(crate::AuthenticatorTransports::USB | crate::AuthenticatorTransports::NFC)
1919
}
2020

2121
fn find_valid_key_handles<'a, F>(

0 commit comments

Comments
 (0)