diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2475df1..4dea18b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -23,3 +23,7 @@ jobs: run: cargo build - name: Run tests run: cargo test --verbose + - name: Install nfc dependencies + run: sudo apt-get install libnfc-dev libpcsclite-dev + - name: Build with nfc-support + run: cargo build --features libnfc,pcsc diff --git a/libwebauthn/Cargo.lock b/libwebauthn/Cargo.lock index 5b56423..111e557 100644 --- a/libwebauthn/Cargo.lock +++ b/libwebauthn/Cargo.lock @@ -112,6 +112,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[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-app" version = "0.1.0" @@ -121,6 +132,24 @@ dependencies = [ "iso7816", ] +[[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.7.1" @@ -188,9 +217,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" dependencies = [ "aws-lc-sys", "zeroize", @@ -198,11 +227,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" dependencies = [ - "bindgen 0.69.5", + "bindgen 0.72.1", "cc", "cmake", "dunce", @@ -253,22 +282,22 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bindgen" -version = "0.69.5" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ - "bitflags 2.9.2", + "bitflags 1.3.2", "cexpr", "clang-sys", - "itertools 0.12.1", "lazy_static", "lazycell", "log", + "peeking_take_while", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.106", "which", @@ -280,14 +309,34 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.106", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.9.4", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", "shlex", "syn 2.0.106", ] @@ -300,9 +349,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "blake2" @@ -355,7 +404,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "bluez-generated", "dbus", "dbus-tokio", @@ -385,7 +434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" dependencies = [ "async-trait", - "bitflags 2.9.2", + "bitflags 2.9.4", "bluez-async", "dashmap 6.1.0", "dbus", @@ -459,10 +508,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -805,15 +855,15 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbus" -version = "0.9.7" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" dependencies = [ "futures-channel", "futures-util", "libc", "libdbus-sys", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -885,9 +935,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -1053,12 +1103,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1117,6 +1167,23 @@ dependencies = [ "trussed-hkdf", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[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 = "flexiber" version = "0.1.3" @@ -1291,7 +1358,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -1365,9 +1432,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heapless" @@ -1475,23 +1542,24 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", + "moxcms", "num-traits", ] [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", ] [[package]] @@ -1515,11 +1583,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -1539,15 +1607,6 @@ dependencies = [ "heapless", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1609,9 +1668,9 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -1619,9 +1678,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -1647,9 +1706,9 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libdbus-sys" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" dependencies = [ "pkg-config", ] @@ -1669,9 +1728,11 @@ name = "libwebauthn" version = "0.2.2" dependencies = [ "aes", + "apdu", + "apdu-core", "async-trait", "base64-url", - "bitflags 2.9.2", + "bitflags 2.9.4", "btleplug", "byteorder", "cbc", @@ -1693,10 +1754,13 @@ dependencies = [ "littlefs2", "maplit", "mockall", + "nfc1", + "nfc1-sys", "num-derive", "num-traits", "num_enum", "p256 0.13.2", + "pcsc", "qrcode", "rand 0.8.5", "rustls", @@ -1733,9 +1797,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "littlefs2" @@ -1756,7 +1820,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a81a4745d38b288b7583fe8ea3736897628df81f4d0f1d0314fa5a3af570de4" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "heapless-bytes", "serde", ] @@ -1783,9 +1847,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" @@ -1808,11 +1872,11 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1873,12 +1937,44 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "nb" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[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 0.65.1", + "cc", + "find-winsdk", + "pkg-config", + "vcpkg", +] + [[package]] name = "nom" version = "7.1.3" @@ -1891,12 +1987,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1988,7 +2083,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "objc2", "objc2-foundation", ] @@ -2005,7 +2100,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "block2", "libc", "objc2", @@ -2053,12 +2148,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.9.0" @@ -2132,6 +2221,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.9.4", + "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" @@ -2272,9 +2386,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -2288,6 +2402,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84" +dependencies = [ + "num-traits", +] + [[package]] name = "qrcode" version = "0.14.1" @@ -2377,7 +2500,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", ] [[package]] @@ -2388,47 +2511,32 @@ checksum = "09c30c54dffee5b40af088d5d50aa3455c91a0127164b51f0215efc4cb28fb3c" [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rfc6979" @@ -2466,6 +2574,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2490,7 +2604,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2499,15 +2613,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.0", ] [[package]] @@ -2549,9 +2663,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "aws-lc-rs", "ring", @@ -2588,11 +2702,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -2624,11 +2738,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -2637,9 +2751,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -2647,16 +2761,17 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" dependencies = [ + "serde_core", "serde_derive", ] @@ -2705,11 +2820,12 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -2722,11 +2838,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -2974,15 +3099,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -3070,9 +3195,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3085,15 +3210,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3132,9 +3257,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ "rustls", "tokio", @@ -3183,18 +3308,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ "indexmap", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ "winnow", ] @@ -3243,14 +3381,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -3265,7 +3403,7 @@ version = "0.1.0" source = "git+https://github.com/trussed-dev/trussed.git?rev=024e0eca5fb7dbd2457831f7c7bffe4341e08775#024e0eca5fb7dbd2457831f7c7bffe4341e08775" dependencies = [ "aes", - "bitflags 2.9.2", + "bitflags 2.9.4", "cbc", "cbor-smol", "cfg-if", @@ -3390,9 +3528,9 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-xid" @@ -3430,9 +3568,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -3446,6 +3584,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[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" @@ -3470,30 +3614,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -3505,9 +3659,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3515,9 +3669,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -3528,9 +3682,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] @@ -3565,11 +3719,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -3596,7 +3750,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -3617,7 +3771,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -3629,7 +3783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -3661,6 +3815,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -3668,7 +3828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3677,7 +3837,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3686,7 +3846,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3725,6 +3885,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3762,7 +3931,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3779,7 +3948,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3922,22 +4091,29 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "winreg" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" dependencies = [ - "bitflags 2.9.2", + "serde", + "winapi", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "x509-parser" version = "0.17.0" @@ -3963,18 +4139,18 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index 5ff6bcc..e39043a 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -15,6 +15,13 @@ path = "src/lib.rs" [features] default = [] +nfc = ["apdu-core", "apdu"] +pcsc = [ "nfc", "dep:pcsc" ] +libnfc = [ + "nfc", + "nfc1-sys", + "nfc1", +] [dependencies] base64-url = "3.0.0" @@ -64,6 +71,11 @@ snow = { version = "0.10", features = ["use-p256"] } ctap-types = { version = "0.4.0" } btleplug = "0.11.7" thiserror = "2.0.12" +apdu-core = { version = "0.4.0", optional = true } +apdu = { version = "0.4.0", 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] tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } @@ -92,3 +104,12 @@ test-log = { version = "0.2" } [patch.crates-io] trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "024e0eca5fb7dbd2457831f7c7bffe4341e08775" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "7922d67e9637a87e5625aaff9e5111f0d4ec0346" } + +# Only compile NFC-examples, if NFC-features are used +[[example]] +name = "u2f_nfc" +required-features = ["nfc"] + +[[example]] +name = "webauthn_nfc" +required-features = ["nfc"] diff --git a/libwebauthn/examples/u2f_nfc.rs b/libwebauthn/examples/u2f_nfc.rs new file mode 100644 index 0000000..437ed73 --- /dev/null +++ b/libwebauthn/examples/u2f_nfc.rs @@ -0,0 +1,70 @@ +use std::error::Error; +use std::time::Duration; + +use libwebauthn::UvUpdate; +use tokio::sync::broadcast::Receiver; +use tracing_subscriber::{self, EnvFilter}; + +use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; +use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available}; +use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::u2f::U2F; + +const TIMEOUT: Duration = Duration::from_secs(10); + +fn setup_logging() { + tracing_subscriber::fmt() + .pretty() + .with_env_filter(EnvFilter::from_default_env()) + // .without_time() + .init(); +} + +async fn handle_updates(mut state_recv: Receiver) { + while let Ok(update) = state_recv.recv().await { + match update { + UvUpdate::PresenceRequired => println!("Please touch your device!"), + _ => { /* U2F doesn't use other state updates */ } + } + } +} + +#[tokio::main] +pub async fn main() -> Result<(), Box> { + setup_logging(); + + if !is_nfc_available() { + println!("No NFC-Reader found. NFC is not available on your system."); + return Err("NFC not available".into()); + } + + let device = get_nfc_device().await?; + + if let Some(mut device) = device { + println!("Selected NFC authenticator: {}", &device); + let mut channel = device.channel().await?; + + const APP_ID: &str = "https://foo.example.org"; + let challenge: &[u8] = + &base64_url::decode("1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70").unwrap(); + // Registration ceremony + println!("Registration request sent (timeout: {:?}).", TIMEOUT); + let register_request = + RegisterRequest::new_u2f_v2(APP_ID, challenge, vec![], TIMEOUT, false); + + let state_recv = channel.get_ux_update_receiver(); + tokio::spawn(handle_updates(state_recv)); + + let response = channel.u2f_register(®ister_request).await?; + println!("Response: {:?}", response); + + // Signature ceremony + println!("Signature request sent (timeout: {:?} seconds).", TIMEOUT); + let new_key = response.as_registered_key()?; + let sign_request = SignRequest::new(APP_ID, challenge, &new_key.key_handle, TIMEOUT, true); + let response = channel.u2f_sign(&sign_request).await?; + println!("Response: {:?}", response); + } + + Ok(()) +} diff --git a/libwebauthn/examples/webauthn_hid.rs b/libwebauthn/examples/webauthn_hid.rs index b5bdb48..e1dd6e2 100644 --- a/libwebauthn/examples/webauthn_hid.rs +++ b/libwebauthn/examples/webauthn_hid.rs @@ -73,7 +73,6 @@ async fn handle_updates(mut state_recv: Receiver) { #[tokio::main] pub async fn main() -> Result<(), Box> { setup_logging(); - let devices = list_devices().await.unwrap(); println!("Devices found: {:?}", devices); diff --git a/libwebauthn/examples/webauthn_nfc.rs b/libwebauthn/examples/webauthn_nfc.rs new file mode 100644 index 0000000..b613332 --- /dev/null +++ b/libwebauthn/examples/webauthn_nfc.rs @@ -0,0 +1,156 @@ +use std::convert::TryInto; +use std::error::Error; +use std::io::{self, Write}; +use std::time::Duration; + +use libwebauthn::UvUpdate; +use rand::{thread_rng, Rng}; +use text_io::read; +use tokio::sync::broadcast::Receiver; +use tracing_subscriber::{self, EnvFilter}; + +use libwebauthn::ops::webauthn::{ + GetAssertionRequest, MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement, +}; +use libwebauthn::pin::PinRequestReason; +use libwebauthn::proto::ctap2::{ + Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, + Ctap2PublicKeyCredentialUserEntity, +}; +use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available}; +use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; + +const TIMEOUT: Duration = Duration::from_secs(10); + +fn setup_logging() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .without_time() + .init(); +} + +async fn handle_updates(mut state_recv: Receiver) { + while let Ok(update) = state_recv.recv().await { + match update { + UvUpdate::PresenceRequired => println!("Please touch your device!"), + UvUpdate::UvRetry { attempts_left } => { + print!("UV failed."); + if let Some(attempts_left) = attempts_left { + print!(" You have {attempts_left} attempts left."); + } + } + UvUpdate::PinRequired(update) => { + let mut attempts_str = String::new(); + if let Some(attempts) = update.attempts_left { + attempts_str = format!(". You have {attempts} attempts left!"); + }; + + match update.reason { + PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), + PinRequestReason::AuthenticatorPolicy => { + println!("Your device requires a PIN.") + } + PinRequestReason::FallbackFromUV => { + println!("UV failed too often and is blocked. Falling back to PIN.") + } + } + print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); + io::stdout().flush().unwrap(); + let pin_raw: String = read!("{}\n"); + + if pin_raw.is_empty() { + println!("PIN: No PIN provided, cancelling operation."); + update.cancel(); + } else { + let _ = update.send_pin(&pin_raw); + } + } + } + } +} + +#[tokio::main] +pub async fn main() -> Result<(), Box> { + setup_logging(); + + if !is_nfc_available() { + println!("No NFC-Reader found. NFC is not available on your system."); + return Err("NFC not available".into()); + } + + let device = get_nfc_device().await?; + + let user_id: [u8; 32] = thread_rng().gen(); + let challenge: [u8; 32] = thread_rng().gen(); + + if let Some(mut device) = device { + println!("Selected NFC authenticator: {}", &device); + let mut channel = device.channel().await?; + + // Make Credentials ceremony + let make_credentials_request = MakeCredentialRequest { + origin: "example.org".to_owned(), + hash: Vec::from(challenge), + relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), + user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), + resident_key: Some(ResidentKeyRequirement::Discouraged), + user_verification: UserVerificationRequirement::Preferred, + algorithms: vec![Ctap2CredentialType::default()], + exclude: None, + extensions: None, + timeout: TIMEOUT, + }; + + let state_recv = channel.get_ux_update_receiver(); + tokio::spawn(handle_updates(state_recv)); + + let response = loop { + match channel + .webauthn_make_credential(&make_credentials_request) + .await + { + Ok(response) => break Ok(response), + Err(WebAuthnError::Ctap(ctap_error)) => { + if ctap_error.is_retryable_user_error() { + println!("Oops, try again! Error: {}", ctap_error); + continue; + } + break Err(WebAuthnError::Ctap(ctap_error)); + } + Err(err) => break Err(err), + }; + } + .unwrap(); + println!("WebAuthn MakeCredential response: {:?}", response); + + let credential: Ctap2PublicKeyCredentialDescriptor = + (&response.authenticator_data).try_into().unwrap(); + let get_assertion = GetAssertionRequest { + relying_party_id: "example.org".to_owned(), + hash: Vec::from(challenge), + allow: vec![credential], + user_verification: UserVerificationRequirement::Discouraged, + extensions: None, + timeout: TIMEOUT, + }; + + let response = loop { + match channel.webauthn_get_assertion(&get_assertion).await { + Ok(response) => break Ok(response), + Err(WebAuthnError::Ctap(ctap_error)) => { + if ctap_error.is_retryable_user_error() { + println!("Oops, try again! Error: {}", ctap_error); + continue; + } + break Err(WebAuthnError::Ctap(ctap_error)); + } + Err(err) => break Err(err), + }; + } + .unwrap(); + println!("WebAuthn GetAssertion response: {:?}", response); + } + + Ok(()) +} diff --git a/libwebauthn/examples/webauthn_preflight_hid.rs b/libwebauthn/examples/webauthn_preflight_hid.rs index bc54eee..98d7165 100644 --- a/libwebauthn/examples/webauthn_preflight_hid.rs +++ b/libwebauthn/examples/webauthn_preflight_hid.rs @@ -3,7 +3,6 @@ use std::error::Error; use std::io::{self, Write}; use std::time::Duration; -use libwebauthn::transport::hid::channel::HidChannel; use libwebauthn::UvUpdate; use rand::{thread_rng, Rng}; use serde_bytes::ByteBuf; @@ -21,7 +20,7 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity, }; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel, Device}; use libwebauthn::webauthn::{CtapError, Error as WebAuthnError, WebAuthn}; const TIMEOUT: Duration = Duration::from_secs(10); @@ -156,7 +155,7 @@ pub async fn main() -> Result<(), Box> { } async fn make_credential_call( - channel: &mut HidChannel<'_>, + channel: &mut impl Channel, user_id: &[u8], exclude_list: Option>, ) -> Result { @@ -194,7 +193,7 @@ async fn make_credential_call( } async fn get_assertion_call( - channel: &mut HidChannel<'_>, + channel: &mut impl Channel, allow_list: Vec, ) -> Result { let challenge: [u8; 32] = thread_rng().gen(); diff --git a/libwebauthn/src/proto/ctap1/apdu/request.rs b/libwebauthn/src/proto/ctap1/apdu/request.rs index bc41384..a2b7ccf 100644 --- a/libwebauthn/src/proto/ctap1/apdu/request.rs +++ b/libwebauthn/src/proto/ctap1/apdu/request.rs @@ -21,11 +21,11 @@ const CONTROL_BYTE_DONT_ENFORCE_UP_AND_SIGN: u8 = 0x08; #[derive(Debug)] pub struct ApduRequest { - ins: u8, - p1: u8, - p2: u8, - data: Option>, - response_max_length: Option, + pub(crate) ins: u8, + pub(crate) p1: u8, + pub(crate) p2: u8, + pub(crate) data: Option>, + pub(crate) response_max_length: Option, } impl ApduRequest { diff --git a/libwebauthn/src/transport/ble/channel.rs b/libwebauthn/src/transport/ble/channel.rs index 22db6c1..c052c7c 100644 --- a/libwebauthn/src/transport/ble/channel.rs +++ b/libwebauthn/src/transport/ble/channel.rs @@ -86,7 +86,7 @@ impl<'a> Channel for BleChannel<'a> { } #[instrument(level = Level::DEBUG, skip_all)] - async fn apdu_send(&self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + async fn apdu_send(&mut self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { debug!({rev = ?self.revision}, "Sending APDU request"); trace!(?request); @@ -100,7 +100,7 @@ impl<'a> Channel for BleChannel<'a> { } #[instrument(level = Level::DEBUG, skip_all)] - async fn apdu_recv(&self, _timeout: Duration) -> Result { + async fn apdu_recv(&mut self, _timeout: Duration) -> Result { let response_frame = self .connection .frame_recv() diff --git a/libwebauthn/src/transport/cable/channel.rs b/libwebauthn/src/transport/cable/channel.rs index 6d63779..48cbd1f 100644 --- a/libwebauthn/src/transport/cable/channel.rs +++ b/libwebauthn/src/transport/cable/channel.rs @@ -133,12 +133,12 @@ impl<'d> Channel for CableChannel { // TODO Send CableTunnelMessageType#Shutdown and drop the connection } - async fn apdu_send(&self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + async fn apdu_send(&mut self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { error!("APDU send not supported in caBLE transport"); Err(Error::Transport(TransportError::TransportUnavailable)) } - async fn apdu_recv(&self, _timeout: Duration) -> Result { + async fn apdu_recv(&mut self, _timeout: Duration) -> Result { error!("APDU recv not supported in caBLE transport"); Err(Error::Transport(TransportError::TransportUnavailable)) } diff --git a/libwebauthn/src/transport/channel.rs b/libwebauthn/src/transport/channel.rs index 6b1931a..c3a8a2c 100644 --- a/libwebauthn/src/transport/channel.rs +++ b/libwebauthn/src/transport/channel.rs @@ -51,8 +51,8 @@ pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { async fn status(&self) -> ChannelStatus; async fn close(&mut self); - async fn apdu_send(&self, request: &ApduRequest, timeout: Duration) -> Result<(), Error>; - async fn apdu_recv(&self, timeout: Duration) -> Result; + async fn apdu_send(&mut self, request: &ApduRequest, timeout: Duration) -> Result<(), Error>; + async fn apdu_recv(&mut self, timeout: Duration) -> Result; async fn cbor_send(&mut self, request: &CborRequest, timeout: Duration) -> Result<(), Error>; async fn cbor_recv(&mut self, timeout: Duration) -> Result; diff --git a/libwebauthn/src/transport/hid/channel.rs b/libwebauthn/src/transport/hid/channel.rs index 9a775f7..3280b09 100644 --- a/libwebauthn/src/transport/hid/channel.rs +++ b/libwebauthn/src/transport/hid/channel.rs @@ -453,7 +453,7 @@ impl Channel for HidChannel<'_> { } async fn apdu_send( - &self, + &mut self, request: &ApduRequest, _timeout: std::time::Duration, ) -> Result<(), Error> { @@ -468,7 +468,7 @@ impl Channel for HidChannel<'_> { Ok(()) } - async fn apdu_recv(&self, timeout: std::time::Duration) -> Result { + async fn apdu_recv(&mut self, timeout: std::time::Duration) -> Result { let hid_response = self.hid_recv(timeout).await?; let apdu_response = ApduResponse::try_from(&hid_response.payload) .or(Err(Error::Transport(TransportError::InvalidFraming)))?; diff --git a/libwebauthn/src/transport/mock/channel.rs b/libwebauthn/src/transport/mock/channel.rs index 20d2658..f6ad459 100644 --- a/libwebauthn/src/transport/mock/channel.rs +++ b/libwebauthn/src/transport/mock/channel.rs @@ -79,10 +79,10 @@ impl Channel for MockChannel { unimplemented!(); } - async fn apdu_send(&self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + async fn apdu_send(&mut self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { unimplemented!(); } - async fn apdu_recv(&self, _timeout: Duration) -> Result { + async fn apdu_recv(&mut self, _timeout: Duration) -> Result { unimplemented!(); } diff --git a/libwebauthn/src/transport/mod.rs b/libwebauthn/src/transport/mod.rs index 85b1262..9b725d4 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; #[cfg(test)] /// A mock channel that can be used in tests to /// queue expected requests and responses in unittests diff --git a/libwebauthn/src/transport/nfc/channel.rs b/libwebauthn/src/transport/nfc/channel.rs new file mode 100644 index 0000000..d703af7 --- /dev/null +++ b/libwebauthn/src/transport/nfc/channel.rs @@ -0,0 +1,335 @@ +use apdu::core::HandleError; +use apdu::{command, Command, Response}; +use apdu_core; +use async_trait::async_trait; +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; +use std::time::Duration; +use tokio::sync::broadcast; +use tokio::sync::mpsc::{self, Sender}; +#[allow(unused_imports)] +use tracing::{debug, instrument, trace, warn, Level}; + +use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; +use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; +use crate::proto::ctap2::Ctap2; +use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; +use crate::transport::device::SupportedProtocols; +use crate::transport::error::TransportError; +use crate::webauthn::Error; +use crate::UvUpdate; + +use super::commands::{command_ctap_msg, command_get_response}; + +const SELECT_P1: u8 = 0x04; +const SELECT_P2: u8 = 0x00; +const FIDO2_AID: &[u8; 8] = b"\xa0\x00\x00\x06\x47\x2f\x00\x01"; +const SW1_MORE_DATA: u8 = 0x61; + +pub type CancelNfcOperation = (); + +#[derive(thiserror::Error)] +pub enum NfcError { + /// APDU error returned by the card. + Apdu(#[from] apdu::Error), + + /// Unexpected error occurred on the device. + Device(#[from] HandleError), +} + +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) + } +} + +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) => Display::fmt(e, f), + } + } +} + +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 {} + +#[derive(Debug, Clone)] +pub struct NfcChannelHandle { + tx: Sender, +} + +impl NfcChannelHandle { + pub async fn cancel_ongoing_operation(&self) { + let _ = self.tx.send(()).await; + } +} + +pub struct NfcChannel +where + Ctx: Copy + Sync, +{ + delegate: Box + Send + Sync>, + auth_token_data: Option, + ux_update_sender: broadcast::Sender, + handle: NfcChannelHandle, + 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) -> Self { + let (ux_update_sender, _) = broadcast::channel(16); + let (handle_tx, _handle_rx) = mpsc::channel(1); + let handle = NfcChannelHandle { tx: handle_tx }; + NfcChannel { + delegate, + auth_token_data: None, + ux_update_sender, + handle, + ctx, + apdu_response: None, + cbor_response: None, + supported: SupportedProtocols { + fido2: false, + u2f: false, + }, + status: ChannelStatus::Ready, + } + } + + pub fn get_handle(&self) -> NfcChannelHandle { + self.handle.clone() + } + + #[instrument(skip_all)] + pub async fn wink(&mut self, _timeout: Duration) -> Result { + warn!("WINK capability is not supported"); + return Ok(false); + } + + pub async fn select_fido2(&mut self) -> Result<(), Error> { + // Given legacy support for CTAP1/U2F, the client MUST determine the capabilities of the device at the selection stage. + let command = command::select_file(SELECT_P1, SELECT_P2, FIDO2_AID); + let response = self.handle(self.ctx, command)?; + let mut u2f = false; + let mut fido2 = false; + if response == b"FIDO_2_0" { + // If the authenticator ONLY implements CTAP2, the device SHALL respond with "FIDO_2_0", or 0x4649444f5f325f30. + fido2 = true; + // NOTE: Yubikeys seem to ignore this part of the specification and always return U2F_V2, even if U2F-NFC is disabled. + } else if response == b"U2F_V2" { + // If the authenticator implements CTAP1/U2F, the version information SHALL be the string "U2F_V2", or 0x5532465f5632, to maintain backwards-compatibility with CTAP1/U2F-only clients. + u2f = true; + // If the authenticator implements both CTAP1/U2F and CTAP2, the version information SHALL be the string "U2F_V2", or 0x5532465f5632, to maintain backwards-compatibility with CTAP1/U2F-only clients. CTAP2-aware clients MAY then issue a CTAP authenticatorGetInfo command to determine if the device supports CTAP2 or not. + fido2 = self.ctap2_get_info().await.is_ok(); + } + + self.supported = SupportedProtocols { u2f, fido2 }; + + Ok(()) + } + + fn handle_in_ctx( + &mut self, + ctx: Ctx, + command_buf: &[u8], + buf: &mut [u8], + ) -> Result { + let res = self.delegate.handle_in_ctx(ctx, command_buf, buf)?; + Ok(res) + } + + 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 = self.handle_in_ctx(ctx, &command_buf, &mut buf)?; + 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() + }) + } + + #[instrument(skip_all)] + pub async fn blink_and_wait_for_user_presence( + &mut self, + _timeout: Duration, + ) -> Result { + unimplemented!() + } +} + +#[async_trait] +impl Channel for NfcChannel +where + Ctx: Copy + Send + Sync + fmt::Debug + Display, +{ + type UxUpdate = UvUpdate; + + async fn supported_protocols(&self) -> Result { + Ok(self.supported) + } + + async fn status(&self) -> ChannelStatus { + self.status + } + + async fn close(&mut self) { + () + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn apdu_send(&mut self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + let resp = self.handle(self.ctx, request)?; + trace!("apdu_send {:?}", resp); + + let apdu_response = ApduResponse::new_success(&resp); + self.apdu_response = Some(apdu_response); + Ok(()) + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn apdu_recv(&mut self, _timeout: Duration) -> Result { + self.apdu_response + .take() + .ok_or(Error::Transport(TransportError::InvalidFraming)) + } + + #[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_ux_update_sender(&self) -> &broadcast::Sender { + &self.ux_update_sender + } +} + +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..3a799b3 --- /dev/null +++ b/libwebauthn/src/transport/nfc/commands.rs @@ -0,0 +1,101 @@ +use apdu::Command; + +use crate::proto::ctap1::apdu::ApduRequest; + +// 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<'a> From<&'a ApduRequest> for Command<'a> { + fn from(cmd: &'a ApduRequest) -> Self { + Self::new_with_payload( + 0, // CLA + cmd.ins, + cmd.p1, + cmd.p2, + cmd.data.as_deref().unwrap_or(&[]), + ) + } +} + +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..2ddb227 --- /dev/null +++ b/libwebauthn/src/transport/nfc/device.rs @@ -0,0 +1,142 @@ +use async_trait::async_trait; +use std::fmt; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +use crate::{ + transport::{device::Device, Channel}, + webauthn::Error, +}; + +use super::channel::NfcChannel; +#[cfg(feature = "libnfc")] +use super::libnfc; +#[cfg(feature = "pcsc")] +use super::pcsc; +use super::{Context, Nfc}; + +#[derive(Clone, Debug)] +enum DeviceInfo { + #[cfg(feature = "libnfc")] + LibNfc(libnfc::Info), + #[cfg(feature = "pcsc")] + Pcsc(pcsc::Info), +} + +#[derive(Clone, 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), + } + } + + async fn channel_sync(&self) -> Result, Error> { + trace!("nfc channel {:?}", self); + let mut channel: NfcChannel = match &self.info { + #[cfg(feature = "libnfc")] + DeviceInfo::LibNfc(info) => info.channel(), + #[cfg(feature = "pcsc")] + DeviceInfo::Pcsc(info) => info.channel(), + }?; + + channel.select_fido2().await?; + Ok(channel) + } +} + +#[async_trait] +impl<'d> Device<'d, Nfc, NfcChannel> for NfcDevice { + async fn channel(&'d mut self) -> Result, Error> { + self.channel_sync().await + } +} + +async fn is_fido(device: &NfcDevice) -> bool +where + Ctx: fmt::Debug + fmt::Display + Copy + Send + Sync, +{ + async fn inner(device: &NfcDevice) -> Result + where + Ctx: fmt::Debug + fmt::Display + Copy + Send + Sync, + { + let chan = device.channel_sync().await?; + // We fill the struct within channel_sync() and the call cannot fail for NFC, + // so unwrap is fine here + let protocols = chan.supported_protocols().await.unwrap(); + Ok(protocols.fido2 || protocols.u2f) + } + + inner::(device).await.is_ok() +} + +#[instrument] +/// Returns Ok(None) if no devices are found, otherwise returns +/// the first device found by either NFC-backend. +pub async fn get_nfc_device() -> Result, Error> { + // See https://github.com/linux-credentials/libwebauthn/issues/154 for + // why we only return the first found device here. + // We'd otherwise need to deduplicate found devices here, as + // we'll potentially have the same device discovered by + // both backends and thus added multiple times to the list. + let list_devices_fns = [ + #[cfg(feature = "libnfc")] + libnfc::list_devices, + #[cfg(feature = "pcsc")] + pcsc::list_devices, + ]; + + for list_devices in list_devices_fns { + for device in list_devices()? { + if is_fido::(&device).await { + return Ok(Some(device)); + } + } + } + + Ok(None) +} + +#[instrument] +pub fn is_nfc_available() -> bool { + let mut available = false; + #[cfg(feature = "libnfc")] + { + available |= libnfc::is_nfc_available(); + } + #[cfg(feature = "pcsc")] + { + available |= pcsc::is_nfc_available(); + } + + available +} diff --git a/libwebauthn/src/transport/nfc/libnfc/mod.rs b/libwebauthn/src/transport/nfc/libnfc/mod.rs new file mode 100644 index 0000000..7328931 --- /dev/null +++ b/libwebauthn/src/transport/nfc/libnfc/mod.rs @@ -0,0 +1,238 @@ +use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; +use super::device::NfcDevice; +use super::Context; +use crate::transport::error::TransportError; +use crate::webauthn::Error; +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; +#[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(Clone, 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: &str) -> Self { + Info { + connstring: connstring.to_string(), + } + } + + pub fn channel(&self) -> Result, Error> { + let context = nfc1::Context::new().map_err(map_error)?; + + 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); + Ok(channel) + } +} + +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(crate) fn is_nfc_available() -> bool { + let Ok(mut context) = nfc1::Context::new() else { + return false; + }; + // "list_devices()" lists readers. If none is found, we say it is not available + context.list_devices(1).map(|d| d.len()).unwrap_or_default() > 0 +} + +#[instrument] +pub(crate) 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..89c7754 --- /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::{get_nfc_device, is_nfc_available}; + +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..191d91f --- /dev/null +++ b/libwebauthn/src/transport/nfc/pcsc/mod.rs @@ -0,0 +1,177 @@ +use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; +use super::device::NfcDevice; +use super::Context; +use crate::transport::error::TransportError; +use crate::webauthn::Error; +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}; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +#[derive(Clone, 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, Error> { + 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); + Ok(channel) + } +} + +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(crate) fn is_nfc_available() -> bool { + let Ok(ctx) = pcsc::Context::establish(pcsc::Scope::User) else { + return false; + }; + // If there is no reader, we say NFC is not available + ctx.list_readers_len().unwrap_or_default() > 0 +} + +#[instrument] +pub(crate) 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"); + if len == 0 { + return Err(Error::Transport(TransportError::TransportUnavailable)); + } + 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) +} diff --git a/libwebauthn/src/webauthn/pin_uv_auth_token.rs b/libwebauthn/src/webauthn/pin_uv_auth_token.rs index e79903a..86bd16d 100644 --- a/libwebauthn/src/webauthn/pin_uv_auth_token.rs +++ b/libwebauthn/src/webauthn/pin_uv_auth_token.rs @@ -109,7 +109,7 @@ where // If it is not discouraged and either RP or device requires it. let uv = !rp_uv_discouraged && (rp_uv_preferred || dev_uv_protected); - debug!(%rp_uv_preferred, %dev_uv_protected, %uv, %needs_shared_secret, %can_establish_shared_secret, "Checking if user verification is required"); + debug!(%rp_uv_preferred, %rp_uv_discouraged, %dev_uv_protected, %uv, %needs_shared_secret, %can_establish_shared_secret, "Checking if user verification is required"); // If we do not need to create a shared secret, we can error out here early if !needs_shared_secret { if !uv {