diff --git a/Cargo.lock b/Cargo.lock index 31546f0..edfd618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "apdu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa1a20ca6e9b354419bd6c2714beb435203b3e942440e09016e6deeffb08ffd" +dependencies = [ + "apdu-core", + "apdu-derive", + "thiserror 1.0.69", +] + +[[package]] +name = "apdu-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5ab921a56bbe68325ba6d3711ee2c681239fe4c9c295c6a1c2fe6992e27f86" + +[[package]] +name = "apdu-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd675f7ce10250005ac39b9ee8e618fe51370ce6f39170559726cdd0ff7fe7c" +dependencies = [ + "apdu-core", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "asn1-rs" version = "0.6.2" @@ -179,6 +208,29 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.95", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -242,7 +294,7 @@ dependencies = [ "log", "serde", "serde-xml-rs", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "uuid", ] @@ -276,7 +328,7 @@ dependencies = [ "objc2-foundation", "once_cell", "static_assertions", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-stream", "uuid", @@ -337,6 +389,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -390,6 +451,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -744,6 +816,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "ff" version = "0.13.0" @@ -760,6 +842,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "find-winsdk" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8cbf17b871570c1f8612b763bac3e86290602bcf5dc3c5ce657e0e1e9071d9e" +dependencies = [ + "serde", + "serde_derive", + "winreg", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -923,6 +1016,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "group" version = "0.13.0" @@ -1032,6 +1131,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1306,6 +1414,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.169" @@ -1321,11 +1435,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +dependencies = [ + "cfg-if 1.0.0", + "windows-targets 0.52.6", +] + [[package]] name = "libwebauthn" version = "0.1.2" dependencies = [ "aes", + "apdu", + "apdu-core", "async-trait", "base64-url", "bitflags 2.6.0", @@ -1344,10 +1470,13 @@ dependencies = [ "hmac", "maplit", "mockall", + "nfc1", + "nfc1-sys", "num-derive", "num-traits", "num_enum", "p256", + "pcsc", "qrcode", "rand", "serde", @@ -1360,6 +1489,7 @@ dependencies = [ "snow", "solo", "text_io", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -1372,6 +1502,12 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "litemap" version = "0.7.4" @@ -1468,6 +1604,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nfc1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d6dc2e4110af159c220d2d004661e380b6c40d93c5b04e839e4944f9d5291d" +dependencies = [ + "nfc1-sys", +] + +[[package]] +name = "nfc1-sys" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6652c6cdf52433ff143439595ffb4b945afafbe5f27cec8d2fc5dfb5832796e8" +dependencies = [ + "bindgen", + "cc", + "find-winsdk", + "pkg-config", + "vcpkg", +] + [[package]] name = "nom" version = "7.1.3" @@ -1705,6 +1863,31 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pcsc" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd833ecf8967e65934c49d3521a175929839bf6d0e497f3bd0d3a2ca08943da" +dependencies = [ + "bitflags 2.6.0", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ef017e15d2e5592a9e39a346c1dbaea5120bab7ed7106b210ef58ebd97003" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1836,6 +2019,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" +dependencies = [ + "proc-macro2", + "syn 2.0.95", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -2003,6 +2196,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2021,6 +2220,19 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.21.12" @@ -2436,11 +2648,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -2456,9 +2668,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -2787,6 +2999,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2809,6 +3027,18 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3050,6 +3280,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" +dependencies = [ + "serde", + "winapi", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index e36fc4b..d689a42 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -16,6 +16,13 @@ path = "src/lib.rs" default = [] hid-device-tests = ["virtual-hid-device"] virtual-hid-device = ["solo"] +nfc = ["apdu-core", "apdu", "thiserror"] +pcsc = [ "nfc", "dep:pcsc" ] +libnfc = [ + "nfc", + "nfc1-sys", + "nfc1", +] [dependencies] base64-url = "2.0.0" @@ -65,6 +72,12 @@ tokio-stream = "0.1.4" snow = { version = "0.10.0-alpha.1", features = ["use-p256"] } ctap-types = { version = "0.3.2" } btleplug = "0.11.7" +apdu-core = { version = "0.4.0", optional = true } +apdu = { version = "0.4.0", optional = true } +thiserror = { version = "2.0.12", optional = true } +pcsc = { version = "2.9.0", optional = true } +nfc1 = { version = "0.6.0", optional = true, default-features = false } +nfc1-sys = { version = "0.3.9", optional = true, default-features = false } [dev-dependencies] diff --git a/libwebauthn/src/transport/mod.rs b/libwebauthn/src/transport/mod.rs index 6b729a9..03108ec 100644 --- a/libwebauthn/src/transport/mod.rs +++ b/libwebauthn/src/transport/mod.rs @@ -4,6 +4,8 @@ pub mod ble; pub mod cable; pub mod device; pub mod hid; +#[cfg(feature = "nfc")] +pub mod nfc; mod channel; mod transport; diff --git a/libwebauthn/src/transport/nfc/channel.rs b/libwebauthn/src/transport/nfc/channel.rs new file mode 100644 index 0000000..e0114fa --- /dev/null +++ b/libwebauthn/src/transport/nfc/channel.rs @@ -0,0 +1,295 @@ +use apdu::core::HandleError; +use apdu::{Command, Response, command}; +use apdu_core; +use async_trait::async_trait; +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; +use std::time::Duration; +use tokio::sync::mpsc; +#[allow(unused_imports)] +use tracing::{Level, debug, instrument, trace, warn}; + +use crate::UxUpdate; +use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; +use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; +use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; +use crate::transport::device::SupportedProtocols; +use crate::transport::error::{Error, TransportError}; + +use super::commands::{command_ctap_msg, command_get_response}; + +const SELECT_P1: u8 = 0x04; +const SELECT_P2: u8 = 0x00; +const APDU_FIDO: &[u8; 8] = b"\xa0\x00\x00\x06\x47\x2f\x00\x01"; +const SW1_MORE_DATA: u8 = 0x61; + +#[derive(thiserror::Error)] +pub enum NfcError { + /// APDU error returned by the card. + Apdu(#[from] apdu::Error), + + /// Unexpected error occurred on the device. + Device(Box), +} + +impl Debug for NfcError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for NfcError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + NfcError::Apdu(e) => Display::fmt(e, f), + NfcError::Device(e) => e.fmt(f), + } + } +} + +impl From for Error { + fn from(input: NfcError) -> Self { + trace!("{:?}", input); + let output = match input { + NfcError::Apdu(_apdu_error) => TransportError::InvalidFraming, + NfcError::Device(_) => TransportError::ConnectionLost, + }; + Error::Transport(output) + } +} + +pub trait HandlerInCtx { + /// Handles the APDU command in a specific context. + /// Implementations must transmit the command to the card through a reader, + /// then receive the response from them, returning length of the data written. + fn handle_in_ctx(&mut self, ctx: Ctx, command: &[u8], response: &mut [u8]) + -> apdu_core::Result; +} + +pub trait NfcBackend: HandlerInCtx + Display {} + +pub struct NfcChannel +where + Ctx: Copy + Sync, +{ + delegate: Box + Send + Sync>, + auth_token_data: Option, + tx: mpsc::Sender, + ctx: Ctx, + apdu_response: Option, + cbor_response: Option, + supported: SupportedProtocols, + status: ChannelStatus, +} + +impl Display for NfcChannel +where + Ctx: Copy + Send + Sync, +{ + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "{}", self.delegate) + } +} + +impl NfcChannel +where + Ctx: fmt::Debug + Display + Copy + Send + Sync, +{ + pub fn new( + delegate: Box + Send + Sync>, + ctx: Ctx, + tx: mpsc::Sender, + ) -> Self { + NfcChannel { + delegate, + auth_token_data: None, + tx, + ctx, + apdu_response: None, + cbor_response: None, + supported: SupportedProtocols { + fido2: false, + u2f: false, + }, + status: ChannelStatus::Ready, + } + } + + #[instrument(skip_all)] + pub async fn wink(&mut self, _timeout: Duration) -> Result { + warn!("WINK capability is not supported"); + return Ok(false); + } + + pub fn select_fido2(&mut self) -> Result<(), Error> { + let command = command::select_file(SELECT_P1, SELECT_P2, APDU_FIDO); + let is_u2f_v2 = self.handle(self.ctx, command).map(|e| (e == b"U2F_V2"))?; + self.supported = SupportedProtocols { + u2f: is_u2f_v2, + // A CTAP authenticatorGetInfo should be issued to + // determine if the device supports CTAP2 or + // not. Assume it does for now. + fido2: true, + }; + + Ok(()) + } + + fn handle_in_ctx( + &mut self, + ctx: Ctx, + command_buf: &Vec, + buf: &mut [u8], + ) -> Result { + self.delegate + .handle_in_ctx(ctx, &command_buf, buf) + .map_err(|e| match e { + HandleError::NotEnoughBuffer(l) => { + NfcError::Device(Box::new(HandleError::NotEnoughBuffer(l))) + } + HandleError::Nfc(e) => NfcError::Device(e), + }) + } + + pub fn handle<'a>( + &'a mut self, + ctx: Ctx, + command: impl Into>, + ) -> Result, NfcError> { + let command = command.into(); + let command_buf = Vec::from(command); + + let mut buf = [0u8; 1024]; + let mut rapdu = Vec::new(); + + let len: usize = self.handle_in_ctx(ctx, &command_buf, &mut buf)? as usize; + let mut resp = Response::from(&buf[..len]); + + let (mut sw1, mut sw2) = resp.trailer; + rapdu.extend_from_slice(resp.payload); + + while sw1 == SW1_MORE_DATA { + let get_response_cmd = command_get_response(0x00, 0x00, sw2); + let get_response_buf = Vec::from(get_response_cmd); + let len = self.handle_in_ctx(ctx, &get_response_buf, &mut buf)?; + resp = Response::from(&buf[..len]); + (sw1, sw2) = resp.trailer; + rapdu.extend_from_slice(resp.payload); + } + + rapdu.extend_from_slice(&[sw1, sw2]); + Result::from(Response::from(rapdu.as_slice())) + .map(|p| p.to_vec()) + .map_err(|e| { + trace!("map_err {:?}", e); + apdu::Error::from(e).into() + }) + } +} + +#[async_trait] +impl<'a, Ctx> Channel for NfcChannel +where + Ctx: Copy + Send + Sync + fmt::Debug + Display, +{ + async fn supported_protocols(&self) -> Result { + Ok(self.supported) + } + + async fn status(&self) -> ChannelStatus { + self.status + } + + async fn close(&mut self) { + todo!("close") + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn apdu_send(&self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + todo!("apdu_send") + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn apdu_recv(&self, _timeout: Duration) -> Result { + todo!("apdu_recv") + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn cbor_send( + &mut self, + request: &CborRequest, + _timeout: std::time::Duration, + ) -> Result<(), Error> { + let data = &request.ctap_hid_data(); + let mut rest: &[u8] = data; + + while rest.len() > 250 { + let to_send = &rest[..250]; + rest = &rest[250..]; + let ctap_msg = command_ctap_msg(true, to_send); + let resp = self.handle(self.ctx, ctap_msg)?; + trace!("cbor_send has_more {:?} {:?}", to_send, resp); + } + + let ctap_msg = command_ctap_msg(false, rest); + let resp = self.handle(self.ctx, ctap_msg)?; + trace!("cbor_send {:?} {:?}", rest, resp); + + // FIXME check for SW_UPDATE? + + // let mut rapdu_buf = [0; pcsc::MAX_BUFFER_SIZE_EXTENDED]; + // let (mut resp, mut sw1, mut sw2) = self.card + // .chain_apdus(0x80, 0x10, 0x80, 0x00, data, &mut rapdu_buf) + // .expect("APDU exchange failed"); + + // loop { + // while (sw1, sw2) == SW_UPDATE { + // // ka_status = STATUS(resp[0]) + // // if on_keepalive and last_ka != ka_status: + // // last_ka = ka_status + // // on_keepalive(ka_status) + // // NFCCTAP_GETRESPONSE + + // (resp, sw1, sw2) = self.card + // .chain_apdus(0x80, 0x11, 0x00, 0x00, &[], &mut rapdu_buf).expect("APDU chained exchange failed"); + // debug!("Error {:?} {:?}", sw1, sw2); + // } + + // if (sw1, sw2) != SW_SUCCESS { + // return Err(Error::Transport(TransportError::InvalidFraming)); + // } + + let cbor_response = CborResponse::try_from(&resp) + .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + self.cbor_response = Some(cbor_response); + Ok(()) + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn cbor_recv(&mut self, _timeout: std::time::Duration) -> Result { + self.cbor_response + .take() + .ok_or(Error::Transport(TransportError::InvalidFraming)) + } + + fn get_state_sender(&self) -> &mpsc::Sender { + &self.tx + } +} + +impl Ctap2AuthTokenStore for NfcChannel +where + Ctx: Copy + Send + Sync, +{ + fn store_auth_data(&mut self, auth_token_data: AuthTokenData) { + self.auth_token_data = Some(auth_token_data); + } + + fn get_auth_data(&self) -> Option<&AuthTokenData> { + self.auth_token_data.as_ref() + } + + fn clear_uv_auth_token_store(&mut self) { + self.auth_token_data = None; + } +} diff --git a/libwebauthn/src/transport/nfc/commands.rs b/libwebauthn/src/transport/nfc/commands.rs new file mode 100644 index 0000000..f2c8f2f --- /dev/null +++ b/libwebauthn/src/transport/nfc/commands.rs @@ -0,0 +1,87 @@ +use apdu::Command; + +// Copy private impl +const CLA_DEFAULT: u8 = 0x00; +const CLA_INTER_INDUSTRY: u8 = 0x80; + +macro_rules! impl_into_vec { + ($name: ty) => { + impl<'a> From<$name> for Vec { + fn from(cmd: $name) -> Self { + Command::from(cmd).into() + } + } + }; +} + +const INS_GET_RESPONSE: u8 = 0xC0; + +/// `GET RESPONSE` (0xC0) command. +#[derive(Debug)] +pub struct GetResponseCommand { + p1: u8, + p2: u8, + le: u8, +} + +impl GetResponseCommand { + /// Constructs a `GET RESPONSE` command. + pub fn new(p1: u8, p2: u8, le: u8) -> Self { + Self { p1, p2, le } + } +} + +impl<'a> From for Command<'a> { + fn from(cmd: GetResponseCommand) -> Self { + Self::new_with_le(CLA_DEFAULT, INS_GET_RESPONSE, cmd.p1, cmd.p2, cmd.le.into()) + } +} + +impl_into_vec!(GetResponseCommand); + +/// Constructs a `GET RESPONSE` command. +pub fn command_get_response(p1: u8, p2: u8, le: u8) -> GetResponseCommand { + GetResponseCommand::new(p1, p2, le) +} + +const CLA_HAS_MORE: u8 = 0x10; +const INS_CTAP_MSG: u8 = 0x10; +const CTAP_P1_SUPP_GET_RESP: u8 = 0x80; +const CTAP_P2: u8 = 0x00; + +/// `CTAP MSG` (0x10) command. +#[derive(Debug)] +pub struct CtapMsgCommand<'a> { + has_more: bool, + payload: &'a [u8], +} + +impl<'a> CtapMsgCommand<'a> { + /// Constructs a `CTAP MSG` command. + pub fn new(has_more: bool, payload: &'a [u8]) -> Self { + Self { has_more, payload } + } +} + +impl<'a> From> for Command<'a> { + fn from(cmd: CtapMsgCommand<'a>) -> Self { + let cla = match cmd.has_more { + true => CLA_HAS_MORE, + false => 0, + } | CLA_INTER_INDUSTRY; + Self::new_with_payload( + cla, + INS_CTAP_MSG, + 0, //CTAP_P1_SUPP_GET_RESP, + CTAP_P2, + cmd.payload, + ) + } +} + +impl_into_vec!(CtapMsgCommand<'a>); + +/// Constructs a `GET MSG` command. +pub fn command_ctap_msg(has_more: bool, payload: &[u8]) -> CtapMsgCommand { + CtapMsgCommand::new(has_more, payload) +} diff --git a/libwebauthn/src/transport/nfc/device.rs b/libwebauthn/src/transport/nfc/device.rs new file mode 100644 index 0000000..36a5395 --- /dev/null +++ b/libwebauthn/src/transport/nfc/device.rs @@ -0,0 +1,125 @@ +use async_trait::async_trait; +use std::fmt; +use tokio::sync::mpsc; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +use crate::UxUpdate; +use crate::transport::device::Device; +use crate::transport::error::Error; + +use super::channel::NfcChannel; +#[cfg(feature = "libnfc")] +use super::libnfc; +#[cfg(feature = "pcsc")] +use super::pcsc; +use super::{Context, Nfc}; + +#[derive(Debug)] +enum DeviceInfo { + #[cfg(feature = "libnfc")] + LibNfc(libnfc::Info), + #[cfg(feature = "pcsc")] + Pcsc(pcsc::Info), +} + +#[derive(Debug)] +pub struct NfcDevice { + info: DeviceInfo, +} + +impl fmt::Display for DeviceInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + #[cfg(feature = "libnfc")] + DeviceInfo::LibNfc(info) => write!(f, "{}", info), + #[cfg(feature = "pcsc")] + DeviceInfo::Pcsc(info) => write!(f, "{}", info), + } + } +} + +impl fmt::Display for NfcDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.info) + } +} + +impl NfcDevice { + #[cfg(feature = "libnfc")] + pub fn new_libnfc(info: libnfc::Info) -> Self { + NfcDevice { + info: DeviceInfo::LibNfc(info), + } + } + + #[cfg(feature = "pcsc")] + pub fn new_pcsc(info: pcsc::Info) -> Self { + NfcDevice { + info: DeviceInfo::Pcsc(info), + } + } + + fn channel_sync<'d>( + &'d self, + ) -> Result<(NfcChannel, mpsc::Receiver), Error> { + trace!("nfc channel {:?}", self); + let (mut channel, recv): (NfcChannel, mpsc::Receiver) = match &self.info + { + #[cfg(feature = "libnfc")] + DeviceInfo::LibNfc(info) => info.channel(), + #[cfg(feature = "pcsc")] + DeviceInfo::Pcsc(info) => info.channel(), + }?; + + channel.select_fido2()?; + + Ok((channel, recv)) + } +} + +#[async_trait] +impl<'d> Device<'d, Nfc, NfcChannel> for NfcDevice { + async fn channel( + &'d mut self, + ) -> Result<(NfcChannel, mpsc::Receiver), Error> { + self.channel_sync() + } +} + +fn is_fido(device: &NfcDevice) -> bool +where + Ctx: fmt::Debug + fmt::Display + Copy + Send + Sync, +{ + fn inner(device: &NfcDevice) -> Result + where + Ctx: fmt::Debug + fmt::Display + Copy + Send + Sync, + { + let (mut chan, _send) = device.channel_sync()?; + let _ = chan.select_fido2()?; + Ok(true) + } + + inner::(device).is_ok() +} + +#[instrument] +pub async fn list_devices() -> Result, Error> { + let mut all_devices = Vec::new(); + let list_devices_fns = [ + #[cfg(feature = "libnfc")] + libnfc::list_devices, + #[cfg(feature = "pcsc")] + pcsc::list_devices, + ]; + + for list_devices in list_devices_fns { + let mut devices = list_devices()? + .into_iter() + .filter(|e| is_fido::(&e)) + .collect::>(); + all_devices.append(&mut devices); + } + + Ok(all_devices) +} diff --git a/libwebauthn/src/transport/nfc/libnfc/mod.rs b/libwebauthn/src/transport/nfc/libnfc/mod.rs new file mode 100644 index 0000000..6abeae5 --- /dev/null +++ b/libwebauthn/src/transport/nfc/libnfc/mod.rs @@ -0,0 +1,232 @@ +use super::Context; +use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; +use super::device::NfcDevice; +use crate::UxUpdate; +use crate::transport::error::{Error, TransportError}; +use apdu::core::HandleError; +use apdu_core; +use std::fmt; +use std::fmt::Debug; +use std::io::Write; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +use tokio::sync::mpsc; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +const MAX_DEVICES: usize = 10; +const TIMEOUT: Duration = Duration::from_millis(5000); +const MODULATION_TYPE: nfc1::ModulationType = nfc1::ModulationType::Iso14443a; + +#[derive(Debug)] +pub struct Info { + connstring: String, +} + +impl fmt::Display for Info { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.connstring) + } +} + +fn map_error(_err: nfc1::Error) -> Error { + Error::Transport(TransportError::ConnectionFailed) +} + +impl From for Error { + fn from(input: nfc1::Error) -> Self { + trace!("{:?}", input); + let output = match input { + // rs-nfc1 errors + nfc1::Error::Malloc => TransportError::TransportUnavailable, + nfc1::Error::Undefined(_c_int) => TransportError::TransportUnavailable, + nfc1::Error::UndefinedModulationType => TransportError::TransportUnavailable, + nfc1::Error::NoDeviceFound => TransportError::TransportUnavailable, + + // libnfc errors + nfc1::Error::Io => TransportError::ConnectionLost, + nfc1::Error::InvalidArgument => TransportError::NegotiationFailed, + nfc1::Error::DeviceNotSupported => TransportError::InvalidEndpoint, + nfc1::Error::NoSuchDeviceFound => TransportError::InvalidEndpoint, + nfc1::Error::BufferOverflow => TransportError::InvalidFraming, + nfc1::Error::Timeout => TransportError::Timeout, + nfc1::Error::OperationAborted => TransportError::InvalidFraming, + nfc1::Error::NotImplemented => TransportError::NegotiationFailed, + nfc1::Error::TargetReleased => TransportError::NegotiationFailed, + nfc1::Error::RfTransmissionError => TransportError::NegotiationFailed, + nfc1::Error::MifareAuthFailed => TransportError::NegotiationFailed, + nfc1::Error::Soft => TransportError::Timeout, + nfc1::Error::Chip => TransportError::InvalidFraming, + }; + Error::Transport(output) + } +} + +impl Info { + pub fn new(connstring: &String) -> Self { + Info { + connstring: connstring.clone(), + } + } + + pub fn channel(&self) -> Result<(NfcChannel, mpsc::Receiver), Error> { + let (send, recv) = mpsc::channel(1); + let context = nfc1::Context::new().map_err(|e| map_error(e))?; + + let mut chan = Channel::new(self, context); + + { + let mut device = chan.device.lock().unwrap(); + device.initiator_init()?; + device.set_property_bool(nfc1::Property::InfiniteSelect, false)?; + + let info = device.get_information_about()?; + debug!("Info: {}", info); + } + + let target = chan.connect_to_target()?; + debug!("Selected: {:?}", target); + + let ctx = Context {}; + let channel = NfcChannel::new(Box::new(chan), ctx, send); + Ok((channel, recv)) + } +} + +pub struct Channel { + device: Arc>, +} + +unsafe impl Send for Channel {} + +impl Channel { + pub fn new(info: &Info, mut context: nfc1::Context) -> Self { + let device = context + .open_with_connstring(&info.connstring) + .expect("opened device"); + + Self { + device: Arc::new(Mutex::new(device)), + } + } + + fn initiator_select_passive_target_ex( + device: &mut nfc1::Device, + modulation: &nfc1::Modulation, + ) -> nfc1::Result { + match device.initiator_select_passive_target(&modulation) { + Ok(target) => { + if let nfc1::target_info::TargetInfo::Iso14443a(iso) = target.target_info { + if iso.uid_len > 0 { + Ok(target) + } else { + Err(nfc1::Error::NoDeviceFound) + } + } else { + Err(nfc1::Error::NoDeviceFound) + } + } + Err(err) => { + println!("Error: {}", err); + Err(err) + } + } + } + + fn connect_to_target(&mut self) -> Result { + let mut device = self.device.lock().unwrap(); + // Assume baudrates are already sorted higher to lower + let baudrates = device.get_supported_baud_rate(nfc1::Mode::Initiator, MODULATION_TYPE)?; + let modulations = baudrates + .iter() + .map(|baud_rate| nfc1::Modulation { + modulation_type: MODULATION_TYPE, + baud_rate: *baud_rate, + }) + .collect::>(); + let modulation = &modulations[modulations.len() - 1]; + let is_one_rate = modulations.len() == 1; + for i in 0..2 { + if i > 0 { + thread::sleep(Duration::from_millis(100)); + } + trace!("Poll {:?} {}", modulation, i); + if let Ok(target) = + Channel::initiator_select_passive_target_ex(&mut device, &modulation) + { + if is_one_rate { + return Ok(target); + } + + for modulation in modulations.iter() { + device.initiator_deselect_target()?; + device.initiator_init()?; + trace!("Try {:?}", modulation); + if let Ok(target) = + Channel::initiator_select_passive_target_ex(&mut device, &modulation) + { + return Ok(target); + } + } + } + } + + Err(Error::Transport(TransportError::TransportUnavailable)) + } +} + +impl HandlerInCtx for Channel +where + Ctx: fmt::Debug + fmt::Display, +{ + fn handle_in_ctx( + &mut self, + _ctx: Ctx, + command: &[u8], + mut response: &mut [u8], + ) -> apdu_core::Result { + let timeout = nfc1::Timeout::Duration(TIMEOUT); + let len = response.len(); + trace!("TX: {:?}", command); + let rapdu = self + .device + .lock() + .unwrap() + .initiator_transceive_bytes(command, len, timeout) + .map_err(|e| HandleError::Nfc(Box::new(e)))?; + + trace!("RX: {:?}", rapdu); + + if response.len() < rapdu.len() { + return Err(HandleError::NotEnoughBuffer(rapdu.len())); + } + + response + .write(&rapdu) + .map_err(|e| HandleError::Nfc(Box::new(e))) + } +} + +impl fmt::Display for Channel { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + let mut device = self.device.lock().unwrap(); + write!(f, "{}", device.name()) + } +} + +impl NfcBackend for Channel where Ctx: fmt::Debug + fmt::Display {} + +#[instrument] +pub fn list_devices() -> Result, Error> { + let mut context = + nfc1::Context::new().map_err(|_| Error::Transport(TransportError::TransportUnavailable))?; + let devices = context + .list_devices(MAX_DEVICES) + .expect("libnfc devices") + .iter() + .map(|x| NfcDevice::new_libnfc(Info::new(x))) + .collect::>(); + + Ok(devices) +} diff --git a/libwebauthn/src/transport/nfc/mod.rs b/libwebauthn/src/transport/nfc/mod.rs new file mode 100644 index 0000000..cc94bb6 --- /dev/null +++ b/libwebauthn/src/transport/nfc/mod.rs @@ -0,0 +1,37 @@ +use std::fmt::{Display, Formatter}; + +pub mod channel; +pub mod commands; +pub mod device; +#[cfg(feature = "libnfc")] +pub mod libnfc; +#[cfg(feature = "pcsc")] +pub mod pcsc; + +pub use device::list_devices; + +use super::Transport; + +pub struct Nfc {} +impl Transport for Nfc {} +unsafe impl Send for Nfc {} +unsafe impl Sync for Nfc {} + +impl Display for Nfc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "NFC") + } +} + +#[derive(Clone, Debug)] +pub struct Context {} + +impl Display for Context { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "context") + } +} + +unsafe impl Send for Context {} +unsafe impl Sync for Context {} +impl Copy for Context {} diff --git a/libwebauthn/src/transport/nfc/pcsc/mod.rs b/libwebauthn/src/transport/nfc/pcsc/mod.rs new file mode 100644 index 0000000..6876ab1 --- /dev/null +++ b/libwebauthn/src/transport/nfc/pcsc/mod.rs @@ -0,0 +1,167 @@ +use super::Context; +use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; +use super::device::NfcDevice; +use crate::UxUpdate; +use crate::transport::error::{Error, TransportError}; +use apdu::core::HandleError; +use pcsc; +use std::ffi::{CStr, CString}; +use std::fmt; +use std::fmt::Debug; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +#[derive(Debug)] +pub struct Info { + name: CString, +} + +pub struct PcscCard { + pub card: Option, +} + +impl<'tx> Deref for PcscCard { + type Target = pcsc::Card; + + fn deref(&self) -> &pcsc::Card { + self.card.as_ref().unwrap() + } +} + +// By default pcsc resets the card but to be able to reconnect the +// card has to be powered down instead. +impl Drop for PcscCard { + fn drop(&mut self) { + let _ = PcscCard::disconnect(self.card.take()); + } +} + +impl PcscCard { + pub fn new(card: pcsc::Card) -> Self { + PcscCard { card: Some(card) } + } + + fn map_disconnect_error(pair: (pcsc::Card, pcsc::Error)) -> Error { + let (_card, _err) = pair; + Error::Transport(TransportError::InvalidFraming) + } + + fn disconnect(card: Option) -> Result<(), Error> { + match card { + Some(card) => { + debug!("Disconnect card"); + card.disconnect(pcsc::Disposition::UnpowerCard) + .map_err(PcscCard::map_disconnect_error) + } + None => Ok(()), + } + } +} + +pub struct Channel { + card: Arc>, +} + +unsafe impl Send for Channel {} + +impl fmt::Display for Info { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.name) + } +} + +impl From for Error { + fn from(input: pcsc::Error) -> Self { + trace!("{:?}", input); + let output = match input { + pcsc::Error::NoSmartcard => TransportError::ConnectionFailed, + _ => TransportError::InvalidFraming, + }; + + Error::Transport(output) + } +} + +impl Info { + pub fn new(name: &CStr) -> Self { + Info { + name: CStr::into_c_string(name.into()), + } + } + + pub fn channel(&self) -> Result<(NfcChannel, mpsc::Receiver), Error> { + let (send, recv) = mpsc::channel(1); + let context = pcsc::Context::establish(pcsc::Scope::User)?; + let chan = Channel::new(self, context)?; + + let ctx = Context {}; + let channel = NfcChannel::new(Box::new(chan), ctx, send); + Ok((channel, recv)) + } +} + +impl Channel { + pub fn new(info: &Info, context: pcsc::Context) -> Result { + let card = context.connect(&info.name, pcsc::ShareMode::Shared, pcsc::Protocols::ANY)?; + + let chan = Self { + card: Arc::new(Mutex::new(PcscCard::new(card))), + }; + + Ok(chan) + } +} + +impl fmt::Display for Channel { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + let card = self.card.lock().unwrap(); + let (names_len, atr_len) = card.status2_len().unwrap(); + let mut names_buf = vec![0; names_len]; + let mut atr_buf = vec![0; atr_len]; + let status = card.status2(&mut names_buf, &mut atr_buf).unwrap(); + write!(f, "{:?}", status.reader_names().collect::>()) + } +} + +impl NfcBackend for Channel where Ctx: fmt::Debug + fmt::Display {} + +impl HandlerInCtx for Channel +where + Ctx: fmt::Debug + fmt::Display, +{ + fn handle_in_ctx( + &mut self, + _ctx: Ctx, + command: &[u8], + response: &mut [u8], + ) -> apdu_core::Result { + trace!("TX: {:?}", command); + + let rapdu = self + .card + .lock() + .unwrap() + .transmit(command, response) + .map_err(|e| HandleError::Nfc(Box::new(e)))?; + + trace!("RX: {:?}", rapdu); + Ok(rapdu.len()) + } +} + +#[instrument] +pub fn list_devices() -> Result, Error> { + let ctx = pcsc::Context::establish(pcsc::Scope::User).expect("PC/SC context"); + let len = ctx.list_readers_len().expect("PC/SC readers len"); + let mut readers_buf = vec![0; len]; + let devices = ctx + .list_readers(&mut readers_buf) + .expect("PC/SC readers") + .map(|x| NfcDevice::new_pcsc(Info::new(x))) + .collect::>(); + + Ok(devices) +}