summaryrefslogtreecommitdiffstats
path: root/third_party/rust/authenticator
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/rust/authenticator
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator')
-rw-r--r--third_party/rust/authenticator/.cargo-checksum.json1
-rw-r--r--third_party/rust/authenticator/Cargo.lock857
-rw-r--r--third_party/rust/authenticator/Cargo.toml152
-rw-r--r--third_party/rust/authenticator/Cross.toml5
-rw-r--r--third_party/rust/authenticator/LICENSE373
-rw-r--r--third_party/rust/authenticator/README.md50
-rw-r--r--third_party/rust/authenticator/build.rs64
-rw-r--r--third_party/rust/authenticator/examples/ctap2.rs285
-rw-r--r--third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs380
-rw-r--r--third_party/rust/authenticator/examples/interactive_management.rs810
-rw-r--r--third_party/rust/authenticator/examples/reset.rs133
-rw-r--r--third_party/rust/authenticator/examples/set_pin.rs146
-rw-r--r--third_party/rust/authenticator/examples/test_exclude_list.rs309
-rw-r--r--third_party/rust/authenticator/rust-toolchain.toml5
-rw-r--r--third_party/rust/authenticator/rustfmt.toml3
-rw-r--r--third_party/rust/authenticator/src/authenticatorservice.rs642
-rw-r--r--third_party/rust/authenticator/src/consts.rs151
-rw-r--r--third_party/rust/authenticator/src/crypto/der.rs185
-rw-r--r--third_party/rust/authenticator/src/crypto/dummy.rs57
-rw-r--r--third_party/rust/authenticator/src/crypto/mod.rs1638
-rw-r--r--third_party/rust/authenticator/src/crypto/nss.rs481
-rw-r--r--third_party/rust/authenticator/src/crypto/openssl.rs183
-rw-r--r--third_party/rust/authenticator/src/ctap2/attestation.rs1129
-rw-r--r--third_party/rust/authenticator/src/ctap2/client_data.rs339
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs225
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs660
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/client_pin.rs851
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/credential_management.rs457
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs1494
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_info.rs1054
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs54
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_version.rs121
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs1061
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/mod.rs477
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/reset.rs123
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/selection.rs123
-rw-r--r--third_party/rust/authenticator/src/ctap2/mod.rs1518
-rw-r--r--third_party/rust/authenticator/src/ctap2/preflight.rs530
-rw-r--r--third_party/rust/authenticator/src/ctap2/server.rs629
-rw-r--r--third_party/rust/authenticator/src/ctap2/utils.rs39
-rw-r--r--third_party/rust/authenticator/src/errors.rs135
-rw-r--r--third_party/rust/authenticator/src/lib.rs113
-rw-r--r--third_party/rust/authenticator/src/manager.rs218
-rw-r--r--third_party/rust/authenticator/src/statecallback.rs166
-rw-r--r--third_party/rust/authenticator/src/statemachine.rs427
-rw-r--r--third_party/rust/authenticator/src/status_update.rs117
-rw-r--r--third_party/rust/authenticator/src/transport/device_selector.rs477
-rw-r--r--third_party/rust/authenticator/src/transport/errors.rs98
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/device.rs235
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/monitor.rs161
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/transaction.rs69
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/uhid.rs89
-rw-r--r--third_party/rust/authenticator/src/transport/hid.rs252
-rw-r--r--third_party/rust/authenticator/src/transport/hidproto.rs257
-rw-r--r--third_party/rust/authenticator/src/transport/linux/device.rs181
-rw-r--r--third_party/rust/authenticator/src/transport/linux/hidraw.rs80
-rw-r--r--third_party/rust/authenticator/src/transport/linux/hidwrapper.h12
-rw-r--r--third_party/rust/authenticator/src/transport/linux/hidwrapper.rs54
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_loongarch64.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/mod.rs12
-rw-r--r--third_party/rust/authenticator/src/transport/linux/monitor.rs194
-rw-r--r--third_party/rust/authenticator/src/transport/linux/transaction.rs69
-rw-r--r--third_party/rust/authenticator/src/transport/macos/device.rs226
-rw-r--r--third_party/rust/authenticator/src/transport/macos/iokit.rs292
-rw-r--r--third_party/rust/authenticator/src/transport/macos/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/transport/macos/monitor.rs212
-rw-r--r--third_party/rust/authenticator/src/transport/macos/transaction.rs107
-rw-r--r--third_party/rust/authenticator/src/transport/mock/device.rs328
-rw-r--r--third_party/rust/authenticator/src/transport/mock/mod.rs6
-rw-r--r--third_party/rust/authenticator/src/transport/mock/transaction.rs35
-rw-r--r--third_party/rust/authenticator/src/transport/mod.rs369
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/device.rs248
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/fd.rs62
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/mod.rs10
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/monitor.rs132
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/transaction.rs69
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/uhid.rs77
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/device.rs224
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/mod.rs8
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/monitor.rs138
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/transaction.rs69
-rw-r--r--third_party/rust/authenticator/src/transport/stub/device.rs115
-rw-r--r--third_party/rust/authenticator/src/transport/stub/mod.rs11
-rw-r--r--third_party/rust/authenticator/src/transport/stub/transaction.rs52
-rw-r--r--third_party/rust/authenticator/src/transport/windows/device.rs173
-rw-r--r--third_party/rust/authenticator/src/transport/windows/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/transport/windows/monitor.rs125
-rw-r--r--third_party/rust/authenticator/src/transport/windows/transaction.rs69
-rw-r--r--third_party/rust/authenticator/src/transport/windows/winapi.rs263
-rw-r--r--third_party/rust/authenticator/src/u2ftypes.rs341
-rw-r--r--third_party/rust/authenticator/src/util.rs76
-rw-r--r--third_party/rust/authenticator/testing/cross/powerpc64le-unknown-linux-gnu.Dockerfile8
-rw-r--r--third_party/rust/authenticator/testing/cross/x86_64-unknown-linux-gnu.Dockerfile7
-rwxr-xr-xthird_party/rust/authenticator/testing/run_cross.sh11
107 files changed, 25165 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/.cargo-checksum.json b/third_party/rust/authenticator/.cargo-checksum.json
new file mode 100644
index 0000000000..1d63f25e48
--- /dev/null
+++ b/third_party/rust/authenticator/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.lock":"4363d40d2b97c58fa394ab07f48175a98dbe615ff0bfdfc55b19b1023fbdb648","Cargo.toml":"a3ee3bfd10a9f6414f90db4e446ee5f731ca98a9c6a6290fb04e05a08c9ace49","Cross.toml":"8d132da818d48492aa9f4b78a348f0df3adfae45d988d42ebd6be8a5adadb6c3","LICENSE":"e866c8f5864d4cacfe403820e722e9dc03fe3c7565efa5e4dad9051d827bb92a","README.md":"c87d9c7cc44f1dd4ef861a3a9f8cd2eb68aedd3814768871f5fb63c2070806cd","build.rs":"5b909f42e52ed2056afa3693544ef1c1dc5e90d00e7d8730175a228bd0233b43","examples/ctap2.rs":"e83d16c1b5aaca585b7df0655a696ddfeb0529aa2f4645bd30c74f58574fc0c6","examples/ctap2_discoverable_creds.rs":"539951c016e6058b7633fc76ec0f1c02a405eedb45dfc912c70c0b6991a77937","examples/interactive_management.rs":"e164439c7925f748620540668e5fea467342a16dd062f9d08f217ffe59bf1b44","examples/reset.rs":"a8ffd75b248beeede129698f2c7bc971789fda0e6e8f66ac76cc2f8ae770432d","examples/set_pin.rs":"14806b2d20034534f77dd5000c440af4dd4f1f4eedbd335942cd1cc1fcd0037a","examples/test_exclude_list.rs":"f8c02aaa6236723f5c52440e5155608517cd19048899a21ee5deb5ea8537edca","rust-toolchain.toml":"faee28253e1b6ece8840fb195d3de62e8f415aad6d39d20414eb389c291f4f12","rustfmt.toml":"ceb6615363d6fff16426eb56f5727f98a7f7ed459ba9af735b1d8b672e2c3b9b","src/authenticatorservice.rs":"59a43779765a7e40841b507eb2a23da0693e0fdfec85a0ffc4f99e66aa76391f","src/consts.rs":"44fb7c396dc87d1657d1feed08e956fc70608c0b06a034716b626419b442bcfe","src/crypto/der.rs":"7afd8c914a2df36052355cd5b21507472ed4d3925305050f9d7f2e5ca7e4d018","src/crypto/dummy.rs":"92e5238da8e6f57bae057f564f5c84719dab6ed22c1b390884fe1a8bccb91f17","src/crypto/mod.rs":"3e77cc3b2be7573145fed8ed8731736f1a6e777e38df248041e3038f2721b91f","src/crypto/nss.rs":"0dcdf2af1d49f8aa9c235c5bcdeabcf26abed219dfd84b17667cd2d0ae9d9c8c","src/crypto/openssl.rs":"b2577be577b884b569a5bc039b82e0402db097b5652d70d6320922ad6795f1c7","src/ctap2/attestation.rs":"cf48b0c79a04920acb4bec18a9472e449e2bcb515832c1b11cf7413e30a6033d","src/ctap2/client_data.rs":"447c970c9d079431a3cab0bf3b0d68f1848966ffebe77f6f08b3e7a865073786","src/ctap2/commands/authenticator_config.rs":"920822bc7dbfbe4dd3429978d24c15091d829e6b69f46cb7347bb7140518af27","src/ctap2/commands/bio_enrollment.rs":"9dc1606abdb8ae660eec9bd28f45d1241892557033241e3aea55082fbab35d60","src/ctap2/commands/client_pin.rs":"ae73e966106041476a177c061c1088d655614c6f8c73cdf66e27d8b503d482c0","src/ctap2/commands/credential_management.rs":"f51ce10c1b2df7131ee4fda258c338811c0afa8156ceb6faa7be2c02fefa4455","src/ctap2/commands/get_assertion.rs":"da260a9ef5aac6939b4599a3fd4e96bf23644b3c5c2a0288bfd8f4e6515f8da8","src/ctap2/commands/get_info.rs":"3e354be80c6afb253b472c8dda336750edddc4c2ca03ca12f68fab8920fe9744","src/ctap2/commands/get_next_assertion.rs":"fb0edd201d90f5a706edf58cdea901f8783f882968a028b466892d0a38d10ffe","src/ctap2/commands/get_version.rs":"5008dec81581d0604e000bd6f2242db06fb550b220709f6e263a9bb340570921","src/ctap2/commands/make_credentials.rs":"5acbb356f828e0dfea9d56ce44a9d3931a05a506df805190d92393faf1963a1a","src/ctap2/commands/mod.rs":"bee7d4f612ab3489a0ebdf9eba217ea29b70e830bb3258aa0148754d984238ce","src/ctap2/commands/reset.rs":"610a1979d20e801cb2ac4a6efe15b40699d30a70ba8c3f834c0066da10af3637","src/ctap2/commands/selection.rs":"dd7d21bd063fd618a53fd64a4e88e41e9344f335dfbcec25f8a886b6c4da8e0c","src/ctap2/mod.rs":"68def2bf21c89035ecc078d657ff3fafcdfd9eb137a42a7334288baad26c2ec6","src/ctap2/preflight.rs":"23f35714f9ec57ad603f3152b805a8a09a09e7629202be54a2ea17cded0d7bb9","src/ctap2/server.rs":"d30fd9fad072416e70c8fbfc87094ce9230a0a6ecc63278d4109ae5d6271c96b","src/ctap2/utils.rs":"7ca56ae241f22de67047d22c6650bbe36f1268ab64bf8cc9256d2f00ef750c0a","src/errors.rs":"0639d55735b5b67562b4ff8b6b6639d1449f56cdcb0a0cdab3209d1bc972cab7","src/lib.rs":"00f2bfd489f77d9f10711983d742148a037e5989d02a44a65ae0fd3cbbd34dc0","src/manager.rs":"b7106c82c62c8bb47d3ec979f3454e0fb04dae0b32d029624b23a880819b16a7","src/statecallback.rs":"6748b74341876d7698aa3b1a6c1de002a74e201375040397255445ff4a1d7982","src/statemachine.rs":"412747465209ac080e941dae7cfdd808709803305977cfc08f349a8d2cfc61b0","src/status_update.rs":"6b8de35dbcba36dcf6ff388b73c49e04568155ed288b35ec9582001fbdd177e7","src/transport/device_selector.rs":"406b947a770ab5db939d06f720f68ddeb3be275eb4567feb96913cecf013902e","src/transport/errors.rs":"5af7cb8d22ffa63bf4264d182a0f54b9b3a2cc9d19d832b3495857229f9a2875","src/transport/freebsd/device.rs":"0aa53590382093225b6f17af4deb3224eb52a883d19dc7da520fcbfceb1cad58","src/transport/freebsd/mod.rs":"42dcb57fbeb00140003a8ad39acac9b547062b8f281a3fa5deb5f92a6169dde6","src/transport/freebsd/monitor.rs":"a6b34af4dd2e357a5775b1f3a723766107c11ef98dba859b1188ed08e0e450a2","src/transport/freebsd/transaction.rs":"ec28475a70dded260f9a7908c7f88dd3771f5d64b9a5dda835411d13b713c39a","src/transport/freebsd/uhid.rs":"a194416a8bc5d428c337f8d96a2248769ca190810852bbe5ee686ab595d8eb4c","src/transport/hid.rs":"3b1e4c6a5f62faa59c964d910ad3fc5928c3e363c07c9a22cf6f43e730198a8f","src/transport/hidproto.rs":"1f36992a806f753bac6582c2263d5cc1dd2924df52af97370e55dd6c189ef545","src/transport/linux/device.rs":"206a5ae404590bc73acc03e22823ea4252848b5afab744a51f9630b0f1af813c","src/transport/linux/hidraw.rs":"c7a0df9b4e51cb2736218ffffa02b2b2547b7c515d69f9bae2c9a8c8f1cb547b","src/transport/linux/hidwrapper.h":"72785db3a9b27ea72b6cf13a958fee032af54304522d002f56322473978a20f9","src/transport/linux/hidwrapper.rs":"d203e8804e7632b8d47a224c186d1f431800f04ddc43360d5c086f71e9b0f674","src/transport/linux/ioctl_aarch64le.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/ioctl_armle.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/ioctl_loongarch64.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/ioctl_mips64le.rs":"fbda309934ad8bda689cd4fb5c0ca696fe26dedb493fe9d5a5322c3047d474fd","src/transport/linux/ioctl_mipsbe.rs":"fbda309934ad8bda689cd4fb5c0ca696fe26dedb493fe9d5a5322c3047d474fd","src/transport/linux/ioctl_mipsle.rs":"fbda309934ad8bda689cd4fb5c0ca696fe26dedb493fe9d5a5322c3047d474fd","src/transport/linux/ioctl_powerpc64be.rs":"fbda309934ad8bda689cd4fb5c0ca696fe26dedb493fe9d5a5322c3047d474fd","src/transport/linux/ioctl_powerpc64le.rs":"fbda309934ad8bda689cd4fb5c0ca696fe26dedb493fe9d5a5322c3047d474fd","src/transport/linux/ioctl_powerpcbe.rs":"fbda309934ad8bda689cd4fb5c0ca696fe26dedb493fe9d5a5322c3047d474fd","src/transport/linux/ioctl_riscv64.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/ioctl_s390xbe.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/ioctl_x86.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/ioctl_x86_64.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/mod.rs":"446e435126d2a58f167f648dd95cba28e8ac9c17f1f799e1eaeab80ea800fc57","src/transport/linux/monitor.rs":"5e3ec2618dd74027ae6ca1527991254e3271cce59106d4920ce0414094e22f64","src/transport/linux/transaction.rs":"ec28475a70dded260f9a7908c7f88dd3771f5d64b9a5dda835411d13b713c39a","src/transport/macos/device.rs":"cef7cec681d9c777aac16e662bcbe8ff0d39efb2116086bf9f792945f1454c96","src/transport/macos/iokit.rs":"7dc4e7bbf8e42e2fcde0cee8e48d14d6234a5a910bd5d3c4e966d8ba6b73992f","src/transport/macos/mod.rs":"333e561554fc901d4f6092f6e4c85823e2b0c4ff31c9188d0e6d542b71a0a07c","src/transport/macos/monitor.rs":"e02288454bb4010e06b705d82646abddb3799f0cd655f574aa19f9d91485a4a2","src/transport/macos/transaction.rs":"9dcdebd13d5fd5a185b5ad777a80c825a6ba5e76b141c238aa115b451b9a72fa","src/transport/mock/device.rs":"16d6472f971f37bf6fbd9f278e683f066c76ea75343de98371e9bc4ba05476f5","src/transport/mock/mod.rs":"9c4c87efd19adddc1a91c699a6c328063cfbac5531b76346a5ff92e986aded8f","src/transport/mock/transaction.rs":"be3ed8c389dfa04122364b82515edd76fad6f5d5f72d15cacd45a84fb8397292","src/transport/mod.rs":"eacb0071e41a567ae0066ebebd6edf9001475f6a3f806f7df2aac7823aa86c9a","src/transport/netbsd/device.rs":"4c8404683c1fe07e562ec7126538643278e632f20e1f38b909a02526ef50d8e4","src/transport/netbsd/fd.rs":"5464019025d03ea2a39c82f76b238bbbdb0ea63f5a5fc7c9d974e235139cd53b","src/transport/netbsd/mod.rs":"b1c52aa29537330cebe67427062d6c94871cab2a9b0c04b2305d686f07e88fd5","src/transport/netbsd/monitor.rs":"fb2917e4ba53cc9867987a539061f82d011f4c6e478df1157d965d32df2eb922","src/transport/netbsd/transaction.rs":"ec28475a70dded260f9a7908c7f88dd3771f5d64b9a5dda835411d13b713c39a","src/transport/netbsd/uhid.rs":"d15be35e2413240066a8f086bb8846b08a6a92bf6a1941c3eec1329dd3a4f9ce","src/transport/openbsd/device.rs":"8fcd46ae1e1df4434aa93e629ec379f7944a0120c3e75b0ee4f9f2afa3a187be","src/transport/openbsd/mod.rs":"514274d414042ff84b3667a41a736e78581e22fda87ccc97c2bc05617e381a30","src/transport/openbsd/monitor.rs":"2e0ba6ecc69b450be9cbfd21a7c65036ed2ce593b12363596d3eae0b5bfb79e8","src/transport/openbsd/transaction.rs":"ec28475a70dded260f9a7908c7f88dd3771f5d64b9a5dda835411d13b713c39a","src/transport/stub/device.rs":"d064faee6c5e4681e6b81878aa419de54c9df9a7eb805336d0cfda82318253d1","src/transport/stub/mod.rs":"6a7fec504a52d403b0241b18cd8b95088a31807571f4c0a67e4055afc74f4453","src/transport/stub/transaction.rs":"c9a3ade9562468163f28fd51e7ff3e0bf5854b7edade9e987000d11c5d0e62d2","src/transport/windows/device.rs":"a5e997dc84acf526cbd23d0228d6182ab23d0e56a9314349e5066457502e10a7","src/transport/windows/mod.rs":"218e7f2fe91ecb390c12bba5a5ffdad2c1f0b22861c937f4d386262e5b3dd617","src/transport/windows/monitor.rs":"95913d49e7d83482e420493d89b53ffceb6a49e646a87de934dff507b3092b4c","src/transport/windows/transaction.rs":"ec28475a70dded260f9a7908c7f88dd3771f5d64b9a5dda835411d13b713c39a","src/transport/windows/winapi.rs":"b2a4cc85f14e39cadfbf068ee001c9d776f028d3cf09cb926d4364c5b437c112","src/u2ftypes.rs":"b9c96004c13a8c2cf510983bfb701909c8f5953dfbb5764040d54814bb05f370","src/util.rs":"80a6e00a86a0042d827dfba1c689864898ac56ee7582baa41dd6ad54c5961e57","testing/cross/powerpc64le-unknown-linux-gnu.Dockerfile":"d7463ff4376e3e0ca3fed879fab4aa975c4c0a3e7924c5b88aef9381a5d013de","testing/cross/x86_64-unknown-linux-gnu.Dockerfile":"11c79c04b07a171b0c9b63ef75fa75f33263ce76e3c1eda0879a3e723ebd0c24","testing/run_cross.sh":"cc2a7e0359f210eba2e7121f81eb8ab0125cea6e0d0f2698177b0fe2ad0c33d8"},"package":"be346361f2602704c3a48d71530df852a59558b9774a144432d91fdfe775f298"} \ No newline at end of file
diff --git a/third_party/rust/authenticator/Cargo.lock b/third_party/rust/authenticator/Cargo.lock
new file mode 100644
index 0000000000..7ecee8aedb
--- /dev/null
+++ b/third_party/rust/authenticator/Cargo.lock
@@ -0,0 +1,857 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "assert_matches"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "authenticator"
+version = "0.4.0-alpha.24"
+dependencies = [
+ "assert_matches",
+ "base64",
+ "bindgen 0.58.1",
+ "bitflags",
+ "bytes",
+ "cfg-if",
+ "core-foundation",
+ "devd-rs",
+ "env_logger 0.6.2",
+ "getopts",
+ "libc",
+ "libudev",
+ "log",
+ "memoffset",
+ "nss-gk-api",
+ "openssl",
+ "openssl-sys",
+ "pkcs11-bindings",
+ "rand",
+ "rpassword",
+ "runloop",
+ "serde",
+ "serde_bytes",
+ "serde_cbor",
+ "serde_json",
+ "sha2",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
+
+[[package]]
+name = "bindgen"
+version = "0.58.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f"
+dependencies = [
+ "bitflags",
+ "cexpr 0.4.0",
+ "clang-sys",
+ "clap",
+ "env_logger 0.8.4",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "which",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a022e58a142a46fea340d68012b9201c094e93ec3d033a944a24f8fd4a4f09a"
+dependencies = [
+ "bitflags",
+ "cexpr 0.6.0",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bytes"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
+
+[[package]]
+name = "cexpr"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
+dependencies = [
+ "nom 5.1.2",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom 7.1.1",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "devd-rs"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9313f104b590510b46fc01c0a324fc76505c13871454d3c48490468d04c8d395"
+dependencies = [
+ "libc",
+ "nom 7.1.1",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3"
+dependencies = [
+ "atty",
+ "humantime 1.3.0",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
+dependencies = [
+ "atty",
+ "humantime 2.1.0",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "half"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
+dependencies = [
+ "quick-error",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "itoa"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[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.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libudev"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea626d3bdf40a1c5aee3bcd4f40826970cae8d80a8fec934c82a63840094dcfe"
+dependencies = [
+ "libc",
+ "libudev-sys",
+]
+
+[[package]]
+name = "libudev-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "mozbuild"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "903970ae2f248d7275214cf8f387f8ba0c4ea7e3d87a320e85493db60ce28616"
+
+[[package]]
+name = "nom"
+version = "5.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
+dependencies = [
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nss-gk-api"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c17aec6d4e1822c023689899f09311592a36cbf6de8f85dfaf5f01976790d8d"
+dependencies = [
+ "bindgen 0.61.0",
+ "mozbuild",
+ "once_cell",
+ "pkcs11-bindings",
+ "serde",
+ "serde_derive",
+ "toml",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "openssl"
+version = "0.10.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "pkcs11-bindings"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20556de5b64f5d7213b8ea103b92261cac789b59978652d9cd831ba9f477c53"
+dependencies = [
+ "bindgen 0.61.0",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rpassword"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "runloop"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d79b4b604167921892e84afbbaad9d5ad74e091bf6c511d9dbfb0593f09fabd"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "serde"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_cbor"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
+dependencies = [
+ "half",
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "syn"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "which"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/third_party/rust/authenticator/Cargo.toml b/third_party/rust/authenticator/Cargo.toml
new file mode 100644
index 0000000000..611ef12a00
--- /dev/null
+++ b/third_party/rust/authenticator/Cargo.toml
@@ -0,0 +1,152 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "authenticator"
+version = "0.4.0-alpha.24"
+authors = [
+ "J.C. Jones <jc@mozilla.com>",
+ "Tim Taubert <ttaubert@mozilla.com>",
+ "Kyle Machulis <kyle@nonpolynomial.com>",
+]
+description = "Library for interacting with CTAP1/2 security keys for Web Authentication. Used by Firefox."
+readme = "README.md"
+keywords = [
+ "ctap2",
+ "u2f",
+ "fido",
+ "webauthn",
+]
+categories = [
+ "cryptography",
+ "hardware-support",
+ "os",
+]
+license = "MPL-2.0"
+repository = "https://github.com/mozilla/authenticator-rs/"
+
+[dependencies.base64]
+version = "^0.21"
+
+[dependencies.bitflags]
+version = "1.0"
+
+[dependencies.bytes]
+version = "0.5"
+features = ["serde"]
+optional = true
+
+[dependencies.cfg-if]
+version = "1.0"
+
+[dependencies.libc]
+version = "0.2"
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.nss-gk-api]
+version = "0.3.0"
+optional = true
+
+[dependencies.openssl]
+version = "0.10"
+optional = true
+
+[dependencies.openssl-sys]
+version = "0.9"
+optional = true
+
+[dependencies.pkcs11-bindings]
+version = "0.1.4"
+optional = true
+
+[dependencies.rand]
+version = "0.8"
+
+[dependencies.runloop]
+version = "0.1.0"
+
+[dependencies.serde]
+version = "1.0"
+features = ["derive"]
+
+[dependencies.serde_bytes]
+version = "0.11"
+
+[dependencies.serde_cbor]
+version = "0.11"
+
+[dependencies.serde_json]
+version = "1.0"
+
+[dependencies.sha2]
+version = "^0.10.0"
+
+[dev-dependencies.assert_matches]
+version = "1.2"
+
+[dev-dependencies.env_logger]
+version = "^0.6"
+
+[dev-dependencies.getopts]
+version = "^0.2"
+
+[dev-dependencies.rpassword]
+version = "5.0"
+
+[build-dependencies.bindgen]
+version = "^0.58.1"
+optional = true
+
+[features]
+binding-recompile = ["bindgen"]
+crypto_dummy = []
+crypto_nss = [
+ "nss-gk-api",
+ "pkcs11-bindings",
+]
+crypto_openssl = [
+ "openssl",
+ "openssl-sys",
+]
+default = ["crypto_nss"]
+gecko = ["nss-gk-api/gecko"]
+
+[target."cfg(target_os = \"freebsd\")".dependencies.devd-rs]
+version = "0.3"
+
+[target."cfg(target_os = \"linux\")".dependencies.libudev]
+version = "^0.2"
+
+[target."cfg(target_os = \"macos\")".dependencies.core-foundation]
+version = "0.9"
+
+[target."cfg(target_os = \"windows\")".dependencies.memoffset]
+version = "0.8"
+
+[target."cfg(target_os = \"windows\")".dependencies.winapi]
+version = "^0.3"
+features = [
+ "handleapi",
+ "hidclass",
+ "hidpi",
+ "hidusage",
+ "setupapi",
+]
+
+[badges.maintenance]
+status = "actively-developed"
+
+[badges.travis-ci]
+branch = "master"
+repository = "mozilla/authenticator-rs"
diff --git a/third_party/rust/authenticator/Cross.toml b/third_party/rust/authenticator/Cross.toml
new file mode 100644
index 0000000000..cde355d84f
--- /dev/null
+++ b/third_party/rust/authenticator/Cross.toml
@@ -0,0 +1,5 @@
+[target.x86_64-unknown-linux-gnu]
+image = "local_cross:x86_64-unknown-linux-gnu"
+
+[target.powerpc64le-unknown-linux-gnu]
+image = "local_cross:powerpc64le-unknown-linux-gnu" \ No newline at end of file
diff --git a/third_party/rust/authenticator/LICENSE b/third_party/rust/authenticator/LICENSE
new file mode 100644
index 0000000000..3f4f7ac8b1
--- /dev/null
+++ b/third_party/rust/authenticator/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+means each individual or legal entity that creates, contributes to
+the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+means the combination of the Contributions of others (if any) used
+by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+means Source Code Form to which the initial Contributor has attached
+the notice in Exhibit A, the Executable Form of such Source Code
+Form, and Modifications of such Source Code Form, in each case
+including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+means
+
+(a) that the initial Contributor has attached the notice described
+in Exhibit B to the Covered Software; or
+
+(b) that the Covered Software was made available under the terms of
+version 1.1 or earlier of the License, but not also under the
+terms of a Secondary License.
+
+1.6. "Executable Form"
+means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+means a work that combines Covered Software with other material, in
+a separate file or files, that is not Covered Software.
+
+1.8. "License"
+means this document.
+
+1.9. "Licensable"
+means having the right to grant, to the maximum extent possible,
+whether at the time of the initial grant or subsequently, any and
+all of the rights conveyed by this License.
+
+1.10. "Modifications"
+means any of the following:
+
+(a) any file in Source Code Form that results from an addition to,
+deletion from, or modification of the contents of Covered
+Software; or
+
+(b) any new file in Source Code Form that contains any Covered
+Software.
+
+1.11. "Patent Claims" of a Contributor
+means any patent claim(s), including without limitation, method,
+process, and apparatus claims, in any patent Licensable by such
+Contributor that would be infringed, but for the grant of the
+License, by the making, using, selling, offering for sale, having
+made, import, or transfer of either its Contributions or its
+Contributor Version.
+
+1.12. "Secondary License"
+means either the GNU General Public License, Version 2.0, the GNU
+Lesser General Public License, Version 2.1, the GNU Affero General
+Public License, Version 3.0, or any later versions of those
+licenses.
+
+1.13. "Source Code Form"
+means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+means an individual or a legal entity exercising rights under this
+License. For legal entities, "You" includes any entity that
+controls, is controlled by, or is under common control with You. For
+purposes of this definition, "control" means (a) the power, direct
+or indirect, to cause the direction or management of such entity,
+whether by contract or otherwise, or (b) ownership of more than
+fifty percent (50%) of the outstanding shares or beneficial
+ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+Licensable by such Contributor to use, reproduce, make available,
+modify, display, perform, distribute, and otherwise exploit its
+Contributions, either on an unmodified basis, with Modifications, or
+as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+for sale, have made, import, and otherwise transfer either its
+Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+or
+
+(b) for infringements caused by: (i) Your and any other third party's
+modifications of Covered Software, or (ii) the combination of its
+Contributions with other software (except as part of its Contributor
+Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+Form, as described in Section 3.1, and You must inform recipients of
+the Executable Form how they can obtain a copy of such Source Code
+Form by reasonable means in a timely manner, at a charge no more
+than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+License, or sublicense it under different terms, provided that the
+license for the Executable Form does not attempt to limit or alter
+the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+This Source Code Form is "Incompatible With Secondary Licenses", as
+defined by the Mozilla Public License, v. 2.0.
diff --git a/third_party/rust/authenticator/README.md b/third_party/rust/authenticator/README.md
new file mode 100644
index 0000000000..74d8cc2e5c
--- /dev/null
+++ b/third_party/rust/authenticator/README.md
@@ -0,0 +1,50 @@
+# A Rust library for interacting with CTAP1/CTAP2 Security Keys
+
+[![Build Status](https://travis-ci.org/mozilla/authenticator-rs.svg?branch=master)](https://travis-ci.org/mozilla/authenticator-rs)
+![Maturity Level](https://img.shields.io/badge/maturity-release-green.svg)
+
+This is a cross-platform library for interacting with Security Key-type devices via Rust.
+
+* **Supported Platforms**: Windows, Linux, FreeBSD, NetBSD, OpenBSD, and macOS.
+* **Supported Transports**: USB HID.
+* **Supported Protocols**: [FIDO U2F over USB](https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html). [CTAP2 support](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html) is forthcoming, with work being done in the **unstable** [`ctap2` branch](https://github.com/mozilla/authenticator-rs/tree/ctap2).
+
+This library currently focuses on USB security keys, but is expected to be extended to
+support additional transports.
+
+## Usage
+
+There's only a simple example function that tries to register and sign right now. It uses
+[env_logger](http://rust-lang-nursery.github.io/log/env_logger/) for logging, which you
+configure with the `RUST_LOG` environment variable:
+
+```
+cargo build --example main
+RUST_LOG=debug cargo run --example main
+```
+
+Proper usage should be to call into this library from something else - e.g., Firefox. There are
+some [C headers exposed for the purpose](./src/u2fhid-capi.h).
+
+## Tests
+
+There are some tests of the cross-platform runloop logic and the protocol decoder:
+
+```
+cargo test
+```
+
+## Fuzzing
+
+There are fuzzers for the USB protocol reader, basically fuzzing inputs from the HID layer.
+There are not (yet) fuzzers for the C API used by callers (such as Gecko).
+
+To fuzz, you will need cargo-fuzz (the latest version from GitHub) as well as Rust Nightly.
+
+```
+rustup install nightly
+cargo install cargo-fuzz
+
+cargo +nightly fuzz run u2f_read -- -max_len=512
+cargo +nightly fuzz run u2f_read_write -- -max_len=512
+```
diff --git a/third_party/rust/authenticator/build.rs b/third_party/rust/authenticator/build.rs
new file mode 100644
index 0000000000..acc4f09466
--- /dev/null
+++ b/third_party/rust/authenticator/build.rs
@@ -0,0 +1,64 @@
+#[cfg(all(target_os = "linux", feature = "binding-recompile"))]
+extern crate bindgen;
+
+#[cfg(all(target_os = "linux", feature = "binding-recompile"))]
+use std::path::PathBuf;
+
+#[cfg(any(not(target_os = "linux"), not(feature = "binding-recompile")))]
+fn main() {}
+
+#[cfg(all(target_os = "linux", feature = "binding-recompile"))]
+fn main() {
+ let bindings = bindgen::Builder::default()
+ .header("src/transport/linux/hidwrapper.h")
+ .allowlist_var("_HIDIOCGRDESCSIZE")
+ .allowlist_var("_HIDIOCGRDESC")
+ .generate()
+ .expect("Unable to get hidraw bindings");
+
+ let out_path = PathBuf::new();
+ let name = if cfg!(target_arch = "x86") {
+ "ioctl_x86.rs"
+ } else if cfg!(target_arch = "x86_64") {
+ "ioctl_x86_64.rs"
+ } else if cfg!(all(target_arch = "mips", target_endian = "big")) {
+ "ioctl_mipsbe.rs"
+ } else if cfg!(all(target_arch = "mips", target_endian = "little")) {
+ "ioctl_mipsle.rs"
+ } else if cfg!(all(target_arch = "mips64", target_endian = "little")) {
+ "ioctl_mips64le.rs"
+ } else if cfg!(all(target_arch = "powerpc", target_endian = "little")) {
+ "ioctl_powerpcle.rs"
+ } else if cfg!(all(target_arch = "powerpc", target_endian = "big")) {
+ "ioctl_powerpcbe.rs"
+ } else if cfg!(all(target_arch = "powerpc64", target_endian = "little")) {
+ "ioctl_powerpc64le.rs"
+ } else if cfg!(all(target_arch = "powerpc64", target_endian = "big")) {
+ "ioctl_powerpc64be.rs"
+ } else if cfg!(all(target_arch = "arm", target_endian = "little")) {
+ "ioctl_armle.rs"
+ } else if cfg!(all(target_arch = "arm", target_endian = "big")) {
+ "ioctl_armbe.rs"
+ } else if cfg!(all(target_arch = "aarch64", target_endian = "little")) {
+ "ioctl_aarch64le.rs"
+ } else if cfg!(all(target_arch = "aarch64", target_endian = "big")) {
+ "ioctl_aarch64be.rs"
+ } else if cfg!(all(target_arch = "s390x", target_endian = "big")) {
+ "ioctl_s390xbe.rs"
+ } else if cfg!(all(target_arch = "riscv64", target_endian = "little")) {
+ "ioctl_riscv64.rs"
+ } else if cfg!(all(target_arch = "loongarch64", target_endian = "little")) {
+ "ioctl_loongarch64.rs"
+ } else {
+ panic!("architecture not supported");
+ };
+ bindings
+ .write_to_file(
+ out_path
+ .join("src")
+ .join("transport")
+ .join("linux")
+ .join(name),
+ )
+ .expect("Couldn't write hidraw bindings");
+}
diff --git a/third_party/rust/authenticator/examples/ctap2.rs b/third_party/rust/authenticator/examples/ctap2.rs
new file mode 100644
index 0000000000..be742a5e0e
--- /dev/null
+++ b/third_party/rust/authenticator/examples/ctap2.rs
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use authenticator::{
+ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs},
+ crypto::COSEAlgorithm,
+ ctap2::server::{
+ AuthenticationExtensionsClientInputs, CredentialProtectionPolicy,
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
+ PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport,
+ UserVerificationRequirement,
+ },
+ statecallback::StateCallback,
+ Pin, StatusPinUv, StatusUpdate,
+};
+use getopts::Options;
+use rand::{thread_rng, RngCore};
+use std::sync::mpsc::{channel, RecvError};
+use std::{env, thread};
+
+fn print_usage(program: &str, opts: Options) {
+ let brief = format!("Usage: {program} [options]");
+ print!("{}", opts.usage(&brief));
+}
+
+fn main() {
+ env_logger::init();
+
+ let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
+
+ let rp_id = "example.com".to_string();
+ let app_id = "https://fido.example.com/myApp".to_string();
+
+ let mut opts = Options::new();
+ opts.optflag("h", "help", "print this help menu").optopt(
+ "t",
+ "timeout",
+ "timeout in seconds",
+ "SEC",
+ );
+ opts.optflag(
+ "a",
+ "app_id",
+ &format!("Using App ID {app_id} from origin 'https://{rp_id}'"),
+ );
+ opts.optflag("s", "hmac_secret", "With hmac-secret");
+ opts.optflag("h", "help", "print this help menu");
+ opts.optflag("f", "fallback", "Use CTAP1 fallback implementation");
+ let matches = match opts.parse(&args[1..]) {
+ Ok(m) => m,
+ Err(f) => panic!("{}", f.to_string()),
+ };
+ if matches.opt_present("help") {
+ print_usage(&program, opts);
+ return;
+ }
+
+ let using_app_id = matches.opt_present("app_id");
+
+ let mut manager =
+ AuthenticatorService::new().expect("The auth service should initialize safely");
+ manager.add_u2f_usb_hid_platform_transports();
+
+ let fallback = matches.opt_present("fallback");
+
+ let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
+ Ok(timeout_s) => {
+ println!("Using {}s as the timeout", &timeout_s);
+ timeout_s * 1_000
+ }
+ Err(e) => {
+ println!("{e}");
+ print_usage(&program, opts);
+ return;
+ }
+ };
+
+ println!("Asking a security key to register now...");
+ let mut chall_bytes = [0u8; 32];
+ thread_rng().fill_bytes(&mut chall_bytes);
+
+ let (status_tx, status_rx) = channel::<StatusUpdate>();
+ thread::spawn(move || loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::InteractiveManagement(..)) => {
+ panic!("STATUS: This can't happen when doing non-interactive usage");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("STATUS: waiting for user presence");
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
+ println!(
+ "Wrong PIN! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
+ panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
+ panic!("Too many failed attempts. Your device has been blocked. Reset it.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
+ println!(
+ "Wrong UV! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
+ println!("Too many failed UV-attempts.");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(e)) => {
+ panic!("Unexpected error: {:?}", e)
+ }
+ Ok(StatusUpdate::SelectResultNotice(_, _)) => {
+ panic!("Unexpected select device notice")
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let user = PublicKeyCredentialUserEntity {
+ id: "user_id".as_bytes().to_vec(),
+ name: Some("A. User".to_string()),
+ display_name: None,
+ };
+ // If we're testing AppID support, then register with an RP ID that isn't valid for the origin.
+ let relying_party = RelyingParty {
+ id: if using_app_id {
+ app_id.clone()
+ } else {
+ rp_id.clone()
+ },
+ name: None,
+ };
+ let ctap_args = RegisterArgs {
+ client_data_hash: chall_bytes,
+ relying_party,
+ origin: format!("https://{rp_id}"),
+ user,
+ pub_cred_params: vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ],
+ exclude_list: vec![PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+ 0x1c, 0x1d, 0x1e, 0x1f,
+ ],
+ transports: vec![Transport::USB, Transport::NFC],
+ }],
+ user_verification_req: UserVerificationRequirement::Preferred,
+ resident_key_req: ResidentKeyRequirement::Discouraged,
+ extensions: AuthenticationExtensionsClientInputs {
+ cred_props: Some(true),
+ hmac_create_secret: Some(matches.opt_present("hmac_secret")),
+ min_pin_length: Some(true),
+ credential_protection_policy: Some(
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList,
+ ),
+ ..Default::default()
+ },
+ pin: None,
+ use_ctap1_fallback: fallback,
+ };
+
+ let attestation_object;
+ loop {
+ let (register_tx, register_rx) = channel();
+ let callback = StateCallback::new(Box::new(move |rv| {
+ register_tx.send(rv).unwrap();
+ }));
+
+ if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx.clone(), callback) {
+ panic!("Couldn't register: {:?}", e);
+ };
+
+ let register_result = register_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match register_result {
+ Ok(a) => {
+ println!("Ok!");
+ attestation_object = a;
+ break;
+ }
+ Err(e) => panic!("Registration failed: {:?}", e),
+ };
+ }
+
+ println!("Register result: {:?}", &attestation_object);
+
+ println!();
+ println!("*********************************************************************");
+ println!("Asking a security key to sign now, with the data from the register...");
+ println!("*********************************************************************");
+
+ let allow_list;
+ if let Some(cred_data) = attestation_object.att_obj.auth_data.credential_data {
+ allow_list = vec![PublicKeyCredentialDescriptor {
+ id: cred_data.credential_id,
+ transports: vec![Transport::USB],
+ }];
+ } else {
+ allow_list = Vec::new();
+ }
+
+ let ctap_args = SignArgs {
+ client_data_hash: chall_bytes,
+ origin: format!("https://{rp_id}"),
+ relying_party_id: rp_id,
+ allow_list,
+ user_verification_req: UserVerificationRequirement::Preferred,
+ user_presence_req: true,
+ extensions: AuthenticationExtensionsClientInputs {
+ app_id: using_app_id.then(|| app_id.clone()),
+ ..Default::default()
+ },
+ pin: None,
+ use_ctap1_fallback: fallback,
+ };
+
+ loop {
+ let (sign_tx, sign_rx) = channel();
+
+ let callback = StateCallback::new(Box::new(move |rv| {
+ sign_tx.send(rv).unwrap();
+ }));
+
+ if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) {
+ panic!("Couldn't sign: {:?}", e);
+ }
+
+ let sign_result = sign_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+
+ match sign_result {
+ Ok(assertion_object) => {
+ println!("Assertion Object: {assertion_object:?}");
+ if using_app_id {
+ println!(
+ "Used AppID: {}",
+ assertion_object
+ .extensions
+ .app_id
+ .expect("app id extension should be set")
+ );
+ }
+ println!("Done.");
+ break;
+ }
+ Err(e) => panic!("Signing failed: {:?}", e),
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs b/third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs
new file mode 100644
index 0000000000..d19ccc6f9e
--- /dev/null
+++ b/third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs
@@ -0,0 +1,380 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use authenticator::{
+ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs},
+ crypto::COSEAlgorithm,
+ ctap2::server::{
+ AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor,
+ PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty,
+ ResidentKeyRequirement, Transport, UserVerificationRequirement,
+ },
+ statecallback::StateCallback,
+ Pin, StatusPinUv, StatusUpdate,
+};
+use getopts::Options;
+use sha2::{Digest, Sha256};
+use std::sync::mpsc::{channel, RecvError};
+use std::{env, io, thread};
+use std::io::Write;
+
+fn print_usage(program: &str, opts: Options) {
+ println!("------------------------------------------------------------------------");
+ println!("This program registers 3x the same origin with different users and");
+ println!("requests 'discoverable credentials' for them.");
+ println!("After that, we try to log in to that origin and list all credentials found.");
+ println!("------------------------------------------------------------------------");
+ let brief = format!("Usage: {program} [options]");
+ print!("{}", opts.usage(&brief));
+}
+
+fn ask_user_choice(choices: &[PublicKeyCredentialUserEntity]) -> Option<usize> {
+ for (idx, op) in choices.iter().enumerate() {
+ println!("({idx}) \"{}\"", op.name.as_ref().unwrap());
+ }
+ println!("({}) Cancel", choices.len());
+
+ let mut input = String::new();
+ loop {
+ input.clear();
+ print!("Your choice: ");
+ io::stdout()
+ .lock()
+ .flush()
+ .expect("Failed to flush stdout!");
+ io::stdin()
+ .read_line(&mut input)
+ .expect("error: unable to read user input");
+ if let Ok(idx) = input.trim().parse::<usize>() {
+ if idx < choices.len() {
+ // Add a newline in case of success for better separation of in/output
+ println!();
+ return Some(idx);
+ } else if idx == choices.len() {
+ println!();
+ return None;
+ }
+ println!("invalid input");
+ }
+ }
+}
+
+fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) {
+ println!();
+ println!("*********************************************************************");
+ println!("Asking a security key to register now with user: {username}");
+ println!("*********************************************************************");
+
+ println!("Asking a security key to register now...");
+ let challenge_str = format!(
+ "{}{}{}{}",
+ r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
+ r#" "version": "U2F_V2", "appId": "http://example.com", "username": ""#,
+ username,
+ r#""}"#
+ );
+ let mut challenge = Sha256::new();
+ challenge.update(challenge_str.as_bytes());
+ let chall_bytes = challenge.finalize().into();
+
+ let (status_tx, status_rx) = channel::<StatusUpdate>();
+ thread::spawn(move || loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::InteractiveManagement(..)) => {
+ panic!("STATUS: This can't happen when doing non-interactive usage");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("STATUS: waiting for user presence");
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
+ println!(
+ "Wrong PIN! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
+ panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
+ panic!("Too many failed attempts. Your device has been blocked. Reset it.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
+ println!(
+ "Wrong UV! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
+ println!("Too many failed UV-attempts.");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(e)) => {
+ panic!("Unexpected error: {:?}", e)
+ }
+ Ok(StatusUpdate::SelectResultNotice(_, _)) => {
+ panic!("Unexpected select result notice")
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let user = PublicKeyCredentialUserEntity {
+ id: username.as_bytes().to_vec(),
+ name: Some(username.to_string()),
+ display_name: None,
+ };
+ let origin = "https://example.com".to_string();
+ let ctap_args = RegisterArgs {
+ client_data_hash: chall_bytes,
+ relying_party: RelyingParty {
+ id: "example.com".to_string(),
+ name: None,
+ },
+ origin,
+ user,
+ pub_cred_params: vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ],
+ exclude_list: vec![PublicKeyCredentialDescriptor {
+ id: vec![],
+ transports: vec![Transport::USB, Transport::NFC],
+ }],
+ user_verification_req: UserVerificationRequirement::Required,
+ resident_key_req: ResidentKeyRequirement::Required,
+ extensions: AuthenticationExtensionsClientInputs {
+ cred_props: Some(true),
+ ..Default::default()
+ },
+ pin: None,
+ use_ctap1_fallback: false,
+ };
+
+ let attestation_object;
+ loop {
+ let (register_tx, register_rx) = channel();
+ let callback = StateCallback::new(Box::new(move |rv| {
+ register_tx.send(rv).unwrap();
+ }));
+
+ if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx, callback) {
+ panic!("Couldn't register: {:?}", e);
+ };
+
+ let register_result = register_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match register_result {
+ Ok(a) => {
+ println!("Ok!");
+ attestation_object = a;
+ break;
+ }
+ Err(e) => panic!("Registration failed: {:?}", e),
+ };
+ }
+
+ println!("Register result: {:?}", &attestation_object);
+}
+
+fn main() {
+ env_logger::init();
+
+ let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
+
+ let mut opts = Options::new();
+ opts.optflag("h", "help", "print this help menu").optopt(
+ "t",
+ "timeout",
+ "timeout in seconds",
+ "SEC",
+ );
+ opts.optflag(
+ "s",
+ "skip_reg",
+ "Skip registration");
+
+ opts.optflag("h", "help", "print this help menu");
+ let matches = match opts.parse(&args[1..]) {
+ Ok(m) => m,
+ Err(f) => panic!("{}", f.to_string()),
+ };
+ if matches.opt_present("help") {
+ print_usage(&program, opts);
+ return;
+ }
+
+ let mut manager =
+ AuthenticatorService::new().expect("The auth service should initialize safely");
+ manager.add_u2f_usb_hid_platform_transports();
+
+ let timeout_ms = match matches.opt_get_default::<u64>("timeout", 15) {
+ Ok(timeout_s) => {
+ println!("Using {}s as the timeout", &timeout_s);
+ timeout_s * 1_000
+ }
+ Err(e) => {
+ println!("{e}");
+ print_usage(&program, opts);
+ return;
+ }
+ };
+
+ if !matches.opt_present("skip_reg") {
+ for username in &["A. User", "A. Nother", "Dr. Who"] {
+ register_user(&mut manager, username, timeout_ms)
+ }
+ }
+
+ println!();
+ println!("*********************************************************************");
+ println!("Asking a security key to sign now, with the data from the register...");
+ println!("*********************************************************************");
+
+ // Discovering creds:
+ let allow_list = Vec::new();
+ let origin = "https://example.com".to_string();
+ let challenge_str = format!(
+ "{}{}",
+ r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
+ r#" "version": "U2F_V2", "appId": "http://example.com" "#,
+ );
+
+ let (status_tx, status_rx) = channel::<StatusUpdate>();
+ thread::spawn(move || loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::InteractiveManagement(..)) => {
+ panic!("STATUS: This can't happen when doing non-interactive usage");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("STATUS: waiting for user presence");
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
+ println!(
+ "Wrong PIN! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
+ panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
+ panic!("Too many failed attempts. Your device has been blocked. Reset it.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
+ println!(
+ "Wrong UV! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
+ println!("Too many failed UV-attempts.");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(e)) => {
+ panic!("Unexpected error: {:?}", e)
+ }
+ Ok(StatusUpdate::SelectResultNotice(index_sender, users)) => {
+ println!("Multiple signatures returned. Select one or cancel.");
+ let idx = ask_user_choice(&users);
+ index_sender.send(idx).expect("Failed to send choice");
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let mut challenge = Sha256::new();
+ challenge.update(challenge_str.as_bytes());
+ let chall_bytes = challenge.finalize().into();
+ let ctap_args = SignArgs {
+ client_data_hash: chall_bytes,
+ origin,
+ relying_party_id: "example.com".to_string(),
+ allow_list,
+ user_verification_req: UserVerificationRequirement::Required,
+ user_presence_req: true,
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ };
+
+ loop {
+ let (sign_tx, sign_rx) = channel();
+
+ let callback = StateCallback::new(Box::new(move |rv| {
+ sign_tx.send(rv).unwrap();
+ }));
+
+ if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) {
+ panic!("Couldn't sign: {:?}", e);
+ }
+
+ let sign_result = sign_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+
+ match sign_result {
+ Ok(assertion_object) => {
+ println!("Assertion Object: {assertion_object:?}");
+ println!("-----------------------------------------------------------------");
+ println!("Found credentials:");
+ println!(
+ "{:?}",
+ assertion_object.assertion.user.clone().unwrap().name.unwrap() // Unwrapping here, as these shouldn't fail
+ );
+ println!("-----------------------------------------------------------------");
+ println!("Done.");
+ break;
+ }
+ Err(e) => panic!("Signing failed: {:?}", e),
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/examples/interactive_management.rs b/third_party/rust/authenticator/examples/interactive_management.rs
new file mode 100644
index 0000000000..6ef0e9dcc3
--- /dev/null
+++ b/third_party/rust/authenticator/examples/interactive_management.rs
@@ -0,0 +1,810 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use authenticator::{
+ authenticatorservice::AuthenticatorService,
+ ctap2::commands::{
+ authenticator_config::{AuthConfigCommand, AuthConfigResult, SetMinPINLength},
+ bio_enrollment::EnrollmentInfo,
+ credential_management::CredentialList,
+ PinUvAuthResult,
+ },
+ errors::AuthenticatorError,
+ statecallback::StateCallback,
+ AuthenticatorInfo, BioEnrollmentCmd, BioEnrollmentResult, CredManagementCmd,
+ CredentialManagementResult, InteractiveRequest, InteractiveUpdate, ManageResult, Pin,
+ StatusPinUv, StatusUpdate,
+};
+use getopts::Options;
+use log::debug;
+use std::{env, io, sync::mpsc::Sender, thread};
+use std::{
+ fmt::Display,
+ io::Write,
+ sync::mpsc::{channel, Receiver, RecvError},
+};
+
+#[derive(Debug, PartialEq, Clone)]
+enum PinOperation {
+ Set,
+ Change,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum ConfigureOperation {
+ ToggleAlwaysUV,
+ EnableEnterpriseAttestation,
+ SetMinPINLength,
+}
+
+impl ConfigureOperation {
+ fn ask_user(info: &AuthenticatorInfo) -> Self {
+ let sub_level = Self::parse_possible_operations(info);
+ println!();
+ println!("What do you wish to do?");
+ let choice = ask_user_choice(&sub_level);
+ sub_level[choice].clone()
+ }
+
+ fn parse_possible_operations(info: &AuthenticatorInfo) -> Vec<Self> {
+ let mut configure_ops = vec![];
+ if info.options.authnr_cfg == Some(true) && info.options.always_uv.is_some() {
+ configure_ops.push(ConfigureOperation::ToggleAlwaysUV);
+ }
+ if info.options.authnr_cfg == Some(true) && info.options.set_min_pin_length.is_some() {
+ configure_ops.push(ConfigureOperation::SetMinPINLength);
+ }
+ if info.options.ep.is_some() {
+ configure_ops.push(ConfigureOperation::EnableEnterpriseAttestation);
+ }
+ configure_ops
+ }
+}
+
+impl Display for ConfigureOperation {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ConfigureOperation::ToggleAlwaysUV => write!(f, "Toggle option 'Always UV'"),
+ ConfigureOperation::EnableEnterpriseAttestation => {
+ write!(f, "Enable Enterprise attestation")
+ }
+ ConfigureOperation::SetMinPINLength => write!(f, "Set min. PIN length"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum CredentialsOperation {
+ List,
+ Delete,
+ UpdateUser,
+}
+
+impl CredentialsOperation {
+ fn ask_user(info: &AuthenticatorInfo, creds: &CredentialList) -> Self {
+ let sub_level = Self::parse_possible_operations(info, creds);
+ println!();
+ println!("What do you wish to do?");
+ let choice = ask_user_choice(&sub_level);
+ sub_level[choice].clone()
+ }
+
+ fn parse_possible_operations(info: &AuthenticatorInfo, creds: &CredentialList) -> Vec<Self> {
+ let mut credentials_ops = vec![];
+ if info.options.cred_mgmt == Some(true)
+ || info.options.credential_mgmt_preview == Some(true)
+ {
+ credentials_ops.push(CredentialsOperation::List);
+ }
+ if creds.existing_resident_credentials_count > 0 {
+ credentials_ops.push(CredentialsOperation::Delete);
+ // FIDO_2_1_PRE devices do not (all?) support UpdateUser.
+ // So we require devices to support full 2.1 for this.
+ if info
+ .versions
+ .contains(&authenticator::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_1)
+ {
+ credentials_ops.push(CredentialsOperation::UpdateUser);
+ }
+ }
+ credentials_ops
+ }
+}
+
+impl Display for CredentialsOperation {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ CredentialsOperation::List => write!(f, "List credentials"),
+ CredentialsOperation::Delete => write!(f, "Delete credentials"),
+ CredentialsOperation::UpdateUser => write!(f, "Update user info"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum BioOperation {
+ ShowInfo,
+ Add,
+ List,
+ Delete,
+ Rename,
+}
+
+impl BioOperation {
+ fn ask_user(info: &AuthenticatorInfo) -> Self {
+ let sub_level = Self::parse_possible_operations(info);
+ println!();
+ println!("What do you wish to do?");
+ let choice = ask_user_choice(&sub_level);
+ sub_level[choice].clone()
+ }
+
+ fn parse_possible_operations(info: &AuthenticatorInfo) -> Vec<Self> {
+ let mut bio_ops = vec![];
+ if info.options.bio_enroll.is_some()
+ || info.options.user_verification_mgmt_preview.is_some()
+ {
+ bio_ops.extend([BioOperation::ShowInfo, BioOperation::Add]);
+ }
+ if info.options.bio_enroll == Some(true)
+ || info.options.user_verification_mgmt_preview == Some(true)
+ {
+ bio_ops.extend([
+ BioOperation::Delete,
+ BioOperation::List,
+ BioOperation::Rename,
+ ]);
+ }
+ bio_ops
+ }
+}
+
+impl Display for BioOperation {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ BioOperation::ShowInfo => write!(f, "Show fingerprint sensor info"),
+ BioOperation::List => write!(f, "List enrollments"),
+ BioOperation::Add => write!(f, "Add enrollment"),
+ BioOperation::Delete => write!(f, "Delete enrollment"),
+ BioOperation::Rename => write!(f, "Rename enrollment"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum TopLevelOperation {
+ Quit,
+ Reset,
+ ShowFullInfo,
+ Pin(PinOperation),
+ Configure,
+ Credentials,
+ Bio,
+}
+
+impl Display for TopLevelOperation {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ TopLevelOperation::Quit => write!(f, "Quit"),
+ TopLevelOperation::Reset => write!(f, "Reset"),
+ TopLevelOperation::ShowFullInfo => write!(f, "Show full info"),
+ TopLevelOperation::Pin(PinOperation::Change) => write!(f, "Change Pin"),
+ TopLevelOperation::Pin(PinOperation::Set) => write!(f, "Set Pin"),
+ TopLevelOperation::Configure => write!(f, "Configure Authenticator"),
+ TopLevelOperation::Credentials => write!(f, "Manage Credentials"),
+ TopLevelOperation::Bio => write!(f, "Manage BioEnrollments"),
+ }
+ }
+}
+
+impl TopLevelOperation {
+ fn ask_user(info: &AuthenticatorInfo) -> TopLevelOperation {
+ let top_level = Self::parse_possible_operations(info);
+ println!();
+ println!("What do you wish to do?");
+ let choice = ask_user_choice(&top_level);
+ top_level[choice].clone()
+ }
+
+ fn parse_possible_operations(info: &AuthenticatorInfo) -> Vec<TopLevelOperation> {
+ let mut ops = vec![
+ TopLevelOperation::Quit,
+ TopLevelOperation::Reset,
+ TopLevelOperation::ShowFullInfo,
+ ];
+
+ // PIN-related
+ match info.options.client_pin {
+ None => {}
+ Some(true) => ops.push(TopLevelOperation::Pin(PinOperation::Change)),
+ Some(false) => ops.push(TopLevelOperation::Pin(PinOperation::Set)),
+ }
+
+ // Authenticator-Configuration
+ if info.options.authnr_cfg == Some(true)
+ && (info.options.always_uv.is_some()
+ || info.options.set_min_pin_length.is_some()
+ || info.options.ep.is_some())
+ {
+ ops.push(TopLevelOperation::Configure);
+ }
+
+ // Credential Management
+ if info.options.cred_mgmt == Some(true)
+ || info.options.credential_mgmt_preview == Some(true)
+ {
+ ops.push(TopLevelOperation::Credentials);
+ }
+
+ // Bio Enrollment
+ if info.options.bio_enroll.is_some()
+ || info.options.user_verification_mgmt_preview.is_some()
+ {
+ ops.push(TopLevelOperation::Bio);
+ }
+
+ ops
+ }
+}
+
+fn print_usage(program: &str, opts: Options) {
+ let brief = format!("Usage: {program} [options]");
+ print!("{}", opts.usage(&brief));
+}
+
+fn ask_user_choice<T: Display>(choices: &[T]) -> usize {
+ for (idx, op) in choices.iter().enumerate() {
+ println!("({idx}) {op}");
+ }
+
+ let mut input = String::new();
+ loop {
+ input.clear();
+ print!("Your choice: ");
+ io::stdout()
+ .lock()
+ .flush()
+ .expect("Failed to flush stdout!");
+ io::stdin()
+ .read_line(&mut input)
+ .expect("error: unable to read user input");
+ if let Ok(idx) = input.trim().parse::<usize>() {
+ if idx < choices.len() {
+ // Add a newline in case of success for better separation of in/output
+ println!();
+ return idx;
+ }
+ }
+ }
+}
+
+fn handle_authenticator_config(
+ res: Option<AuthConfigResult>,
+ puat: Option<PinUvAuthResult>,
+ auth_info: &mut Option<AuthenticatorInfo>,
+ tx: &Sender<InteractiveRequest>,
+) {
+ if let Some(AuthConfigResult::Success(info)) = res {
+ println!("{:#?}", info.options);
+ *auth_info = Some(info);
+ }
+ let choice = ConfigureOperation::ask_user(auth_info.as_ref().unwrap());
+ match choice {
+ ConfigureOperation::ToggleAlwaysUV => {
+ tx.send(InteractiveRequest::ChangeConfig(
+ AuthConfigCommand::ToggleAlwaysUv,
+ puat,
+ ))
+ .expect("Failed to send ToggleAlwaysUV request.");
+ }
+ ConfigureOperation::EnableEnterpriseAttestation => {
+ tx.send(InteractiveRequest::ChangeConfig(
+ AuthConfigCommand::EnableEnterpriseAttestation,
+ puat,
+ ))
+ .expect("Failed to send EnableEnterpriseAttestation request.");
+ }
+ ConfigureOperation::SetMinPINLength => {
+ let mut length = String::new();
+ while length.trim().parse::<u64>().is_err() {
+ length.clear();
+ print!("New minimum PIN length: ");
+ io::stdout()
+ .lock()
+ .flush()
+ .expect("Failed to flush stdout!");
+ io::stdin()
+ .read_line(&mut length)
+ .expect("error: unable to read user input");
+ }
+ let new_length = length.trim().parse::<u64>().unwrap();
+ let cmd = SetMinPINLength {
+ new_min_pin_length: Some(new_length),
+ min_pin_length_rpids: None,
+ force_change_pin: None,
+ };
+
+ tx.send(InteractiveRequest::ChangeConfig(
+ AuthConfigCommand::SetMinPINLength(cmd),
+ puat,
+ ))
+ .expect("Failed to send SetMinPINLength request.");
+ }
+ }
+}
+
+fn handle_credential_management(
+ res: Option<CredentialManagementResult>,
+ puat: Option<PinUvAuthResult>,
+ auth_info: &mut Option<AuthenticatorInfo>,
+ tx: &Sender<InteractiveRequest>,
+) {
+ match res {
+ Some(CredentialManagementResult::CredentialList(credlist)) => {
+ let mut creds = vec![];
+ for rp in &credlist.credential_list {
+ for cred in &rp.credentials {
+ creds.push((
+ rp.rp.name.clone(),
+ cred.user.clone(),
+ cred.credential_id.clone(),
+ ));
+ }
+ }
+ let display_creds: Vec<_> = creds
+ .iter()
+ .map(|(rp, user, id)| format!("{:?} - {:?} - {:?}", rp, user, id))
+ .collect();
+ for (idx, op) in display_creds.iter().enumerate() {
+ println!("({idx}) {op}");
+ }
+
+ loop {
+ match CredentialsOperation::ask_user(auth_info.as_ref().unwrap(), &credlist) {
+ CredentialsOperation::List => {
+ let mut idx = 0;
+ for rp in credlist.credential_list.iter() {
+ for cred in &rp.credentials {
+ println!("({idx}) - {:?}: {:?}", rp.rp.name, cred);
+ idx += 1;
+ }
+ }
+ continue;
+ }
+ CredentialsOperation::Delete => {
+ let choice = ask_user_choice(&display_creds);
+ tx.send(InteractiveRequest::CredentialManagement(
+ CredManagementCmd::DeleteCredential(creds[choice].2.clone()),
+ puat,
+ ))
+ .expect("Failed to send DeleteCredentials request.");
+ break;
+ }
+ CredentialsOperation::UpdateUser => {
+ let choice = ask_user_choice(&display_creds);
+ // Updating username. Asking for the new one.
+ let mut input = String::new();
+ print!("New username: ");
+ io::stdout()
+ .lock()
+ .flush()
+ .expect("Failed to flush stdout!");
+ io::stdin()
+ .read_line(&mut input)
+ .expect("error: unable to read user input");
+ input = input.trim().to_string();
+ let name = if input.is_empty() { None } else { Some(input) };
+ let mut new_user = creds[choice].1.clone();
+ new_user.name = name;
+ tx.send(InteractiveRequest::CredentialManagement(
+ CredManagementCmd::UpdateUserInformation(
+ creds[choice].2.clone(),
+ new_user,
+ ),
+ puat,
+ ))
+ .expect("Failed to send UpdateUserinformation request.");
+ break;
+ }
+ }
+ }
+ }
+ None
+ | Some(CredentialManagementResult::DeleteSucess)
+ | Some(CredentialManagementResult::UpdateSuccess) => {
+ tx.send(InteractiveRequest::CredentialManagement(
+ CredManagementCmd::GetCredentials,
+ puat,
+ ))
+ .expect("Failed to send GetCredentials request.");
+ }
+ }
+}
+
+fn ask_user_bio_options(
+ biolist: Vec<EnrollmentInfo>,
+ puat: Option<PinUvAuthResult>,
+ auth_info: &mut Option<AuthenticatorInfo>,
+ tx: &Sender<InteractiveRequest>,
+) {
+ let display_bios: Vec<_> = biolist
+ .iter()
+ .map(|x| format!("{:?}: {:?}", x.template_friendly_name, x.template_id))
+ .collect();
+ loop {
+ match BioOperation::ask_user(auth_info.as_ref().unwrap()) {
+ BioOperation::List => {
+ for (idx, bio) in display_bios.iter().enumerate() {
+ println!("({idx}) - {bio}");
+ }
+ continue;
+ }
+ BioOperation::ShowInfo => {
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::GetFingerprintSensorInfo,
+ puat,
+ ))
+ .expect("Failed to send GetFingerprintSensorInfo request.");
+ break;
+ }
+ BioOperation::Delete => {
+ let choice = ask_user_choice(&display_bios);
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::DeleteEnrollment(biolist[choice].template_id.clone()),
+ puat,
+ ))
+ .expect("Failed to send GetEnrollments request.");
+ break;
+ }
+ BioOperation::Rename => {
+ let choice = ask_user_choice(&display_bios);
+ let chosen_id = biolist[choice].template_id.clone();
+ // Updating enrollment name. Asking for the new one.
+ let mut input = String::new();
+ print!("New name: ");
+ io::stdout()
+ .lock()
+ .flush()
+ .expect("Failed to flush stdout!");
+ io::stdin()
+ .read_line(&mut input)
+ .expect("error: unable to read user input");
+ let name = input.trim().to_string();
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::ChangeName(chosen_id, name),
+ puat,
+ ))
+ .expect("Failed to send GetEnrollments request.");
+ break;
+ }
+ BioOperation::Add => {
+ let mut input = String::new();
+ print!(
+ "The name of the new bio enrollment (leave empty if you don't want to name it): "
+ );
+ io::stdout()
+ .lock()
+ .flush()
+ .expect("Failed to flush stdout!");
+ io::stdin()
+ .read_line(&mut input)
+ .expect("error: unable to read user input");
+ input = input.trim().to_string();
+ let name = if input.is_empty() { None } else { Some(input) };
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::StartNewEnrollment(name),
+ puat,
+ ))
+ .expect("Failed to send StartNewEnrollment request.");
+ break;
+ }
+ }
+ }
+}
+
+fn handle_bio_enrollments(
+ res: Option<BioEnrollmentResult>,
+ puat: Option<PinUvAuthResult>,
+ auth_info: &mut Option<AuthenticatorInfo>,
+ tx: &Sender<InteractiveRequest>,
+) {
+ match res {
+ Some(BioEnrollmentResult::EnrollmentList(biolist)) => {
+ ask_user_bio_options(biolist, puat, auth_info, tx);
+ }
+ None => {
+ if BioOperation::parse_possible_operations(auth_info.as_ref().unwrap())
+ .contains(&BioOperation::List)
+ {
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::GetEnrollments,
+ puat,
+ ))
+ .expect("Failed to send GetEnrollments request.");
+ } else {
+ ask_user_bio_options(vec![], puat, auth_info, tx);
+ }
+ }
+ Some(BioEnrollmentResult::UpdateSuccess) => {
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::GetEnrollments,
+ puat,
+ ))
+ .expect("Failed to send GetEnrollments request.");
+ }
+ Some(BioEnrollmentResult::DeleteSuccess(info)) => {
+ *auth_info = Some(info.clone());
+ if BioOperation::parse_possible_operations(&info).contains(&BioOperation::List) {
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::GetEnrollments,
+ puat,
+ ))
+ .expect("Failed to send GetEnrollments request.");
+ } else {
+ ask_user_bio_options(vec![], puat, auth_info, tx);
+ }
+ }
+ Some(BioEnrollmentResult::AddSuccess(info)) => {
+ *auth_info = Some(info);
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::GetEnrollments,
+ puat,
+ ))
+ .expect("Failed to send GetEnrollments request.");
+ }
+ Some(BioEnrollmentResult::FingerprintSensorInfo(info)) => {
+ println!("{info:#?}");
+ if BioOperation::parse_possible_operations(auth_info.as_ref().unwrap())
+ .contains(&BioOperation::List)
+ {
+ tx.send(InteractiveRequest::BioEnrollment(
+ BioEnrollmentCmd::GetEnrollments,
+ puat,
+ ))
+ .expect("Failed to send GetEnrollments request.");
+ } else {
+ ask_user_bio_options(vec![], puat, auth_info, tx);
+ }
+ }
+ Some(BioEnrollmentResult::SampleStatus(last_sample_status, remaining_samples)) => {
+ println!("Last sample status: {last_sample_status:?}, remaining samples: {remaining_samples:?}");
+ }
+ }
+}
+
+fn interactive_status_callback(status_rx: Receiver<StatusUpdate>) {
+ let mut tx = None;
+ let mut auth_info = None;
+ loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::StartManagement((
+ tx_new,
+ auth_info_new,
+ )))) => {
+ let info = match auth_info_new {
+ Some(info) => info,
+ None => {
+ println!("Device only supports CTAP1 and can't be managed.");
+ return;
+ }
+ };
+ auth_info = Some(info.clone());
+ tx = Some(tx_new);
+ match TopLevelOperation::ask_user(&info) {
+ TopLevelOperation::Quit => {
+ tx.as_ref()
+ .unwrap()
+ .send(InteractiveRequest::Quit)
+ .expect("Failed to send PIN-set request");
+ }
+ TopLevelOperation::Reset => tx
+ .as_ref()
+ .unwrap()
+ .send(InteractiveRequest::Reset)
+ .expect("Failed to send Reset request."),
+ TopLevelOperation::ShowFullInfo => {
+ println!("Authenticator Info {:#?}", info);
+ return;
+ }
+ TopLevelOperation::Pin(op) => {
+ let raw_new_pin = rpassword::prompt_password_stderr("Enter new PIN: ")
+ .expect("Failed to read PIN");
+ let new_pin = Pin::new(&raw_new_pin);
+ if op == PinOperation::Change {
+ let raw_curr_pin =
+ rpassword::prompt_password_stderr("Enter current PIN: ")
+ .expect("Failed to read PIN");
+ let curr_pin = Pin::new(&raw_curr_pin);
+ tx.as_ref()
+ .unwrap()
+ .send(InteractiveRequest::ChangePIN(curr_pin, new_pin))
+ .expect("Failed to send PIN-change request");
+ } else {
+ tx.as_ref()
+ .unwrap()
+ .send(InteractiveRequest::SetPIN(new_pin))
+ .expect("Failed to send PIN-set request");
+ }
+ }
+ TopLevelOperation::Configure => handle_authenticator_config(
+ None,
+ None,
+ &mut auth_info,
+ tx.as_ref().unwrap(),
+ ),
+ TopLevelOperation::Credentials => handle_credential_management(
+ None,
+ None,
+ &mut auth_info,
+ tx.as_ref().unwrap(),
+ ),
+ TopLevelOperation::Bio => {
+ handle_bio_enrollments(None, None, &mut auth_info, tx.as_ref().unwrap())
+ }
+ }
+ continue;
+ }
+ Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::AuthConfigUpdate((
+ cfg_result,
+ puat_res,
+ )))) => {
+ handle_authenticator_config(
+ Some(cfg_result),
+ puat_res,
+ &mut auth_info,
+ tx.as_ref().unwrap(),
+ );
+ continue;
+ }
+ Ok(StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::CredentialManagementUpdate((cfg_result, puat_res)),
+ )) => {
+ handle_credential_management(
+ Some(cfg_result),
+ puat_res,
+ &mut auth_info,
+ tx.as_ref().unwrap(),
+ );
+ continue;
+ }
+ Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::BioEnrollmentUpdate((
+ bio_res,
+ puat_res,
+ )))) => {
+ handle_bio_enrollments(
+ Some(bio_res),
+ puat_res,
+ &mut auth_info,
+ tx.as_ref().unwrap(),
+ );
+ continue;
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
+ println!(
+ "Wrong PIN! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
+ panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
+ panic!("Too many failed attempts. Your device has been blocked. Reset it.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
+ println!(
+ "Wrong UV! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
+ println!("Too many failed UV-attempts.");
+ continue;
+ }
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("Please touch your device!");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(e)) => {
+ panic!("Unexpected error: {:?}", e)
+ }
+ Ok(StatusUpdate::SelectResultNotice(_, _)) => {
+ panic!("Unexpected select device notice")
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ }
+}
+
+fn main() {
+ env_logger::init();
+
+ let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
+
+ let mut opts = Options::new();
+ opts.optflag("h", "help", "print this help menu").optopt(
+ "t",
+ "timeout",
+ "timeout in seconds",
+ "SEC",
+ );
+ opts.optflag("h", "help", "print this help menu");
+ let matches = match opts.parse(&args[1..]) {
+ Ok(m) => m,
+ Err(f) => panic!("{}", f.to_string()),
+ };
+ if matches.opt_present("help") {
+ print_usage(&program, opts);
+ return;
+ }
+
+ let mut manager =
+ AuthenticatorService::new().expect("The auth service should initialize safely");
+ manager.add_u2f_usb_hid_platform_transports();
+
+ let timeout_ms = match matches.opt_get_default::<u64>("timeout", 120) {
+ Ok(timeout_s) => {
+ println!("Using {}s as the timeout", &timeout_s);
+ timeout_s * 1_000
+ }
+ Err(e) => {
+ println!("{e}");
+ print_usage(&program, opts);
+ return;
+ }
+ };
+
+ let (status_tx, status_rx) = channel::<StatusUpdate>();
+ thread::spawn(move || interactive_status_callback(status_rx));
+
+ let (manage_tx, manage_rx) = channel();
+ let state_callback =
+ StateCallback::<Result<ManageResult, AuthenticatorError>>::new(Box::new(move |rv| {
+ manage_tx.send(rv).unwrap();
+ }));
+
+ match manager.manage(timeout_ms, status_tx, state_callback) {
+ Ok(_) => {
+ debug!("Started management");
+ }
+ Err(e) => {
+ println!("Error! Failed to start interactive management: {:?}", e);
+ return;
+ }
+ }
+ let manage_result = manage_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match manage_result {
+ Ok(r) => {
+ println!("Success! Result = {r:?}");
+ }
+ Err(e) => {
+ println!("Error! {:?}", e);
+ }
+ };
+ println!("Done");
+}
diff --git a/third_party/rust/authenticator/examples/reset.rs b/third_party/rust/authenticator/examples/reset.rs
new file mode 100644
index 0000000000..867ab5448f
--- /dev/null
+++ b/third_party/rust/authenticator/examples/reset.rs
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use authenticator::{
+ authenticatorservice::AuthenticatorService,
+ ctap2::commands::StatusCode,
+ errors::{AuthenticatorError, CommandError, HIDError},
+ statecallback::StateCallback,
+ StatusUpdate,
+};
+use getopts::Options;
+use std::env;
+use std::sync::mpsc::{channel, RecvError};
+
+fn print_usage(program: &str, opts: Options) {
+ let brief = format!("Usage: {program} [options]");
+ print!("{}", opts.usage(&brief));
+}
+
+fn main() {
+ env_logger::init();
+
+ let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
+
+ let mut opts = Options::new();
+ opts.optflag("h", "help", "print this help menu").optopt(
+ "t",
+ "timeout",
+ "timeout in seconds",
+ "SEC",
+ );
+ opts.optflag("h", "help", "print this help menu");
+ let matches = match opts.parse(&args[1..]) {
+ Ok(m) => m,
+ Err(f) => panic!("{}", f.to_string()),
+ };
+ if matches.opt_present("help") {
+ print_usage(&program, opts);
+ return;
+ }
+
+ let mut manager =
+ AuthenticatorService::new().expect("The auth service should initialize safely");
+ manager.add_u2f_usb_hid_platform_transports();
+
+ let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
+ Ok(timeout_s) => {
+ println!("Using {}s as the timeout", &timeout_s);
+ timeout_s * 1_000
+ }
+ Err(e) => {
+ println!("{e}");
+ print_usage(&program, opts);
+ return;
+ }
+ };
+
+ println!(
+ "NOTE: Please unplug all devices, type in 'yes' and plug in the device that should be reset."
+ );
+ loop {
+ let mut s = String::new();
+ println!("ATTENTION: Resetting a device will wipe all credentials! Do you wish to continue? [yes/N]");
+ std::io::stdin()
+ .read_line(&mut s)
+ .expect("Did not enter a correct string");
+ let trimmed = s.trim();
+ if trimmed.is_empty() || trimmed == "N" || trimmed == "n" {
+ println!("Exiting without reset.");
+ return;
+ }
+ if trimmed == "y" {
+ println!("Please type in the whole word 'yes'");
+ continue;
+ }
+ if trimmed == "yes" {
+ break;
+ }
+ }
+
+ let (status_tx, status_rx) = channel::<StatusUpdate>();
+ let (reset_tx, reset_rx) = channel();
+ let rs_tx = reset_tx;
+ let callback = StateCallback::new(Box::new(move |rv| {
+ let _ = rs_tx.send(rv);
+ }));
+
+ if let Err(e) = manager.reset(timeout_ms, status_tx, callback) {
+ panic!("Couldn't register: {:?}", e);
+ };
+
+ loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("ERROR: Please unplug all other tokens that should not be reset!");
+ // Needed to give the tokens enough time to start blinking
+ // otherwise we may cancel pre-maturely and this binary will hang
+ std::thread::sleep(std::time::Duration::from_millis(200));
+ manager.cancel().unwrap();
+ return;
+ }
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("STATUS: waiting for user presence");
+ break;
+ }
+ Ok(StatusUpdate::PinUvError(..)) => panic!("Reset should never ask for a PIN!"),
+ Ok(_) => { /* Ignore all other updates */ }
+ Err(RecvError) => {
+ println!("RecvError");
+ return;
+ }
+ }
+ }
+
+ let reset_result = reset_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match reset_result {
+ Ok(()) => {
+ println!("Token successfully reset!");
+ }
+ Err(AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NotAllowed,
+ _,
+ )))) => {
+ println!("Resetting is only allowed within the first 10 seconds after powering up.");
+ println!("Please unplug your device, plug it back in and try again.");
+ }
+ Err(e) => panic!("Reset failed: {:?}", e),
+ };
+}
diff --git a/third_party/rust/authenticator/examples/set_pin.rs b/third_party/rust/authenticator/examples/set_pin.rs
new file mode 100644
index 0000000000..5534ca08fd
--- /dev/null
+++ b/third_party/rust/authenticator/examples/set_pin.rs
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use authenticator::{
+ authenticatorservice::AuthenticatorService, statecallback::StateCallback, Pin, StatusPinUv,
+ StatusUpdate,
+};
+use getopts::Options;
+use std::sync::mpsc::{channel, RecvError};
+use std::{env, thread};
+
+fn print_usage(program: &str, opts: Options) {
+ let brief = format!("Usage: {program} [options]");
+ print!("{}", opts.usage(&brief));
+}
+
+fn main() {
+ env_logger::init();
+
+ let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
+
+ let mut opts = Options::new();
+ opts.optflag("h", "help", "print this help menu").optopt(
+ "t",
+ "timeout",
+ "timeout in seconds",
+ "SEC",
+ );
+ opts.optflag("h", "help", "print this help menu");
+ let matches = match opts.parse(&args[1..]) {
+ Ok(m) => m,
+ Err(f) => panic!("{}", f.to_string()),
+ };
+ if matches.opt_present("help") {
+ print_usage(&program, opts);
+ return;
+ }
+
+ let mut manager =
+ AuthenticatorService::new().expect("The auth service should initialize safely");
+ manager.add_u2f_usb_hid_platform_transports();
+
+ let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
+ Ok(timeout_s) => {
+ println!("Using {}s as the timeout", &timeout_s);
+ timeout_s * 1_000
+ }
+ Err(e) => {
+ println!("{e}");
+ print_usage(&program, opts);
+ return;
+ }
+ };
+
+ let new_pin = rpassword::prompt_password_stderr("Enter new PIN: ").expect("Failed to read PIN");
+ let repeat_new_pin =
+ rpassword::prompt_password_stderr("Enter it again: ").expect("Failed to read PIN");
+ if new_pin != repeat_new_pin {
+ println!("PINs did not match!");
+ return;
+ }
+
+ let (status_tx, status_rx) = channel::<StatusUpdate>();
+ thread::spawn(move || loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::InteractiveManagement(..)) => {
+ panic!("STATUS: This can't happen when doing non-interactive usage");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("STATUS: waiting for user presence");
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
+ println!(
+ "Wrong PIN! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
+ panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
+ panic!("Too many failed attempts. Your device has been blocked. Reset it.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
+ println!(
+ "Wrong UV! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
+ println!("Too many failed UV-attempts.");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(e)) => {
+ panic!("Unexpected error: {:?}", e)
+ }
+ Ok(StatusUpdate::SelectResultNotice(_, _)) => {
+ panic!("Unexpected select device notice")
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let (reset_tx, reset_rx) = channel();
+ let rs_tx = reset_tx;
+ let callback = StateCallback::new(Box::new(move |rv| {
+ let _ = rs_tx.send(rv);
+ }));
+
+ if let Err(e) = manager.set_pin(timeout_ms, Pin::new(&new_pin), status_tx, callback) {
+ panic!("Couldn't call set_pin: {:?}", e);
+ };
+
+ let reset_result = reset_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match reset_result {
+ Ok(()) => {
+ println!("PIN successfully set!");
+ }
+ Err(e) => panic!("Setting PIN failed: {:?}", e),
+ };
+}
diff --git a/third_party/rust/authenticator/examples/test_exclude_list.rs b/third_party/rust/authenticator/examples/test_exclude_list.rs
new file mode 100644
index 0000000000..e24c49d05a
--- /dev/null
+++ b/third_party/rust/authenticator/examples/test_exclude_list.rs
@@ -0,0 +1,309 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use authenticator::{
+ authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs},
+ crypto::COSEAlgorithm,
+ ctap2::commands::StatusCode,
+ ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
+ PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport,
+ UserVerificationRequirement,
+ },
+ errors::{AuthenticatorError, CommandError, HIDError, UnsupportedOption},
+ statecallback::StateCallback,
+ Pin, StatusPinUv, StatusUpdate,
+};
+
+use getopts::Options;
+use sha2::{Digest, Sha256};
+use std::sync::mpsc::{channel, RecvError};
+use std::{env, thread};
+
+fn print_usage(program: &str, opts: Options) {
+ let brief = format!("Usage: {program} [options]");
+ print!("{}", opts.usage(&brief));
+}
+
+fn main() {
+ env_logger::init();
+
+ let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
+
+ let mut opts = Options::new();
+ opts.optflag("h", "help", "print this help menu").optopt(
+ "t",
+ "timeout",
+ "timeout in seconds",
+ "SEC",
+ );
+ opts.optflag("h", "help", "print this help menu");
+ let matches = match opts.parse(&args[1..]) {
+ Ok(m) => m,
+ Err(f) => panic!("{}", f.to_string()),
+ };
+ if matches.opt_present("help") {
+ print_usage(&program, opts);
+ return;
+ }
+
+ let mut manager =
+ AuthenticatorService::new().expect("The auth service should initialize safely");
+
+ manager.add_u2f_usb_hid_platform_transports();
+
+ let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
+ Ok(timeout_s) => {
+ println!("Using {}s as the timeout", &timeout_s);
+ timeout_s * 1_000
+ }
+ Err(e) => {
+ println!("{e}");
+ print_usage(&program, opts);
+ return;
+ }
+ };
+
+ println!("Asking a security key to register now...");
+ let challenge_str = format!(
+ "{}{}",
+ r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
+ r#" "version": "U2F_V2", "appId": "http://example.com"}"#
+ );
+ let mut challenge = Sha256::new();
+ challenge.update(challenge_str.as_bytes());
+ let chall_bytes = challenge.finalize().into();
+
+ let (status_tx, status_rx) = channel::<StatusUpdate>();
+ thread::spawn(move || loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::InteractiveManagement(..)) => {
+ panic!("STATUS: This can't happen when doing non-interactive usage");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("STATUS: waiting for user presence");
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
+ println!(
+ "Wrong PIN! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ let raw_pin =
+ rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
+ sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
+ panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
+ panic!("Too many failed attempts. Your device has been blocked. Reset it.")
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
+ println!(
+ "Wrong UV! {}",
+ attempts.map_or("Try again.".to_string(), |a| format!(
+ "You have {a} attempts left."
+ ))
+ );
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
+ println!("Too many failed UV-attempts.");
+ continue;
+ }
+ Ok(StatusUpdate::PinUvError(e)) => {
+ panic!("Unexpected error: {:?}", e)
+ }
+ Ok(StatusUpdate::SelectResultNotice(_, _)) => {
+ panic!("Unexpected select device notice")
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let user = PublicKeyCredentialUserEntity {
+ id: "user_id".as_bytes().to_vec(),
+ name: Some("A. User".to_string()),
+ display_name: None,
+ };
+ let origin = "https://example.com".to_string();
+ let mut ctap_args = RegisterArgs {
+ client_data_hash: chall_bytes,
+ relying_party: RelyingParty {
+ id: "example.com".to_string(),
+ name: None,
+ },
+ origin: origin.clone(),
+ user,
+ pub_cred_params: vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ],
+ exclude_list: vec![],
+ user_verification_req: UserVerificationRequirement::Preferred,
+ resident_key_req: ResidentKeyRequirement::Discouraged,
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ };
+
+ let mut registered_key_handle = None;
+ loop {
+ let (register_tx, register_rx) = channel();
+ let callback = StateCallback::new(Box::new(move |rv| {
+ register_tx.send(rv).unwrap();
+ }));
+
+ if let Err(e) = manager.register(timeout_ms, ctap_args.clone(), status_tx.clone(), callback)
+ {
+ panic!("Couldn't register: {:?}", e);
+ };
+
+ let register_result = register_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match register_result {
+ Ok(a) => {
+ println!("Ok!");
+ println!("Registering again with the key_handle we just got back. This should result in a 'already registered' error.");
+ let key_handle = a
+ .att_obj
+ .auth_data
+ .credential_data
+ .unwrap()
+ .credential_id
+ .clone();
+ let pub_key = PublicKeyCredentialDescriptor {
+ id: key_handle,
+ transports: vec![Transport::USB],
+ };
+ ctap_args.exclude_list = vec![pub_key.clone()];
+ registered_key_handle = Some(pub_key);
+ continue;
+ }
+ Err(AuthenticatorError::CredentialExcluded) => {
+ println!("Got an 'already registered' error, as expected.");
+ if ctap_args.exclude_list.len() > 1 {
+ println!("Quitting.");
+ break;
+ }
+ println!("Extending the list to contain more invalid handles.");
+ let registered_handle = ctap_args.exclude_list[0].clone();
+ ctap_args.exclude_list = vec![];
+ for ii in 0..10 {
+ ctap_args.exclude_list.push(PublicKeyCredentialDescriptor {
+ id: vec![ii; 50],
+ transports: vec![Transport::USB],
+ });
+ }
+ ctap_args.exclude_list.push(registered_handle);
+ continue;
+ }
+ Err(e) => panic!("Registration failed: {:?}", e),
+ };
+ }
+
+ // Signing
+ let mut ctap_args = SignArgs {
+ client_data_hash: chall_bytes,
+ origin,
+ relying_party_id: "example.com".to_string(),
+ allow_list: vec![],
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ user_verification_req: UserVerificationRequirement::Preferred,
+ user_presence_req: true,
+ };
+
+ let mut no_cred_errors_done = false;
+ loop {
+ let (sign_tx, sign_rx) = channel();
+ let callback = StateCallback::new(Box::new(move |rv| {
+ sign_tx.send(rv).unwrap();
+ }));
+
+ if let Err(e) = manager.sign(timeout_ms, ctap_args.clone(), status_tx.clone(), callback) {
+ panic!("Couldn't sign: {:?}", e);
+ };
+
+ let sign_result = sign_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match sign_result {
+ Ok(_) => {
+ if !no_cred_errors_done {
+ panic!("Should have errored out with NoCredentials, but it succeeded.");
+ }
+ println!("Successfully signed!");
+ if ctap_args.allow_list.len() > 1 {
+ println!("Quitting.");
+ break;
+ }
+ println!("Signing again with a long allow_list that needs pre-flighting.");
+ let registered_handle = registered_key_handle.as_ref().unwrap().clone();
+ ctap_args.allow_list = vec![];
+ for ii in 0..10 {
+ ctap_args.allow_list.push(PublicKeyCredentialDescriptor {
+ id: vec![ii; 50],
+ transports: vec![Transport::USB],
+ });
+ }
+ ctap_args.allow_list.push(registered_handle);
+ continue;
+ }
+ Err(AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NoCredentials,
+ None,
+ ))))
+ | Err(AuthenticatorError::UnsupportedOption(UnsupportedOption::EmptyAllowList)) => {
+ if ctap_args.allow_list.is_empty() {
+ // Try again with a list of false creds. We should end up here again.
+ println!(
+ "Got an 'no credentials' error, as expected with an empty allow-list."
+ );
+ println!("Extending the list to contain only fake handles.");
+ ctap_args.allow_list = vec![];
+ for ii in 0..10 {
+ ctap_args.allow_list.push(PublicKeyCredentialDescriptor {
+ id: vec![ii; 50],
+ transports: vec![Transport::USB],
+ });
+ }
+ } else {
+ println!(
+ "Got an 'no credentials' error, as expected with an all-fake allow-list."
+ );
+ println!("Extending the list to contain one valid handle.");
+ let registered_handle = registered_key_handle.as_ref().unwrap().clone();
+ ctap_args.allow_list = vec![registered_handle];
+ no_cred_errors_done = true;
+ }
+ continue;
+ }
+ Err(e) => panic!("Registration failed: {:?}", e),
+ };
+ }
+ println!("Done")
+}
diff --git a/third_party/rust/authenticator/rust-toolchain.toml b/third_party/rust/authenticator/rust-toolchain.toml
new file mode 100644
index 0000000000..a726970a98
--- /dev/null
+++ b/third_party/rust/authenticator/rust-toolchain.toml
@@ -0,0 +1,5 @@
+[toolchain]
+# Current rust version for mozilla-central
+# https://firefox-source-docs.mozilla.org/writing-rust-code/update-policy.html
+channel = "1.73.0"
+components = ["rustfmt", "clippy", "rust-analyzer"]
diff --git a/third_party/rust/authenticator/rustfmt.toml b/third_party/rust/authenticator/rustfmt.toml
new file mode 100644
index 0000000000..b3e96e305b
--- /dev/null
+++ b/third_party/rust/authenticator/rustfmt.toml
@@ -0,0 +1,3 @@
+comment_width = 200
+wrap_comments = true
+edition = "2018"
diff --git a/third_party/rust/authenticator/src/authenticatorservice.rs b/third_party/rust/authenticator/src/authenticatorservice.rs
new file mode 100644
index 0000000000..e5935150ed
--- /dev/null
+++ b/third_party/rust/authenticator/src/authenticatorservice.rs
@@ -0,0 +1,642 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::ctap2::commands::client_pin::Pin;
+use crate::ctap2::server::{
+ AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor,
+ PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty,
+ ResidentKeyRequirement, UserVerificationRequirement,
+};
+use crate::errors::*;
+use crate::manager::Manager;
+use crate::statecallback::StateCallback;
+use std::sync::{mpsc::Sender, Arc, Mutex};
+
+#[derive(Debug, Clone)]
+pub struct RegisterArgs {
+ pub client_data_hash: [u8; 32],
+ pub relying_party: RelyingParty,
+ pub origin: String,
+ pub user: PublicKeyCredentialUserEntity,
+ pub pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ pub exclude_list: Vec<PublicKeyCredentialDescriptor>,
+ pub user_verification_req: UserVerificationRequirement,
+ pub resident_key_req: ResidentKeyRequirement,
+ pub extensions: AuthenticationExtensionsClientInputs,
+ pub pin: Option<Pin>,
+ pub use_ctap1_fallback: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct SignArgs {
+ pub client_data_hash: [u8; 32],
+ pub origin: String,
+ pub relying_party_id: String,
+ pub allow_list: Vec<PublicKeyCredentialDescriptor>,
+ pub user_verification_req: UserVerificationRequirement,
+ pub user_presence_req: bool,
+ pub extensions: AuthenticationExtensionsClientInputs,
+ pub pin: Option<Pin>,
+ pub use_ctap1_fallback: bool,
+}
+
+pub trait AuthenticatorTransport {
+ /// The implementation of this method must return quickly and should
+ /// report its status via the status and callback methods
+ fn register(
+ &mut self,
+ timeout: u64,
+ ctap_args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()>;
+
+ /// The implementation of this method must return quickly and should
+ /// report its status via the status and callback methods
+ fn sign(
+ &mut self,
+ timeout: u64,
+ ctap_args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()>;
+
+ fn cancel(&mut self) -> crate::Result<()>;
+ fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()>;
+ fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()>;
+ fn manage(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ ) -> crate::Result<()>;
+}
+
+pub struct AuthenticatorService {
+ transports: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>,
+}
+
+fn clone_and_configure_cancellation_callback<T>(
+ mut callback: StateCallback<T>,
+ transports_to_cancel: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>,
+) -> StateCallback<T> {
+ callback.add_uncloneable_observer(Box::new(move || {
+ debug!(
+ "Callback observer is running, cancelling \
+ {} unchosen transports...",
+ transports_to_cancel.len()
+ );
+ for transport_mutex in &transports_to_cancel {
+ if let Err(e) = transport_mutex.lock().unwrap().cancel() {
+ error!("Cancellation failed: {:?}", e);
+ }
+ }
+ }));
+ callback
+}
+
+impl AuthenticatorService {
+ pub fn new() -> crate::Result<Self> {
+ Ok(Self {
+ transports: Vec::new(),
+ })
+ }
+
+ /// Add any detected platform transports
+ pub fn add_detected_transports(&mut self) {
+ self.add_u2f_usb_hid_platform_transports();
+ }
+
+ pub fn add_transport(&mut self, boxed_token: Box<dyn AuthenticatorTransport + Send>) {
+ self.transports.push(Arc::new(Mutex::new(boxed_token)))
+ }
+
+ pub fn add_u2f_usb_hid_platform_transports(&mut self) {
+ match Manager::new() {
+ Ok(token) => self.add_transport(Box::new(token)),
+ Err(e) => error!("Could not add CTAP2 HID transport: {}", e),
+ }
+ }
+
+ pub fn register(
+ &mut self,
+ timeout: u64,
+ args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "register called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!(
+ "register transports_to_cancel {}",
+ transports_to_cancel.len()
+ );
+
+ transport_mutex.lock().unwrap().register(
+ timeout,
+ args.clone(),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn sign(
+ &mut self,
+ timeout: u64,
+ args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ transport_mutex.lock().unwrap().sign(
+ timeout,
+ args.clone(),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn cancel(&mut self) -> crate::Result<()> {
+ if self.transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ for transport_mutex in &mut self.transports {
+ transport_mutex.lock().unwrap().cancel()?;
+ }
+
+ Ok(())
+ }
+
+ pub fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "reset called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!("reset transports_to_cancel {}", transports_to_cancel.len());
+
+ transport_mutex.lock().unwrap().reset(
+ timeout,
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "reset called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!("reset transports_to_cancel {}", transports_to_cancel.len());
+
+ transport_mutex.lock().unwrap().set_pin(
+ timeout,
+ new_pin.clone(),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn manage(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "Manage called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!("reset transports_to_cancel {}", transports_to_cancel.len());
+
+ transport_mutex.lock().unwrap().manage(
+ timeout,
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+ use super::{AuthenticatorService, AuthenticatorTransport, Pin, RegisterArgs, SignArgs};
+ use crate::consts::PARAMETER_SIZE;
+ use crate::ctap2::server::{
+ PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement,
+ UserVerificationRequirement,
+ };
+ use crate::errors::AuthenticatorError;
+ use crate::statecallback::StateCallback;
+ use crate::StatusUpdate;
+ use std::sync::atomic::{AtomicBool, Ordering};
+ use std::sync::mpsc::{channel, Sender};
+ use std::sync::Arc;
+ use std::{io, thread};
+
+ fn init() {
+ let _ = env_logger::builder().is_test(true).try_init();
+ }
+
+ pub struct TestTransportDriver {
+ consent: bool,
+ was_cancelled: Arc<AtomicBool>,
+ }
+
+ impl TestTransportDriver {
+ pub fn new(consent: bool) -> io::Result<Self> {
+ Ok(Self {
+ consent,
+ was_cancelled: Arc::new(AtomicBool::new(false)),
+ })
+ }
+ }
+
+ impl AuthenticatorTransport for TestTransportDriver {
+ fn register(
+ &mut self,
+ _timeout: u64,
+ _args: RegisterArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ if self.consent {
+ // The value we send is ignored, and this is easier than constructing a
+ // RegisterResult
+ let rv = Err(AuthenticatorError::Platform);
+ thread::spawn(move || callback.call(rv));
+ }
+ Ok(())
+ }
+
+ fn sign(
+ &mut self,
+ _timeout: u64,
+ _ctap_args: SignArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ if self.consent {
+ // The value we send is ignored, and this is easier than constructing a
+ // RegisterResult
+ let rv = Err(AuthenticatorError::Platform);
+ thread::spawn(move || callback.call(rv));
+ }
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> crate::Result<()> {
+ self.was_cancelled
+ .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
+ .map_or(
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::InvalidState,
+ )),
+ |_| Ok(()),
+ )
+ }
+
+ fn reset(
+ &mut self,
+ _timeout: u64,
+ _status: Sender<crate::StatusUpdate>,
+ _callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ unimplemented!();
+ }
+
+ fn set_pin(
+ &mut self,
+ _timeout: u64,
+ _new_pin: Pin,
+ _status: Sender<crate::StatusUpdate>,
+ _callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ unimplemented!();
+ }
+
+ fn manage(
+ &mut self,
+ _timeout: u64,
+ _status: Sender<crate::StatusUpdate>,
+ _callback: StateCallback<crate::Result<crate::ManageResult>>,
+ ) -> crate::Result<()> {
+ unimplemented!();
+ }
+ }
+
+ fn mk_challenge() -> [u8; PARAMETER_SIZE] {
+ [0x11; PARAMETER_SIZE]
+ }
+
+ #[test]
+ fn test_no_transports() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new().unwrap();
+ assert_matches!(
+ s.register(
+ 1_000,
+ RegisterArgs {
+ client_data_hash: mk_challenge(),
+ relying_party: RelyingParty {
+ id: "example.com".to_string(),
+ name: None,
+ },
+ origin: "example.com".to_string(),
+ user: PublicKeyCredentialUserEntity {
+ id: "user_id".as_bytes().to_vec(),
+ name: Some("A. User".to_string()),
+ display_name: None,
+ },
+ pub_cred_params: vec![],
+ exclude_list: vec![],
+ user_verification_req: UserVerificationRequirement::Preferred,
+ resident_key_req: ResidentKeyRequirement::Preferred,
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ },
+ status_tx.clone(),
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::NoConfiguredTransports
+ );
+
+ assert_matches!(
+ s.sign(
+ 1_000,
+ SignArgs {
+ client_data_hash: mk_challenge(),
+ origin: "example.com".to_string(),
+ relying_party_id: "example.com".to_string(),
+ allow_list: vec![],
+ user_verification_req: UserVerificationRequirement::Preferred,
+ user_presence_req: true,
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ },
+ status_tx,
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::NoConfiguredTransports
+ );
+
+ assert_matches!(
+ s.cancel().unwrap_err(),
+ crate::errors::AuthenticatorError::NoConfiguredTransports
+ );
+ }
+
+ #[test]
+ fn test_cancellation_register() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new().unwrap();
+ let ttd_one = TestTransportDriver::new(true).unwrap();
+ let ttd_two = TestTransportDriver::new(false).unwrap();
+ let ttd_three = TestTransportDriver::new(false).unwrap();
+
+ let was_cancelled_one = ttd_one.was_cancelled.clone();
+ let was_cancelled_two = ttd_two.was_cancelled.clone();
+ let was_cancelled_three = ttd_three.was_cancelled.clone();
+
+ s.add_transport(Box::new(ttd_one));
+ s.add_transport(Box::new(ttd_two));
+ s.add_transport(Box::new(ttd_three));
+
+ let callback = StateCallback::new(Box::new(move |_rv| {}));
+ assert!(s
+ .register(
+ 1_000,
+ RegisterArgs {
+ client_data_hash: mk_challenge(),
+ relying_party: RelyingParty {
+ id: "example.com".to_string(),
+ name: None,
+ },
+ origin: "example.com".to_string(),
+ user: PublicKeyCredentialUserEntity {
+ id: "user_id".as_bytes().to_vec(),
+ name: Some("A. User".to_string()),
+ display_name: None,
+ },
+ pub_cred_params: vec![],
+ exclude_list: vec![],
+ user_verification_req: UserVerificationRequirement::Preferred,
+ resident_key_req: ResidentKeyRequirement::Preferred,
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ },
+ status_tx,
+ callback.clone(),
+ )
+ .is_ok());
+ callback.wait();
+
+ assert!(!was_cancelled_one.load(Ordering::SeqCst));
+ assert!(was_cancelled_two.load(Ordering::SeqCst));
+ assert!(was_cancelled_three.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_cancellation_sign() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new().unwrap();
+ let ttd_one = TestTransportDriver::new(true).unwrap();
+ let ttd_two = TestTransportDriver::new(false).unwrap();
+ let ttd_three = TestTransportDriver::new(false).unwrap();
+
+ let was_cancelled_one = ttd_one.was_cancelled.clone();
+ let was_cancelled_two = ttd_two.was_cancelled.clone();
+ let was_cancelled_three = ttd_three.was_cancelled.clone();
+
+ s.add_transport(Box::new(ttd_one));
+ s.add_transport(Box::new(ttd_two));
+ s.add_transport(Box::new(ttd_three));
+
+ let callback = StateCallback::new(Box::new(move |_rv| {}));
+ assert!(s
+ .sign(
+ 1_000,
+ SignArgs {
+ client_data_hash: mk_challenge(),
+ origin: "example.com".to_string(),
+ relying_party_id: "example.com".to_string(),
+ allow_list: vec![],
+ user_verification_req: UserVerificationRequirement::Preferred,
+ user_presence_req: true,
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ },
+ status_tx,
+ callback.clone(),
+ )
+ .is_ok());
+ callback.wait();
+
+ assert!(!was_cancelled_one.load(Ordering::SeqCst));
+ assert!(was_cancelled_two.load(Ordering::SeqCst));
+ assert!(was_cancelled_three.load(Ordering::SeqCst));
+ }
+
+ #[test]
+ fn test_cancellation_race() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new().unwrap();
+ // Let both of these race which one provides consent.
+ let ttd_one = TestTransportDriver::new(true).unwrap();
+ let ttd_two = TestTransportDriver::new(true).unwrap();
+
+ let was_cancelled_one = ttd_one.was_cancelled.clone();
+ let was_cancelled_two = ttd_two.was_cancelled.clone();
+
+ s.add_transport(Box::new(ttd_one));
+ s.add_transport(Box::new(ttd_two));
+
+ let callback = StateCallback::new(Box::new(move |_rv| {}));
+ assert!(s
+ .register(
+ 1_000,
+ RegisterArgs {
+ client_data_hash: mk_challenge(),
+ relying_party: RelyingParty {
+ id: "example.com".to_string(),
+ name: None,
+ },
+ origin: "example.com".to_string(),
+ user: PublicKeyCredentialUserEntity {
+ id: "user_id".as_bytes().to_vec(),
+ name: Some("A. User".to_string()),
+ display_name: None,
+ },
+ pub_cred_params: vec![],
+ exclude_list: vec![],
+ user_verification_req: UserVerificationRequirement::Preferred,
+ resident_key_req: ResidentKeyRequirement::Preferred,
+ extensions: Default::default(),
+ pin: None,
+ use_ctap1_fallback: false,
+ },
+ status_tx,
+ callback.clone(),
+ )
+ .is_ok());
+ callback.wait();
+
+ let one = was_cancelled_one.load(Ordering::SeqCst);
+ let two = was_cancelled_two.load(Ordering::SeqCst);
+ assert!(
+ one ^ two,
+ "asserting that one={} xor two={} is true",
+ one,
+ two
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/consts.rs b/third_party/rust/authenticator/src/consts.rs
new file mode 100644
index 0000000000..3579d85ee7
--- /dev/null
+++ b/third_party/rust/authenticator/src/consts.rs
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Allow dead code in this module, since it's all packet consts anyways.
+#![allow(dead_code)]
+
+use serde::Serialize;
+
+pub const MAX_HID_RPT_SIZE: usize = 64;
+
+/// Minimum size of the U2F Raw Message header (FIDO v1.x) in extended mode,
+/// including expected response length (L<sub>e</sub>).
+///
+/// Fields `CLA`, `INS`, `P1` and `P2` are 1 byte each, and L<sub>e</sub> is 3
+/// bytes. If there is a data payload, add 2 bytes (L<sub>c</sub> is 3 bytes,
+/// and L<sub>e</sub> is 2 bytes).
+pub const U2FAPDUHEADER_SIZE: usize = 7;
+
+pub const CID_BROADCAST: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
+pub const TYPE_MASK: u8 = 0x80;
+pub const TYPE_INIT: u8 = 0x80;
+pub const TYPE_CONT: u8 = 0x80;
+
+// Size of header in U2F Init USB HID Packets
+pub const INIT_HEADER_SIZE: usize = 7;
+// Size of header in U2F Cont USB HID Packets
+pub const CONT_HEADER_SIZE: usize = 5;
+
+pub const PARAMETER_SIZE: usize = 32;
+
+pub const FIDO_USAGE_PAGE: u16 = 0xf1d0; // FIDO alliance HID usage page
+pub const FIDO_USAGE_U2FHID: u16 = 0x01; // U2FHID usage for top-level collection
+pub const FIDO_USAGE_DATA_IN: u8 = 0x20; // Raw IN data report
+pub const FIDO_USAGE_DATA_OUT: u8 = 0x21; // Raw OUT data report
+
+// General pub constants
+
+pub const U2FHID_IF_VERSION: u32 = 2; // Current interface implementation version
+pub const U2FHID_FRAME_TIMEOUT: u32 = 500; // Default frame timeout in ms
+pub const U2FHID_TRANS_TIMEOUT: u32 = 3000; // Default message timeout in ms
+
+// CTAPHID native commands
+const CTAPHID_PING: u8 = TYPE_INIT | 0x01; // Echo data through local processor only
+const CTAPHID_MSG: u8 = TYPE_INIT | 0x03; // Send U2F message frame
+const CTAPHID_LOCK: u8 = TYPE_INIT | 0x04; // Send lock channel command
+const CTAPHID_INIT: u8 = TYPE_INIT | 0x06; // Channel initialization
+const CTAPHID_WINK: u8 = TYPE_INIT | 0x08; // Send device identification wink
+const CTAPHID_CBOR: u8 = TYPE_INIT | 0x10; // Encapsulated CBOR encoded message
+const CTAPHID_CANCEL: u8 = TYPE_INIT | 0x11; // Cancel outstanding requests
+const CTAPHID_KEEPALIVE: u8 = TYPE_INIT | 0x3b; // Keepalive sent to authenticator every 100ms and whenever a status changes
+const CTAPHID_ERROR: u8 = TYPE_INIT | 0x3f; // Error response
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+#[repr(u8)]
+pub enum HIDCmd {
+ Ping,
+ Msg,
+ Lock,
+ Init,
+ Wink,
+ Cbor,
+ Cancel,
+ Keepalive,
+ Error,
+ Unknown(u8),
+}
+
+impl From<HIDCmd> for u8 {
+ fn from(v: HIDCmd) -> u8 {
+ match v {
+ HIDCmd::Ping => CTAPHID_PING,
+ HIDCmd::Msg => CTAPHID_MSG,
+ HIDCmd::Lock => CTAPHID_LOCK,
+ HIDCmd::Init => CTAPHID_INIT,
+ HIDCmd::Wink => CTAPHID_WINK,
+ HIDCmd::Cbor => CTAPHID_CBOR,
+ HIDCmd::Cancel => CTAPHID_CANCEL,
+ HIDCmd::Keepalive => CTAPHID_KEEPALIVE,
+ HIDCmd::Error => CTAPHID_ERROR,
+ HIDCmd::Unknown(v) => v,
+ }
+ }
+}
+
+impl From<u8> for HIDCmd {
+ fn from(v: u8) -> HIDCmd {
+ match v {
+ CTAPHID_PING => HIDCmd::Ping,
+ CTAPHID_MSG => HIDCmd::Msg,
+ CTAPHID_LOCK => HIDCmd::Lock,
+ CTAPHID_INIT => HIDCmd::Init,
+ CTAPHID_WINK => HIDCmd::Wink,
+ CTAPHID_CBOR => HIDCmd::Cbor,
+ CTAPHID_CANCEL => HIDCmd::Cancel,
+ CTAPHID_KEEPALIVE => HIDCmd::Keepalive,
+ CTAPHID_ERROR => HIDCmd::Error,
+ v => HIDCmd::Unknown(v),
+ }
+ }
+}
+
+// U2FHID_MSG commands
+pub const U2F_VENDOR_FIRST: u8 = TYPE_INIT | 0x40; // First vendor defined command
+pub const U2F_VENDOR_LAST: u8 = TYPE_INIT | 0x7f; // Last vendor defined command
+pub const U2F_REGISTER: u8 = 0x01; // Registration command
+pub const U2F_AUTHENTICATE: u8 = 0x02; // Authenticate/sign command
+pub const U2F_VERSION: u8 = 0x03; // Read version string command
+
+pub const YKPIV_INS_GET_VERSION: u8 = 0xfd; // Get firmware version, yubico ext
+
+// U2F_REGISTER command defines
+pub const U2F_REGISTER_ID: u8 = 0x05; // Version 2 registration identifier
+pub const U2F_REGISTER_HASH_ID: u8 = 0x00; // Version 2 hash identintifier
+
+// U2F_AUTHENTICATE command defines
+pub const U2F_REQUEST_USER_PRESENCE: u8 = 0x03; // Verify user presence and sign
+pub const U2F_CHECK_IS_REGISTERED: u8 = 0x07; // Check if the key handle is registered
+pub const U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN: u8 = 0x08; // Sign, but don't verify user presence
+
+// U2FHID_INIT command defines
+pub const INIT_NONCE_SIZE: usize = 8; // Size of channel initialization challenge
+
+bitflags! {
+ #[derive(Serialize)]
+ pub struct Capability: u8 {
+ const WINK = 0x01;
+ const LOCK = 0x02;
+ const CBOR = 0x04;
+ const NMSG = 0x08;
+ }
+}
+
+// Low-level error codes. Return as negatives.
+
+pub const ERR_NONE: u8 = 0x00; // No error
+pub const ERR_INVALID_CMD: u8 = 0x01; // Invalid command
+pub const ERR_INVALID_PAR: u8 = 0x02; // Invalid parameter
+pub const ERR_INVALID_LEN: u8 = 0x03; // Invalid message length
+pub const ERR_INVALID_SEQ: u8 = 0x04; // Invalid message sequencing
+pub const ERR_MSG_TIMEOUT: u8 = 0x05; // Message has timed out
+pub const ERR_CHANNEL_BUSY: u8 = 0x06; // Channel busy
+pub const ERR_LOCK_REQUIRED: u8 = 0x0a; // Command requires channel lock
+pub const ERR_INVALID_CID: u8 = 0x0b; // Command not allowed on this cid
+pub const ERR_OTHER: u8 = 0x7f; // Other unspecified error
+
+// These are ISO 7816-4 defined response status words.
+pub const SW_NO_ERROR: [u8; 2] = [0x90, 0x00];
+pub const SW_CONDITIONS_NOT_SATISFIED: [u8; 2] = [0x69, 0x85];
+pub const SW_WRONG_DATA: [u8; 2] = [0x6A, 0x80];
+pub const SW_WRONG_LENGTH: [u8; 2] = [0x67, 0x00];
diff --git a/third_party/rust/authenticator/src/crypto/der.rs b/third_party/rust/authenticator/src/crypto/der.rs
new file mode 100644
index 0000000000..39c5e0b676
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/der.rs
@@ -0,0 +1,185 @@
+use super::CryptoError;
+
+pub const TAG_INTEGER: u8 = 0x02;
+pub const TAG_BIT_STRING: u8 = 0x03;
+#[cfg(all(test, feature = "crypto_nss"))]
+pub const TAG_OCTET_STRING: u8 = 0x04;
+pub const TAG_NULL: u8 = 0x05;
+pub const TAG_OBJECT_ID: u8 = 0x06;
+pub const TAG_SEQUENCE: u8 = 0x30;
+
+// Object identifiers in DER tag-length-value form
+pub const OID_EC_PUBLIC_KEY_BYTES: &[u8] = &[
+ /* RFC 5480 (id-ecPublicKey) */
+ 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
+];
+pub const OID_SECP256R1_BYTES: &[u8] = &[
+ /* RFC 5480 (secp256r1) */
+ 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
+];
+pub const OID_ED25519_BYTES: &[u8] = &[/* RFC 8410 (id-ed25519) */ 0x2b, 0x65, 0x70];
+pub const OID_RS256_BYTES: &[u8] = &[
+ /* RFC 4055 (sha256WithRSAEncryption) */
+ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b,
+];
+
+pub type Result<T> = std::result::Result<T, CryptoError>;
+
+const MAX_TAG_AND_LENGTH_BYTES: usize = 4;
+fn write_tag_and_length(out: &mut Vec<u8>, tag: u8, len: usize) -> Result<()> {
+ if len > 0xFFFF {
+ return Err(CryptoError::LibraryFailure);
+ }
+ out.push(tag);
+ if len > 0xFF {
+ out.push(0x82);
+ out.push((len >> 8) as u8);
+ out.push(len as u8);
+ } else if len > 0x7F {
+ out.push(0x81);
+ out.push(len as u8);
+ } else {
+ out.push(len as u8);
+ }
+ Ok(())
+}
+
+pub fn integer(val: &[u8]) -> Result<Vec<u8>> {
+ if val.is_empty() {
+ return Err(CryptoError::MalformedInput);
+ }
+ // trim leading zeros, leaving a single zero if the input is the zero vector.
+ let mut val = val;
+ while val.len() > 1 && val[0] == 0 {
+ val = &val[1..];
+ }
+ let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + 1 + val.len());
+ if val[0] & 0x80 != 0 {
+ // needs zero prefix
+ write_tag_and_length(&mut out, TAG_INTEGER, 1 + val.len())?;
+ out.push(0x00);
+ out.extend_from_slice(val);
+ } else {
+ write_tag_and_length(&mut out, TAG_INTEGER, val.len())?;
+ out.extend_from_slice(val);
+ }
+ Ok(out)
+}
+
+pub fn bit_string(val: &[u8]) -> Result<Vec<u8>> {
+ let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + 1 + val.len());
+ write_tag_and_length(&mut out, TAG_BIT_STRING, 1 + val.len())?;
+ out.push(0x00); // trailing bits aren't supported
+ out.extend_from_slice(val);
+ Ok(out)
+}
+
+pub fn null() -> Result<Vec<u8>> {
+ let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES);
+ write_tag_and_length(&mut out, TAG_NULL, 0)?;
+ Ok(out)
+}
+
+pub fn object_id(val: &[u8]) -> Result<Vec<u8>> {
+ let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + val.len());
+ write_tag_and_length(&mut out, TAG_OBJECT_ID, val.len())?;
+ out.extend_from_slice(val);
+ Ok(out)
+}
+
+pub fn sequence(items: &[&[u8]]) -> Result<Vec<u8>> {
+ let len = items.iter().map(|i| i.len()).sum();
+ let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + len);
+ write_tag_and_length(&mut out, TAG_SEQUENCE, len)?;
+ for item in items {
+ out.extend_from_slice(item);
+ }
+ Ok(out)
+}
+
+#[cfg(all(test, feature = "crypto_nss"))]
+pub fn octet_string(val: &[u8]) -> Result<Vec<u8>> {
+ let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + val.len());
+ write_tag_and_length(&mut out, TAG_OCTET_STRING, val.len())?;
+ out.extend_from_slice(val);
+ Ok(out)
+}
+
+#[cfg(all(test, feature = "crypto_nss"))]
+pub fn context_specific_explicit_tag(tag: u8, content: &[u8]) -> Result<Vec<u8>> {
+ let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + content.len());
+ write_tag_and_length(&mut out, 0xa0 + tag, content.len())?;
+ out.extend_from_slice(content);
+ Ok(out)
+}
+
+// Given "tag || len || value || rest" where tag and len are of length one, len is in [0, 127],
+// and value is of length len, returns (value, rest)
+#[cfg(all(test, feature = "crypto_nss"))]
+fn expect_tag_with_short_len(tag: u8, z: &[u8]) -> Result<(&[u8], &[u8])> {
+ if z.is_empty() {
+ return Err(CryptoError::MalformedInput);
+ }
+ let (h, z) = z.split_at(1);
+ if h[0] != tag || z.is_empty() {
+ return Err(CryptoError::MalformedInput);
+ }
+ let (h, z) = z.split_at(1);
+ if h[0] >= 0x80 || h[0] as usize > z.len() {
+ return Err(CryptoError::MalformedInput);
+ }
+ Ok(z.split_at(h[0] as usize))
+}
+
+// Given a DER encoded RFC 3279 Ecdsa-Sig-Value,
+// Ecdsa-Sig-Value ::= SEQUENCE {
+// r INTEGER,
+// s INTEGER },
+// with r and s < 2^256, returns a 64 byte array containing
+// r and s encoded as 32 byte zero-padded big endian unsigned
+// integers
+#[cfg(all(test, feature = "crypto_nss"))]
+pub fn read_p256_sig(z: &[u8]) -> Result<Vec<u8>> {
+ // Strip the tag and length.
+ let (z, rest) = expect_tag_with_short_len(TAG_SEQUENCE, z)?;
+
+ // The input should not have any trailing data.
+ if !rest.is_empty() {
+ return Err(CryptoError::MalformedInput);
+ }
+
+ let read_u256 = |z| -> Result<(&[u8], &[u8])> {
+ let (r, z) = expect_tag_with_short_len(TAG_INTEGER, z)?;
+ // We're expecting r < 2^256, so no more than 33 bytes as a signed integer.
+ if r.is_empty() || r.len() > 33 {
+ return Err(CryptoError::MalformedInput);
+ }
+ // If it is 33 bytes the leading byte must be zero.
+ if r.len() == 33 && r[0] != 0 {
+ return Err(CryptoError::MalformedInput);
+ }
+ // Ensure r is no more than 32 bytes.
+ if r.len() == 33 {
+ Ok((&r[1..], z))
+ } else {
+ Ok((r, z))
+ }
+ };
+
+ let (r, z) = read_u256(z)?;
+ let (s, z) = read_u256(z)?;
+
+ // We should have consumed the entire buffer
+ if !z.is_empty() {
+ return Err(CryptoError::MalformedInput);
+ }
+
+ // Left pad each integer with zeros to length 32 and concatenate the results
+ let mut out = vec![0u8; 64];
+ {
+ let (r_out, s_out) = out.split_at_mut(32);
+ r_out[32 - r.len()..].copy_from_slice(r);
+ s_out[32 - s.len()..].copy_from_slice(s);
+ }
+ Ok(out)
+}
diff --git a/third_party/rust/authenticator/src/crypto/dummy.rs b/third_party/rust/authenticator/src/crypto/dummy.rs
new file mode 100644
index 0000000000..9d16856958
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/dummy.rs
@@ -0,0 +1,57 @@
+use super::CryptoError;
+
+/*
+This is a dummy implementation for CI, to avoid having to install NSS or openSSL in the CI-pipeline
+*/
+
+pub type Result<T> = std::result::Result<T, CryptoError>;
+
+pub fn ecdhe_p256_raw(_peer_spki: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
+ unimplemented!()
+}
+
+pub fn encrypt_aes_256_cbc_no_pad(
+ _key: &[u8],
+ _iv: Option<&[u8]>,
+ _data: &[u8],
+) -> Result<Vec<u8>> {
+ unimplemented!()
+}
+
+pub fn decrypt_aes_256_cbc_no_pad(
+ _key: &[u8],
+ _iv: Option<&[u8]>,
+ _data: &[u8],
+) -> Result<Vec<u8>> {
+ unimplemented!()
+}
+
+pub fn hmac_sha256(_key: &[u8], _data: &[u8]) -> Result<Vec<u8>> {
+ unimplemented!()
+}
+
+pub fn sha256(_data: &[u8]) -> Result<Vec<u8>> {
+ unimplemented!()
+}
+
+pub fn random_bytes(_count: usize) -> Result<Vec<u8>> {
+ unimplemented!()
+}
+
+pub fn gen_p256() -> Result<(Vec<u8>, Vec<u8>)> {
+ unimplemented!()
+}
+
+pub fn ecdsa_p256_sha256_sign_raw(_private: &[u8], _data: &[u8]) -> Result<Vec<u8>> {
+ unimplemented!()
+}
+
+#[allow(dead_code)]
+#[cfg(test)]
+pub fn test_ecdsa_p256_sha256_verify_raw(
+ _public: &[u8],
+ _signature: &[u8],
+ _data: &[u8],
+) -> Result<()> {
+ unimplemented!()
+}
diff --git a/third_party/rust/authenticator/src/crypto/mod.rs b/third_party/rust/authenticator/src/crypto/mod.rs
new file mode 100644
index 0000000000..fd74030ed7
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/mod.rs
@@ -0,0 +1,1638 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::ctap2::commands::client_pin::PinUvAuthTokenPermission;
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::errors::AuthenticatorError;
+use crate::{ctap2::commands::CommandError, transport::errors::HIDError};
+use serde::{
+ de::{Error as SerdeError, MapAccess, Unexpected, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use std::convert::TryFrom;
+use std::fmt;
+
+#[cfg(feature = "crypto_nss")]
+mod nss;
+#[cfg(feature = "crypto_nss")]
+use nss as backend;
+
+#[cfg(feature = "crypto_openssl")]
+mod openssl;
+#[cfg(feature = "crypto_openssl")]
+use self::openssl as backend;
+
+#[cfg(feature = "crypto_dummy")]
+mod dummy;
+#[cfg(feature = "crypto_dummy")]
+use dummy as backend;
+
+use backend::{
+ decrypt_aes_256_cbc_no_pad, ecdhe_p256_raw, encrypt_aes_256_cbc_no_pad, gen_p256, hmac_sha256,
+ random_bytes, sha256,
+};
+
+mod der;
+
+pub use backend::ecdsa_p256_sha256_sign_raw;
+
+pub struct PinUvAuthProtocol(Box<dyn PinProtocolImpl + Send + Sync>);
+impl PinUvAuthProtocol {
+ pub fn id(&self) -> u64 {
+ self.0.protocol_id()
+ }
+ pub fn encapsulate(&self, peer_cose_key: &COSEKey) -> Result<SharedSecret, CryptoError> {
+ self.0.encapsulate(peer_cose_key)
+ }
+}
+
+/// The output of `PinUvAuthProtocol::encapsulate` is supposed to be used with the same
+/// PinProtocolImpl. So we stash a copy of the calling PinUvAuthProtocol in the output SharedSecret.
+/// We need a trick here to tell the compiler that every PinProtocolImpl we define will implement
+/// Clone.
+trait ClonablePinProtocolImpl {
+ fn clone_box(&self) -> Box<dyn PinProtocolImpl + Send + Sync>;
+}
+
+impl<T> ClonablePinProtocolImpl for T
+where
+ T: 'static + PinProtocolImpl + Clone + Send + Sync,
+{
+ fn clone_box(&self) -> Box<dyn PinProtocolImpl + Send + Sync> {
+ Box::new(self.clone())
+ }
+}
+
+impl Clone for PinUvAuthProtocol {
+ fn clone(&self) -> Self {
+ PinUvAuthProtocol(self.0.as_ref().clone_box())
+ }
+}
+
+/// CTAP 2.1, Section 6.5.4. PIN/UV Auth Protocol Abstract Definition
+trait PinProtocolImpl: ClonablePinProtocolImpl {
+ fn protocol_id(&self) -> u64;
+ fn initialize(&self);
+ fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, CryptoError>;
+ fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError>;
+ fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError>;
+ fn kdf(&self, z: &[u8]) -> Result<Vec<u8>, CryptoError>;
+ fn encapsulate(&self, peer_cose_key: &COSEKey) -> Result<SharedSecret, CryptoError> {
+ // [CTAP 2.1]
+ // encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
+ // 1) Let sharedSecret be the result of calling ecdh(peerCoseKey). Return any
+ // resulting error.
+ // 2) Return (getPublicKey(), sharedSecret)
+ //
+ // ecdh(peerCoseKey) → sharedSecret | error
+ // Parse peerCoseKey as specified for getPublicKey, below, and produce a P-256
+ // point, Y. If unsuccessful, or if the resulting point is not on the curve, return
+ // error. Calculate xY, the shared point. (I.e. the scalar-multiplication of the
+ // peer's point, Y, with the local private key agreement key.) Let Z be the
+ // 32-byte, big-endian encoding of the x-coordinate of the shared point. Return
+ // kdf(Z).
+
+ match peer_cose_key.alg {
+ // There is no COSEAlgorithm for ECDHE with the KDF used here. Section 6.5.6. of CTAP
+ // 2.1 says to use value -25 (= ECDH_ES_HKDF256) even though "this is not the algorithm
+ // actually used".
+ COSEAlgorithm::ECDH_ES_HKDF256 => (),
+ other => return Err(CryptoError::UnsupportedAlgorithm(other)),
+ }
+
+ let peer_cose_ec2_key = match peer_cose_key.key {
+ COSEKeyType::EC2(ref key) => key,
+ _ => return Err(CryptoError::UnsupportedKeyType),
+ };
+
+ let peer_spki = peer_cose_ec2_key.der_spki()?;
+
+ let (shared_point, client_public_sec1) = ecdhe_p256_raw(&peer_spki)?;
+
+ let client_cose_ec2_key =
+ COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &client_public_sec1)?;
+
+ let client_cose_key = COSEKey {
+ alg: COSEAlgorithm::ECDH_ES_HKDF256,
+ key: COSEKeyType::EC2(client_cose_ec2_key),
+ };
+
+ let shared_secret = SharedSecret {
+ pin_protocol: PinUvAuthProtocol(self.clone_box()),
+ key: self.kdf(&shared_point)?,
+ inputs: PublicInputs {
+ peer: peer_cose_key.clone(),
+ client: client_cose_key,
+ },
+ };
+
+ Ok(shared_secret)
+ }
+}
+
+impl TryFrom<&AuthenticatorInfo> for PinUvAuthProtocol {
+ type Error = CommandError;
+
+ fn try_from(info: &AuthenticatorInfo) -> Result<Self, Self::Error> {
+ // CTAP 2.1, Section 6.5.5.4
+ // "If there are multiple mutually supported protocols, and the platform
+ // has no preference, it SHOULD select the one listed first in
+ // pinUvAuthProtocols."
+ if let Some(pin_protocols) = &info.pin_protocols {
+ for proto_id in pin_protocols.iter() {
+ match proto_id {
+ 1 => return Ok(PinUvAuthProtocol(Box::new(PinUvAuth1 {}))),
+ 2 => return Ok(PinUvAuthProtocol(Box::new(PinUvAuth2 {}))),
+ _ => continue,
+ }
+ }
+ } else {
+ match info.max_supported_version() {
+ crate::ctap2::commands::get_info::AuthenticatorVersion::U2F_V2 => {
+ return Err(CommandError::UnsupportedPinProtocol)
+ }
+ crate::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_0 => {
+ return Ok(PinUvAuthProtocol(Box::new(PinUvAuth1 {})))
+ }
+ crate::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_1_PRE
+ | crate::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_1 => {
+ return Ok(PinUvAuthProtocol(Box::new(PinUvAuth2 {})))
+ }
+ }
+ }
+ Err(CommandError::UnsupportedPinProtocol)
+ }
+}
+
+impl fmt::Debug for PinUvAuthProtocol {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("PinUvAuthProtocol")
+ .field("id", &self.id())
+ .finish()
+ }
+}
+
+/// CTAP 2.1, Section 6.5.6.
+#[derive(Copy, Clone)]
+pub struct PinUvAuth1;
+
+impl PinProtocolImpl for PinUvAuth1 {
+ fn protocol_id(&self) -> u64 {
+ 1
+ }
+
+ fn initialize(&self) {}
+
+ fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // [CTAP 2.1]
+ // encrypt(key, demPlaintext) → ciphertext
+ // Return the AES-256-CBC encryption of plaintext using an all-zero IV. (No padding is
+ // performed as the size of plaintext is required to be a multiple of the AES block
+ // length.)
+ encrypt_aes_256_cbc_no_pad(key, None, plaintext)
+ }
+
+ fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // [CTAP 2.1]
+ // decrypt(key, demCiphertext) → plaintext | error
+ // If the size of ciphertext is not a multiple of the AES block length, return error.
+ // Otherwise return the AES-256-CBC decryption of ciphertext using an all-zero IV.
+ decrypt_aes_256_cbc_no_pad(key, None, ciphertext)
+ }
+
+ fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // [CTAP 2.1]
+ // authenticate(key, message) → signature
+ // Return the first 16 bytes of the result of computing HMAC-SHA-256 with the given
+ // key and message.
+ let mut hmac = hmac_sha256(key, message)?;
+ hmac.truncate(16);
+ Ok(hmac)
+ }
+
+ fn kdf(&self, z: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // kdf(Z) → sharedSecret
+ // Return SHA-256(Z)
+ sha256(z)
+ }
+}
+
+/// CTAP 2.1, Section 6.5.7.
+#[derive(Copy, Clone)]
+pub struct PinUvAuth2;
+
+impl PinProtocolImpl for PinUvAuth2 {
+ fn protocol_id(&self) -> u64 {
+ 2
+ }
+
+ fn initialize(&self) {}
+
+ fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // [CTAP 2.1]
+ // encrypt(key, demPlaintext) → ciphertext
+ // 1. Discard the first 32 bytes of key. (This selects the AES-key portion of the
+ // shared secret.)
+ // 2. Let iv be a 16-byte, random bytestring.
+ // 3. Let ct be the AES-256-CBC encryption of demPlaintext using key and iv. (No
+ // padding is performed as the size of demPlaintext is required to be a multiple of
+ // the AES block length.)
+ // 4. Return iv || ct.
+ if key.len() != 64 {
+ return Err(CryptoError::LibraryFailure);
+ }
+ let key = &key[32..64];
+
+ let iv = random_bytes(16)?;
+ let mut ct = encrypt_aes_256_cbc_no_pad(key, Some(&iv), plaintext)?;
+
+ let mut out = iv;
+ out.append(&mut ct);
+ Ok(out)
+ }
+
+ fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // decrypt(key, demCiphertext) → plaintext | error
+ // 1. Discard the first 32 bytes of key. (This selects the AES-key portion of the
+ // shared secret.)
+ // 2. If demCiphertext is less than 16 bytes in length, return an error
+ // 3. Split demCiphertext after the 16th byte to produce two subspans, iv and ct.
+ // 4. Return the AES-256-CBC decryption of ct using key and iv.
+ if key.len() < 64 || ciphertext.len() < 16 {
+ return Err(CryptoError::LibraryFailure);
+ }
+ let key = &key[32..64];
+ let (iv, ct) = ciphertext.split_at(16);
+ decrypt_aes_256_cbc_no_pad(key, Some(iv), ct)
+ }
+
+ fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // authenticate(key, message) → signature
+ // 1. If key is longer than 32 bytes, discard the excess. (This selects the HMAC-key
+ // portion of the shared secret. When key is the pinUvAuthToken, it is exactly 32
+ // bytes long and thus this step has no effect.)
+ // 2. Return the result of computing HMAC-SHA-256 on key and message.
+ if key.len() < 32 {
+ return Err(CryptoError::LibraryFailure);
+ }
+ let key = &key[0..32];
+ hmac_sha256(key, message)
+ }
+
+ fn kdf(&self, z: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ // kdf(Z) → sharedSecret
+ // return HKDF-SHA-256(salt, Z, L = 32, info = "CTAP2 HMAC key") ||
+ // HKDF-SHA-256(salt, Z, L = 32, info = "CTAP2 AES key")
+ // where salt = [0u8; 32].
+ //
+ // From Section 2 of RFC 5869, we have
+ // HKDF(salt, Z, 32, info) =
+ // HKDF-Expand(HKDF-Extract(salt, Z), info || 0x01)
+ //
+ // And for HKDF-SHA256 both Extract and Expand are instantiated with HMAC-SHA256.
+
+ let prk = hmac_sha256(&[0u8; 32], z)?;
+ let mut shared_secret = hmac_sha256(&prk, "CTAP2 HMAC key\x01".as_bytes())?;
+ shared_secret.append(&mut hmac_sha256(&prk, "CTAP2 AES key\x01".as_bytes())?);
+ Ok(shared_secret)
+ }
+}
+
+#[derive(Clone, Debug)]
+struct PublicInputs {
+ client: COSEKey,
+ peer: COSEKey,
+}
+
+#[derive(Clone, Debug)]
+pub struct SharedSecret {
+ pub pin_protocol: PinUvAuthProtocol,
+ key: Vec<u8>,
+ inputs: PublicInputs,
+}
+
+impl SharedSecret {
+ pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ self.pin_protocol.0.encrypt(&self.key, plaintext)
+ }
+ pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ self.pin_protocol.0.decrypt(&self.key, ciphertext)
+ }
+ pub fn decrypt_pin_token(
+ &self,
+ permissions: PinUvAuthTokenPermission,
+ encrypted_pin_token: &[u8],
+ ) -> Result<PinUvAuthToken, CryptoError> {
+ let pin_token = self.decrypt(encrypted_pin_token)?;
+ Ok(PinUvAuthToken {
+ pin_protocol: self.pin_protocol.clone(),
+ pin_token,
+ permissions,
+ })
+ }
+ pub fn authenticate(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ self.pin_protocol.0.authenticate(&self.key, message)
+ }
+ pub fn client_input(&self) -> &COSEKey {
+ &self.inputs.client
+ }
+ pub fn peer_input(&self) -> &COSEKey {
+ &self.inputs.peer
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct PinUvAuthToken {
+ pub pin_protocol: PinUvAuthProtocol,
+ pin_token: Vec<u8>,
+ pub permissions: PinUvAuthTokenPermission,
+}
+
+impl PinUvAuthToken {
+ pub fn derive(self, message: &[u8]) -> Result<PinUvAuthParam, CryptoError> {
+ let pin_auth = self.pin_protocol.0.authenticate(&self.pin_token, message)?;
+ Ok(PinUvAuthParam {
+ pin_auth,
+ pin_protocol: self.pin_protocol,
+ permissions: self.permissions,
+ })
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct PinUvAuthParam {
+ pin_auth: Vec<u8>,
+ pub pin_protocol: PinUvAuthProtocol,
+ #[allow(dead_code)] // Not yet used
+ permissions: PinUvAuthTokenPermission,
+}
+
+impl PinUvAuthParam {
+ pub(crate) fn create_empty() -> Self {
+ let pin_protocol = PinUvAuthProtocol(Box::new(PinUvAuth1 {}));
+ Self {
+ pin_auth: vec![],
+ pin_protocol,
+ permissions: PinUvAuthTokenPermission::empty(),
+ }
+ }
+}
+
+impl Serialize for PinUvAuthParam {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serde_bytes::serialize(&self.pin_auth[..], serializer)
+ }
+}
+
+/// A Curve identifier. You probably will never need to alter
+/// or use this value, as it is set inside the Credential for you.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Curve {
+ // +---------+-------+----------+------------------------------------+
+ // | Name | Value | Key Type | Description |
+ // +---------+-------+----------+------------------------------------+
+ // | P-256 | 1 | EC2 | NIST P-256 also known as secp256r1 |
+ // | P-384 | 2 | EC2 | NIST P-384 also known as secp384r1 |
+ // | P-521 | 3 | EC2 | NIST P-521 also known as secp521r1 |
+ // | X25519 | 4 | OKP | X25519 for use w/ ECDH only |
+ // | X448 | 5 | OKP | X448 for use w/ ECDH only |
+ // | Ed25519 | 6 | OKP | Ed25519 for use w/ EdDSA only |
+ // | Ed448 | 7 | OKP | Ed448 for use w/ EdDSA only |
+ // +---------+-------+----------+------------------------------------+
+ /// Identifies this curve as SECP256R1 (X9_62_PRIME256V1 in OpenSSL)
+ SECP256R1 = 1,
+ /// Identifies this curve as SECP384R1
+ SECP384R1 = 2,
+ /// Identifies this curve as SECP521R1
+ SECP521R1 = 3,
+ /// Identifieds this as OKP X25519 for use w/ ECDH only
+ X25519 = 4,
+ /// Identifieds this as OKP X448 for use w/ ECDH only
+ X448 = 5,
+ /// Identifieds this as OKP Ed25519 for use w/ EdDSA only
+ Ed25519 = 6,
+ /// Identifieds this as OKP Ed448 for use w/ EdDSA only
+ Ed448 = 7,
+}
+
+impl Serialize for Curve {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_i64(*self as i64)
+ }
+}
+
+impl TryFrom<i64> for Curve {
+ type Error = CryptoError;
+ fn try_from(i: i64) -> Result<Self, Self::Error> {
+ match i {
+ i if i == Curve::SECP256R1 as i64 => Ok(Curve::SECP256R1),
+ i if i == Curve::SECP384R1 as i64 => Ok(Curve::SECP384R1),
+ i if i == Curve::SECP521R1 as i64 => Ok(Curve::SECP521R1),
+ i if i == Curve::X25519 as i64 => Ok(Curve::X25519),
+ i if i == Curve::X448 as i64 => Ok(Curve::X448),
+ i if i == Curve::Ed25519 as i64 => Ok(Curve::Ed25519),
+ i if i == Curve::Ed448 as i64 => Ok(Curve::Ed448),
+ _ => Err(CryptoError::UnknownKeyType),
+ }
+ }
+}
+/// A COSE signature algorithm, indicating the type of key and hash type
+/// that should be used.
+/// see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms
+#[rustfmt::skip]
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum COSEAlgorithm {
+ // /// Identifies this key as ECDSA (recommended SECP256R1) with SHA256 hashing
+ // //#[serde(alias = "ECDSA_SHA256")]
+ // ES256 = -7, // recommends curve SECP256R1
+ // /// Identifies this key as ECDSA (recommended SECP384R1) with SHA384 hashing
+ // //#[serde(alias = "ECDSA_SHA384")]
+ // ES384 = -35, // recommends curve SECP384R1
+ // /// Identifies this key as ECDSA (recommended SECP521R1) with SHA512 hashing
+ // //#[serde(alias = "ECDSA_SHA512")]
+ // ES512 = -36, // recommends curve SECP521R1
+ // /// Identifies this key as RS256 aka RSASSA-PKCS1-v1_5 w/ SHA-256
+ // RS256 = -257,
+ // /// Identifies this key as RS384 aka RSASSA-PKCS1-v1_5 w/ SHA-384
+ // RS384 = -258,
+ // /// Identifies this key as RS512 aka RSASSA-PKCS1-v1_5 w/ SHA-512
+ // RS512 = -259,
+ // /// Identifies this key as PS256 aka RSASSA-PSS w/ SHA-256
+ // PS256 = -37,
+ // /// Identifies this key as PS384 aka RSASSA-PSS w/ SHA-384
+ // PS384 = -38,
+ // /// Identifies this key as PS512 aka RSASSA-PSS w/ SHA-512
+ // PS512 = -39,
+ // /// Identifies this key as EdDSA (likely curve ed25519)
+ // EDDSA = -8,
+ // /// Identifies this as an INSECURE RS1 aka RSASSA-PKCS1-v1_5 using SHA-1. This is not
+ // /// used by validators, but can exist in some windows hello tpm's
+ // INSECURE_RS1 = -65535,
+ INSECURE_RS1 = -65535, // RSASSA-PKCS1-v1_5 using SHA-1
+ RS512 = -259, // RSASSA-PKCS1-v1_5 using SHA-512
+ RS384 = -258, // RSASSA-PKCS1-v1_5 using SHA-384
+ RS256 = -257, // RSASSA-PKCS1-v1_5 using SHA-256
+ ES256K = -47, // ECDSA using secp256k1 curve and SHA-256
+ HSS_LMS = -46, // HSS/LMS hash-based digital signature
+ SHAKE256 = -45, // SHAKE-256 512-bit Hash Value
+ SHA512 = -44, // SHA-2 512-bit Hash
+ SHA384 = -43, // SHA-2 384-bit Hash
+ RSAES_OAEP_SHA_512 = -42, // RSAES-OAEP w/ SHA-512
+ RSAES_OAEP_SHA_256 = -41, // RSAES-OAEP w/ SHA-256
+ RSAES_OAEP_RFC_8017_default = -40, // RSAES-OAEP w/ SHA-1
+ PS512 = -39, // RSASSA-PSS w/ SHA-512
+ PS384 = -38, // RSASSA-PSS w/ SHA-384
+ PS256 = -37, // RSASSA-PSS w/ SHA-256
+ ES512 = -36, // ECDSA w/ SHA-512
+ ES384 = -35, // ECDSA w/ SHA-384
+ ECDH_SS_A256KW = -34, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key
+ ECDH_SS_A192KW = -33, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key
+ ECDH_SS_A128KW = -32, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key
+ ECDH_ES_A256KW = -31, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key
+ ECDH_ES_A192KW = -30, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key
+ ECDH_ES_A128KW = -29, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key
+ ECDH_SS_HKDF512 = -28, // ECDH SS w/ HKDF - generate key directly
+ ECDH_SS_HKDF256 = -27, // ECDH SS w/ HKDF - generate key directly
+ ECDH_ES_HKDF512 = -26, // ECDH ES w/ HKDF - generate key directly
+ ECDH_ES_HKDF256 = -25, // ECDH ES w/ HKDF - generate key directly
+ SHAKE128 = -18, // SHAKE-128 256-bit Hash Value
+ SHA512_256 = -17, // SHA-2 512-bit Hash truncated to 256-bits
+ SHA256 = -16, // SHA-2 256-bit Hash
+ SHA256_64 = -15, // SHA-2 256-bit Hash truncated to 64-bits
+ SHA1 = -14, // SHA-1 Hash
+ Direct_HKDF_AES256 = -13, // Shared secret w/ AES-MAC 256-bit key
+ Direct_HKDF_AES128 = -12, // Shared secret w/ AES-MAC 128-bit key
+ Direct_HKDF_SHA512 = -11, // Shared secret w/ HKDF and SHA-512
+ Direct_HKDF_SHA256 = -10, // Shared secret w/ HKDF and SHA-256
+ EDDSA = -8, // EdDSA
+ ES256 = -7, // ECDSA w/ SHA-256
+ Direct = -6, // Direct use of CEK
+ A256KW = -5, // AES Key Wrap w/ 256-bit key
+ A192KW = -4, // AES Key Wrap w/ 192-bit key
+ A128KW = -3, // AES Key Wrap w/ 128-bit key
+ A128GCM = 1, // AES-GCM mode w/ 128-bit key, 128-bit tag
+ A192GCM = 2, // AES-GCM mode w/ 192-bit key, 128-bit tag
+ A256GCM = 3, // AES-GCM mode w/ 256-bit key, 128-bit tag
+ HMAC256_64 = 4, // HMAC w/ SHA-256 truncated to 64 bits
+ HMAC256_256 = 5, // HMAC w/ SHA-256
+ HMAC384_384 = 6, // HMAC w/ SHA-384
+ HMAC512_512 = 7, // HMAC w/ SHA-512
+ AES_CCM_16_64_128 = 10, // AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce
+ AES_CCM_16_64_256 = 11, // AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce
+ AES_CCM_64_64_128 = 12, // AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce
+ AES_CCM_64_64_256 = 13, // AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce
+ AES_MAC_128_64 = 14, // AES-MAC 128-bit key, 64-bit tag
+ AES_MAC_256_64 = 15, // AES-MAC 256-bit key, 64-bit tag
+ ChaCha20_Poly1305 = 24, // ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag
+ AES_MAC_128_128 = 25, // AES-MAC 128-bit key, 128-bit tag
+ AES_MAC_256_128 = 26, // AES-MAC 256-bit key, 128-bit tag
+ AES_CCM_16_128_128 = 30, // AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce
+ AES_CCM_16_128_256 = 31, // AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce
+ AES_CCM_64_128_128 = 32, // AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce
+ AES_CCM_64_128_256 = 33, // AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce
+ IV_GENERATION = 34, // For doing IV generation for symmetric algorithms.
+}
+
+impl Serialize for COSEAlgorithm {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_i64(*self as i64)
+ }
+}
+
+impl<'de> Deserialize<'de> for COSEAlgorithm {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct COSEAlgorithmVisitor;
+
+ impl<'de> Visitor<'de> for COSEAlgorithmVisitor {
+ type Value = COSEAlgorithm;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a signed integer")
+ }
+
+ fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ COSEAlgorithm::try_from(v).map_err(|_| {
+ SerdeError::invalid_value(Unexpected::Signed(v), &"valid COSEAlgorithm")
+ })
+ }
+ }
+
+ deserializer.deserialize_any(COSEAlgorithmVisitor)
+ }
+}
+
+impl TryFrom<i64> for COSEAlgorithm {
+ type Error = CryptoError;
+ fn try_from(i: i64) -> Result<Self, Self::Error> {
+ match i {
+ i if i == COSEAlgorithm::RS512 as i64 => Ok(COSEAlgorithm::RS512),
+ i if i == COSEAlgorithm::RS384 as i64 => Ok(COSEAlgorithm::RS384),
+ i if i == COSEAlgorithm::RS256 as i64 => Ok(COSEAlgorithm::RS256),
+ i if i == COSEAlgorithm::ES256K as i64 => Ok(COSEAlgorithm::ES256K),
+ i if i == COSEAlgorithm::HSS_LMS as i64 => Ok(COSEAlgorithm::HSS_LMS),
+ i if i == COSEAlgorithm::SHAKE256 as i64 => Ok(COSEAlgorithm::SHAKE256),
+ i if i == COSEAlgorithm::SHA512 as i64 => Ok(COSEAlgorithm::SHA512),
+ i if i == COSEAlgorithm::SHA384 as i64 => Ok(COSEAlgorithm::SHA384),
+ i if i == COSEAlgorithm::RSAES_OAEP_SHA_512 as i64 => {
+ Ok(COSEAlgorithm::RSAES_OAEP_SHA_512)
+ }
+ i if i == COSEAlgorithm::RSAES_OAEP_SHA_256 as i64 => {
+ Ok(COSEAlgorithm::RSAES_OAEP_SHA_256)
+ }
+ i if i == COSEAlgorithm::RSAES_OAEP_RFC_8017_default as i64 => {
+ Ok(COSEAlgorithm::RSAES_OAEP_RFC_8017_default)
+ }
+ i if i == COSEAlgorithm::PS512 as i64 => Ok(COSEAlgorithm::PS512),
+ i if i == COSEAlgorithm::PS384 as i64 => Ok(COSEAlgorithm::PS384),
+ i if i == COSEAlgorithm::PS256 as i64 => Ok(COSEAlgorithm::PS256),
+ i if i == COSEAlgorithm::ES512 as i64 => Ok(COSEAlgorithm::ES512),
+ i if i == COSEAlgorithm::ES384 as i64 => Ok(COSEAlgorithm::ES384),
+ i if i == COSEAlgorithm::ECDH_SS_A256KW as i64 => Ok(COSEAlgorithm::ECDH_SS_A256KW),
+ i if i == COSEAlgorithm::ECDH_SS_A192KW as i64 => Ok(COSEAlgorithm::ECDH_SS_A192KW),
+ i if i == COSEAlgorithm::ECDH_SS_A128KW as i64 => Ok(COSEAlgorithm::ECDH_SS_A128KW),
+ i if i == COSEAlgorithm::ECDH_ES_A256KW as i64 => Ok(COSEAlgorithm::ECDH_ES_A256KW),
+ i if i == COSEAlgorithm::ECDH_ES_A192KW as i64 => Ok(COSEAlgorithm::ECDH_ES_A192KW),
+ i if i == COSEAlgorithm::ECDH_ES_A128KW as i64 => Ok(COSEAlgorithm::ECDH_ES_A128KW),
+ i if i == COSEAlgorithm::ECDH_SS_HKDF512 as i64 => Ok(COSEAlgorithm::ECDH_SS_HKDF512),
+ i if i == COSEAlgorithm::ECDH_SS_HKDF256 as i64 => Ok(COSEAlgorithm::ECDH_SS_HKDF256),
+ i if i == COSEAlgorithm::ECDH_ES_HKDF512 as i64 => Ok(COSEAlgorithm::ECDH_ES_HKDF512),
+ i if i == COSEAlgorithm::ECDH_ES_HKDF256 as i64 => Ok(COSEAlgorithm::ECDH_ES_HKDF256),
+ i if i == COSEAlgorithm::SHAKE128 as i64 => Ok(COSEAlgorithm::SHAKE128),
+ i if i == COSEAlgorithm::SHA512_256 as i64 => Ok(COSEAlgorithm::SHA512_256),
+ i if i == COSEAlgorithm::SHA256 as i64 => Ok(COSEAlgorithm::SHA256),
+ i if i == COSEAlgorithm::SHA256_64 as i64 => Ok(COSEAlgorithm::SHA256_64),
+ i if i == COSEAlgorithm::SHA1 as i64 => Ok(COSEAlgorithm::SHA1),
+ i if i == COSEAlgorithm::Direct_HKDF_AES256 as i64 => {
+ Ok(COSEAlgorithm::Direct_HKDF_AES256)
+ }
+ i if i == COSEAlgorithm::Direct_HKDF_AES128 as i64 => {
+ Ok(COSEAlgorithm::Direct_HKDF_AES128)
+ }
+ i if i == COSEAlgorithm::Direct_HKDF_SHA512 as i64 => {
+ Ok(COSEAlgorithm::Direct_HKDF_SHA512)
+ }
+ i if i == COSEAlgorithm::Direct_HKDF_SHA256 as i64 => {
+ Ok(COSEAlgorithm::Direct_HKDF_SHA256)
+ }
+ i if i == COSEAlgorithm::EDDSA as i64 => Ok(COSEAlgorithm::EDDSA),
+ i if i == COSEAlgorithm::ES256 as i64 => Ok(COSEAlgorithm::ES256),
+ i if i == COSEAlgorithm::Direct as i64 => Ok(COSEAlgorithm::Direct),
+ i if i == COSEAlgorithm::A256KW as i64 => Ok(COSEAlgorithm::A256KW),
+ i if i == COSEAlgorithm::A192KW as i64 => Ok(COSEAlgorithm::A192KW),
+ i if i == COSEAlgorithm::A128KW as i64 => Ok(COSEAlgorithm::A128KW),
+ i if i == COSEAlgorithm::A128GCM as i64 => Ok(COSEAlgorithm::A128GCM),
+ i if i == COSEAlgorithm::A192GCM as i64 => Ok(COSEAlgorithm::A192GCM),
+ i if i == COSEAlgorithm::A256GCM as i64 => Ok(COSEAlgorithm::A256GCM),
+ i if i == COSEAlgorithm::HMAC256_64 as i64 => Ok(COSEAlgorithm::HMAC256_64),
+ i if i == COSEAlgorithm::HMAC256_256 as i64 => Ok(COSEAlgorithm::HMAC256_256),
+ i if i == COSEAlgorithm::HMAC384_384 as i64 => Ok(COSEAlgorithm::HMAC384_384),
+ i if i == COSEAlgorithm::HMAC512_512 as i64 => Ok(COSEAlgorithm::HMAC512_512),
+ i if i == COSEAlgorithm::AES_CCM_16_64_128 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_16_64_128)
+ }
+ i if i == COSEAlgorithm::AES_CCM_16_64_256 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_16_64_256)
+ }
+ i if i == COSEAlgorithm::AES_CCM_64_64_128 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_64_64_128)
+ }
+ i if i == COSEAlgorithm::AES_CCM_64_64_256 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_64_64_256)
+ }
+ i if i == COSEAlgorithm::AES_MAC_128_64 as i64 => Ok(COSEAlgorithm::AES_MAC_128_64),
+ i if i == COSEAlgorithm::AES_MAC_256_64 as i64 => Ok(COSEAlgorithm::AES_MAC_256_64),
+ i if i == COSEAlgorithm::ChaCha20_Poly1305 as i64 => {
+ Ok(COSEAlgorithm::ChaCha20_Poly1305)
+ }
+ i if i == COSEAlgorithm::AES_MAC_128_128 as i64 => Ok(COSEAlgorithm::AES_MAC_128_128),
+ i if i == COSEAlgorithm::AES_MAC_256_128 as i64 => Ok(COSEAlgorithm::AES_MAC_256_128),
+ i if i == COSEAlgorithm::AES_CCM_16_128_128 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_16_128_128)
+ }
+ i if i == COSEAlgorithm::AES_CCM_16_128_256 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_16_128_256)
+ }
+ i if i == COSEAlgorithm::AES_CCM_64_128_128 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_64_128_128)
+ }
+ i if i == COSEAlgorithm::AES_CCM_64_128_256 as i64 => {
+ Ok(COSEAlgorithm::AES_CCM_64_128_256)
+ }
+ i if i == COSEAlgorithm::IV_GENERATION as i64 => Ok(COSEAlgorithm::IV_GENERATION),
+ i if i == COSEAlgorithm::INSECURE_RS1 as i64 => Ok(COSEAlgorithm::INSECURE_RS1),
+ _ => Err(CryptoError::UnknownAlgorithm),
+ }
+ }
+}
+
+/// A COSE Elliptic Curve Public Key. This is generally the provided credential
+/// that an authenticator registers, and is used to authenticate the user.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSEEC2Key {
+ /// The curve that this key references.
+ pub curve: Curve,
+ /// The key's public X coordinate.
+ pub x: Vec<u8>,
+ /// The key's public Y coordinate.
+ pub y: Vec<u8>,
+}
+
+impl COSEEC2Key {
+ // The SEC 1 uncompressed point format is "0x04 || x coordinate || y coordinate".
+ // See Section 2.3.3 of "SEC 1: Elliptic Curve Cryptography" https://www.secg.org/sec1-v2.pdf.
+ pub fn from_sec1_uncompressed(curve: Curve, key: &[u8]) -> Result<Self, CryptoError> {
+ if !(curve == Curve::SECP256R1 && key.len() == 65) {
+ return Err(CryptoError::UnsupportedCurve(curve));
+ }
+ if key[0] != 0x04 {
+ return Err(CryptoError::MalformedInput);
+ }
+ let key = &key[1..];
+ let (x, y) = key.split_at(key.len() / 2);
+ Ok(COSEEC2Key {
+ curve,
+ x: x.to_vec(),
+ y: y.to_vec(),
+ })
+ }
+
+ pub fn der_spki(&self) -> Result<Vec<u8>, CryptoError> {
+ if self.curve != Curve::SECP256R1 {
+ return Err(CryptoError::UnsupportedCurve(self.curve));
+ }
+
+ // SubjectPublicKeyInfo
+ der::sequence(&[
+ // algorithm: AlgorithmIdentifier
+ &der::sequence(&[
+ // algorithm
+ &der::object_id(der::OID_EC_PUBLIC_KEY_BYTES)?,
+ // parameters
+ &der::object_id(der::OID_SECP256R1_BYTES)?,
+ ])?,
+ // subjectPublicKey
+ &der::bit_string(
+ // SEC 1 uncompressed format
+ &[&[0x04], self.x.as_slice(), self.y.as_slice()].concat(),
+ )?,
+ ])
+ }
+}
+
+/// A Octet Key Pair (OKP).
+/// The other version uses only the x-coordinate as the y-coordinate is
+/// either to be recomputed or not needed for the key agreement operation ('OKP').
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSEOKPKey {
+ /// The curve that this key references.
+ pub curve: Curve,
+ /// The key's public X coordinate.
+ pub x: Vec<u8>,
+}
+
+impl COSEOKPKey {
+ pub fn der_spki(&self) -> Result<Vec<u8>, CryptoError> {
+ if self.curve != Curve::Ed25519 {
+ return Err(CryptoError::UnsupportedCurve(self.curve));
+ }
+
+ // SubjectPublicKeyInfo
+ der::sequence(&[
+ // algorithm: AlgorithmIdentifier
+ &der::sequence(&[
+ // algorithm
+ &der::object_id(der::OID_ED25519_BYTES)?,
+ // parameters
+ // (absent as per RFC 8410)
+ ])?,
+ // subjectPublicKey
+ &der::bit_string(
+ // RFC 8410
+ self.x.as_slice(),
+ )?,
+ ])
+ }
+}
+
+/// A COSE RSA PublicKey. This is a provided credential from a registered authenticator.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSERSAKey {
+ /// An RSA modulus
+ pub n: Vec<u8>,
+ /// An RSA exponent
+ pub e: Vec<u8>,
+}
+
+impl COSERSAKey {
+ pub fn der_spki(&self) -> Result<Vec<u8>, CryptoError> {
+ // SubjectPublicKeyInfo
+ der::sequence(&[
+ // algorithm: AlgorithmIdentifier
+ &der::sequence(&[
+ // algorithm
+ &der::object_id(der::OID_RS256_BYTES)?,
+ // parameters
+ &der::null()?,
+ ])?,
+ // subjectPublicKey
+ &der::bit_string(
+ // RFC 4055 RSAPublicKey
+ &der::sequence(&[&der::integer(&self.n)?, &der::integer(&self.e)?])?,
+ )?,
+ ])
+ }
+}
+
+// https://tools.ietf.org/html/rfc8152#section-13
+#[allow(non_camel_case_types)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum COSEKeyTypeId {
+ // Reserved is invalid
+ // Reserved = 0,
+ /// Octet Key Pair
+ OKP = 1,
+ /// Elliptic Curve Keys w/ x- and y-coordinate
+ EC2 = 2,
+ /// RSA
+ RSA = 3,
+}
+
+impl Serialize for COSEKeyTypeId {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_i64(*self as i64)
+ }
+}
+
+impl TryFrom<i64> for COSEKeyTypeId {
+ type Error = CryptoError;
+ fn try_from(i: i64) -> Result<Self, Self::Error> {
+ match i {
+ i if i == COSEKeyTypeId::OKP as i64 => Ok(COSEKeyTypeId::OKP),
+ i if i == COSEKeyTypeId::EC2 as i64 => Ok(COSEKeyTypeId::EC2),
+ i if i == COSEKeyTypeId::RSA as i64 => Ok(COSEKeyTypeId::RSA),
+ _ => Err(CryptoError::UnknownKeyType),
+ }
+ }
+}
+
+/// The type of Key contained within a COSE value. You should never need
+/// to alter or change this type.
+#[allow(non_camel_case_types)]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum COSEKeyType {
+ /// Identifies this as an Elliptic Curve EC2 key
+ EC2(COSEEC2Key),
+ /// Identifies this as an Elliptic Curve octet key pair
+ OKP(COSEOKPKey),
+ /// Identifies this as an RSA key
+ RSA(COSERSAKey),
+}
+
+/// A COSE Key as provided by the Authenticator. You should never need
+/// to alter or change these values.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSEKey {
+ /// COSE signature algorithm, indicating the type of key and hash type
+ /// that should be used.
+ pub alg: COSEAlgorithm,
+ /// The public key
+ pub key: COSEKeyType,
+}
+
+impl COSEKey {
+ /// Generates a new key pair for the specified algorithm.
+ /// Returns an PKCS#8 encoding of the private key, and the public key as a COSEKey.
+ pub fn generate(alg: COSEAlgorithm) -> Result<(Vec<u8>, Self), CryptoError> {
+ if alg != COSEAlgorithm::ES256 && alg != COSEAlgorithm::ECDH_ES_HKDF256 {
+ return Err(CryptoError::UnsupportedAlgorithm(alg));
+ }
+ let (private, public) = gen_p256()?;
+ let cose_ec2_key = COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &public)?;
+ let public = COSEKey {
+ alg,
+ key: COSEKeyType::EC2(cose_ec2_key),
+ };
+ Ok((private, public))
+ }
+
+ pub fn der_spki(&self) -> Result<Vec<u8>, CryptoError> {
+ match &self.key {
+ COSEKeyType::EC2(ec2_key) => ec2_key.der_spki(),
+ COSEKeyType::OKP(okp_key) => okp_key.der_spki(),
+ COSEKeyType::RSA(rsa_key) => rsa_key.der_spki(),
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for COSEKey {
+ fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct COSEKeyVisitor;
+
+ impl<'de> Visitor<'de> for COSEKeyVisitor {
+ type Value = COSEKey;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut key_type: Option<COSEKeyTypeId> = None;
+ let mut alg: Option<COSEAlgorithm> = None;
+ // OKP / EC2
+ let mut curve: Option<Curve> = None;
+ let mut x: Option<Vec<u8>> = None;
+ let mut y: Option<Vec<u8>> = None;
+
+ // RSA specific
+ let mut n: Option<Vec<u8>> = None;
+ let mut e: Option<Vec<u8>> = None;
+
+ while let Some(key) = map.next_key()? {
+ // See https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
+ match key {
+ 1 => {
+ if key_type.is_some() {
+ return Err(SerdeError::duplicate_field("key_type"));
+ }
+ let value: i64 = map.next_value()?;
+ let val = COSEKeyTypeId::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported key_type {value}"))
+ })?;
+ key_type = Some(val);
+ }
+ 3 => {
+ if alg.is_some() {
+ return Err(SerdeError::duplicate_field("alg"));
+ }
+ let value: i64 = map.next_value()?;
+ let val = COSEAlgorithm::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported algorithm {value}"))
+ })?;
+ alg = Some(val);
+ }
+ -1 => match key_type {
+ None => return Err(SerdeError::missing_field("key_type")),
+ Some(COSEKeyTypeId::OKP) | Some(COSEKeyTypeId::EC2) => {
+ if curve.is_some() {
+ return Err(SerdeError::duplicate_field("curve"));
+ }
+ let value: i64 = map.next_value()?;
+ let val = Curve::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported curve {value}"))
+ })?;
+ curve = Some(val);
+ }
+ Some(COSEKeyTypeId::RSA) => {
+ if n.is_some() {
+ return Err(SerdeError::duplicate_field("n"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ n = Some(value.to_vec());
+ }
+ },
+ -2 => match key_type {
+ None => return Err(SerdeError::missing_field("key_type")),
+ Some(COSEKeyTypeId::OKP) | Some(COSEKeyTypeId::EC2) => {
+ if x.is_some() {
+ return Err(SerdeError::duplicate_field("x"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ x = Some(value.to_vec());
+ }
+ Some(COSEKeyTypeId::RSA) => {
+ if e.is_some() {
+ return Err(SerdeError::duplicate_field("e"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ e = Some(value.to_vec());
+ }
+ },
+ -3 if key_type == Some(COSEKeyTypeId::EC2) => {
+ if y.is_some() {
+ return Err(SerdeError::duplicate_field("y"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ y = Some(value.to_vec());
+ }
+ other => {
+ return Err(SerdeError::custom(format!("unexpected field: {other}")));
+ }
+ };
+ }
+
+ let key_type = key_type.ok_or_else(|| SerdeError::missing_field("key_type (1)"))?;
+ let alg = alg.ok_or_else(|| SerdeError::missing_field("alg (3)"))?;
+
+ let res = match key_type {
+ COSEKeyTypeId::OKP => {
+ let curve = curve.ok_or_else(|| SerdeError::missing_field("curve (-1)"))?;
+ let x = x.ok_or_else(|| SerdeError::missing_field("x (-2)"))?;
+ COSEKeyType::OKP(COSEOKPKey { curve, x })
+ }
+ COSEKeyTypeId::EC2 => {
+ let curve = curve.ok_or_else(|| SerdeError::missing_field("curve (-1)"))?;
+ let x = x.ok_or_else(|| SerdeError::missing_field("x (-2)"))?;
+ let y = y.ok_or_else(|| SerdeError::missing_field("y (-3)"))?;
+ COSEKeyType::EC2(COSEEC2Key { curve, x, y })
+ }
+ COSEKeyTypeId::RSA => {
+ let n = n.ok_or_else(|| SerdeError::missing_field("n (-1)"))?;
+ let e = e.ok_or_else(|| SerdeError::missing_field("e (-2)"))?;
+ COSEKeyType::RSA(COSERSAKey { e, n })
+ }
+ };
+ Ok(COSEKey { alg, key: res })
+ }
+ }
+
+ deserializer.deserialize_bytes(COSEKeyVisitor)
+ }
+}
+
+impl Serialize for COSEKey {
+ fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let map_len = match &self.key {
+ COSEKeyType::OKP(_) => 4,
+ COSEKeyType::EC2(_) => 5,
+ COSEKeyType::RSA(_) => 4,
+ };
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ match &self.key {
+ COSEKeyType::OKP(key) => {
+ map.serialize_entry(&1, &COSEKeyTypeId::OKP)?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &key.curve)?;
+ map.serialize_entry(&-2, &serde_bytes::Bytes::new(&key.x))?;
+ }
+ COSEKeyType::EC2(key) => {
+ map.serialize_entry(&1, &COSEKeyTypeId::EC2)?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &key.curve)?;
+ map.serialize_entry(&-2, &serde_bytes::Bytes::new(&key.x))?;
+ map.serialize_entry(&-3, &serde_bytes::Bytes::new(&key.y))?;
+ }
+ COSEKeyType::RSA(key) => {
+ map.serialize_entry(&1, &COSEKeyTypeId::RSA)?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &serde_bytes::Bytes::new(&key.n))?;
+ map.serialize_entry(&-2, &serde_bytes::Bytes::new(&key.e))?;
+ }
+ }
+
+ map.end()
+ }
+}
+
+/// Errors that can be returned from COSE functions.
+#[derive(Debug, Clone, Serialize)]
+pub enum CryptoError {
+ // DecodingFailure,
+ LibraryFailure,
+ MalformedInput,
+ // MissingHeader,
+ // UnexpectedHeaderValue,
+ // UnexpectedTag,
+ // UnexpectedType,
+ // Unimplemented,
+ // VerificationFailed,
+ // SigningFailed,
+ // InvalidArgument,
+ UnknownKeyType,
+ UnknownSignatureScheme,
+ UnknownAlgorithm,
+ WrongSaltLength,
+ UnsupportedAlgorithm(COSEAlgorithm),
+ UnsupportedCurve(Curve),
+ UnsupportedKeyType,
+ Backend(String),
+}
+
+impl From<CryptoError> for CommandError {
+ fn from(e: CryptoError) -> Self {
+ CommandError::Crypto(e)
+ }
+}
+
+impl From<CryptoError> for AuthenticatorError {
+ fn from(e: CryptoError) -> Self {
+ AuthenticatorError::HIDError(HIDError::Command(CommandError::Crypto(e)))
+ }
+}
+
+pub struct U2FRegisterAnswer<'a> {
+ pub certificate: &'a [u8],
+ pub signature: &'a [u8],
+}
+
+// We will only return MalformedInput here
+pub fn parse_u2f_der_certificate(data: &[u8]) -> Result<U2FRegisterAnswer, CryptoError> {
+ // So we don't panic below, when accessing individual bytes
+ if data.len() < 4 {
+ return Err(CryptoError::MalformedInput);
+ }
+ // Check if it is a SEQUENCE
+ if data[0] != 0x30 {
+ return Err(CryptoError::MalformedInput);
+ }
+
+ // This algorithm is taken from mozilla-central/security/nss/lib/mozpkix/lib/pkixder.cpp
+ // The short form of length is a single byte with the high order bit set
+ // to zero. The long form of length is one byte with the high order bit
+ // set, followed by N bytes, where N is encoded in the lowest 7 bits of
+ // the first byte.
+ let end = if (data[1] & 0x80) == 0 {
+ 2 + data[1] as usize
+ } else if data[1] == 0x81 {
+ // The next byte specifies the length
+
+ if data[2] < 128 {
+ // Not shortest possible encoding
+ // Forbidden by DER-format
+ return Err(CryptoError::MalformedInput);
+ }
+ 3 + data[2] as usize
+ } else if data[1] == 0x82 {
+ // The next 2 bytes specify the length
+ let l = u16::from_be_bytes([data[2], data[3]]);
+ if l < 256 {
+ // Not shortest possible encoding
+ // Forbidden by DER-format
+ return Err(CryptoError::MalformedInput);
+ }
+ 4 + l as usize
+ } else {
+ // We don't support lengths larger than 2^16 - 1.
+ return Err(CryptoError::MalformedInput);
+ };
+
+ if data.len() < end {
+ return Err(CryptoError::MalformedInput);
+ }
+
+ Ok(U2FRegisterAnswer {
+ certificate: &data[0..end],
+ signature: &data[end..],
+ })
+}
+
+#[cfg(all(test, not(feature = "crypto_dummy")))]
+mod test {
+ use std::convert::TryFrom;
+
+ #[cfg(feature = "crypto_nss")]
+ use super::backend::{ecdsa_p256_sha256_sign_raw, test_ecdsa_p256_sha256_verify_raw};
+ use super::{
+ backend::hmac_sha256, backend::sha256, backend::test_ecdh_p256_raw, COSEAlgorithm, COSEKey,
+ Curve, PinProtocolImpl, PinUvAuth1, PinUvAuth2, PinUvAuthProtocol, PublicInputs,
+ SharedSecret,
+ };
+ use crate::crypto::{COSEEC2Key, COSEKeyType, COSEOKPKey, COSERSAKey};
+ use crate::ctap2::attestation::AAGuid;
+ use crate::ctap2::commands::client_pin::Pin;
+ use crate::ctap2::commands::get_info::{
+ tests::AAGUID_RAW, AuthenticatorOptions, AuthenticatorVersion,
+ };
+ use crate::util::decode_hex;
+ use crate::AuthenticatorInfo;
+ use serde_cbor::de::from_slice;
+
+ // Extracted from RFC 8410 Example 10.1
+ const SAMPLE_ED25519_KEY: &[u8] = &[
+ 0x19, 0xbf, 0x44, 0x09, 0x69, 0x84, 0xcd, 0xfe, 0x85, 0x41, 0xba, 0xc1, 0x67, 0xdc, 0x3b,
+ 0x96, 0xc8, 0x50, 0x86, 0xaa, 0x30, 0xb6, 0xb6, 0xcb, 0x0c, 0x5c, 0x38, 0xad, 0x70, 0x31,
+ 0x66, 0xe1,
+ ];
+
+ const SAMPLE_P256_X: &[u8] = &[
+ 0xfc, 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0, 0x75, 0x67,
+ 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d, 0x33, 0x05, 0xe3,
+ 0x1a, 0x80,
+ ];
+ const SAMPLE_P256_Y: &[u8] = &[
+ 0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda, 0x8d, 0xe0, 0xac, 0xf9, 0xd8, 0xe1,
+ 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73, 0xd4, 0xd3, 0x2c, 0x9a, 0xad, 0x6d, 0xfa,
+ 0x8b, 0x27,
+ ];
+
+ const SAMPLE_RSA_MODULUS: &[u8] = &[
+ 0xd4, 0xd2, 0x53, 0xed, 0x7a, 0x69, 0xb1, 0x84, 0xc9, 0xfb, 0x70, 0x30, 0x0c, 0x51, 0xb1,
+ 0x8f, 0x89, 0x6c, 0xb1, 0x31, 0x6d, 0x87, 0xbe, 0xe1, 0xc7, 0xf7, 0xb0, 0x4f, 0xe7, 0x27,
+ 0xa7, 0xb7, 0x7c, 0x55, 0x20, 0x37, 0xa8, 0xac, 0x40, 0xf4, 0xbc, 0x59, 0xc4, 0x92, 0x8f,
+ 0x13, 0x5b, 0x5e, 0xa7, 0x18, 0x05, 0xcc, 0xd7, 0x9c, 0xfb, 0x88, 0x6c, 0xf1, 0xbc, 0x6b,
+ 0x1b, 0x8d, 0xb7, 0x8d, 0x2d, 0xaa, 0xcb, 0xee, 0xdb, 0xab, 0x49, 0x36, 0x77, 0xe5, 0xd1,
+ 0x84, 0xa1, 0x40, 0x3f, 0xf6, 0xf7, 0x98, 0x6c, 0xaa, 0x24, 0x48, 0x30, 0x44, 0xdc, 0x68,
+ 0xbd, 0x9e, 0x74, 0x37, 0xaf, 0x27, 0x12, 0x90, 0x74, 0x0d, 0x9e, 0x3c, 0xa5, 0x3a, 0x1d,
+ 0xb8, 0x54, 0x92, 0xd4, 0x6d, 0x1f, 0xf9, 0x39, 0xb8, 0x1d, 0x8a, 0x5e, 0xbe, 0x12, 0xbd,
+ 0xe2, 0x9c, 0xf2, 0x5a, 0x48, 0x5d, 0x71, 0x2c, 0x71, 0x72, 0x6d, 0xd2, 0xcb, 0x37, 0xb1,
+ 0xe6, 0x2f, 0x76, 0x43, 0xda, 0xca, 0x44, 0x30, 0x7b, 0x28, 0xe7, 0xe4, 0xec, 0xa9, 0xc9,
+ 0x1a, 0x5f, 0xe5, 0x51, 0x03, 0x25, 0x60, 0x7c, 0x5a, 0x69, 0x12, 0x4d, 0x50, 0xfd, 0xb2,
+ 0xb8, 0x6e, 0x13, 0xb2, 0x92, 0xda, 0x0e, 0x31, 0xc9, 0xf1, 0x9c, 0xde, 0x17, 0x63, 0xe4,
+ 0xcb, 0xac, 0xd5, 0xee, 0x84, 0x06, 0xde, 0x67, 0x2d, 0xb8, 0xd2, 0xe1, 0x4b, 0xbb, 0x49,
+ 0xea, 0x45, 0xd4, 0xa1, 0x7f, 0x46, 0xf2, 0xd6, 0x0c, 0x05, 0x9d, 0x1d, 0x1a, 0x99, 0x41,
+ 0x20, 0x5e, 0x1a, 0xa4, 0xcc, 0x21, 0x44, 0x58, 0x8b, 0xcd, 0x98, 0xe4, 0x3d, 0x53, 0x20,
+ 0xfc, 0xfc, 0x7b, 0x9f, 0x43, 0x35, 0xfb, 0x38, 0x37, 0x23, 0xd0, 0x76, 0xe3, 0x3d, 0x4f,
+ 0x89, 0x9b, 0x89, 0x32, 0x81, 0x89, 0xed, 0x58, 0xc0, 0x80, 0x18, 0x83, 0x5b, 0xaf, 0x5a,
+ 0xa5,
+ ];
+
+ #[test]
+ fn test_rsa_key_to_der_spki() {
+ // $ ascii2der | xxd -i
+ // SEQUENCE {
+ // SEQUENCE {
+ // # sha256WithRSAEncryption
+ // OBJECT_IDENTIFIER { 1.2.840.113549.1.1.11 }
+ // NULL {}
+ // }
+ // BIT_STRING {
+ // `00`
+ // SEQUENCE {
+ // INTEGER { `00d4d253ed7a69b184c9fb70300c51b18f896cb1316d87bee1c7f7b04fe727a7b77c552037a8ac40f4bc59c4928f135b5ea71805ccd79cfb886cf1bc6b1b8db78d2daacbeedbab493677e5d184a1403ff6f7986caa24483044dc68bd9e7437af271290740d9e3ca53a1db85492d46d1ff939b81d8a5ebe12bde29cf25a485d712c71726dd2cb37b1e62f7643daca44307b28e7e4eca9c91a5fe5510325607c5a69124d50fdb2b86e13b292da0e31c9f19cde1763e4cbacd5ee8406de672db8d2e14bbb49ea45d4a17f46f2d60c059d1d1a9941205e1aa4cc2144588bcd98e43d5320fcfc7b9f4335fb383723d076e33d4f899b89328189ed58c08018835baf5aa5` }
+ // INTEGER { 65537 }
+ // }
+ // }
+ // }
+ let expected: &[u8] = &[
+ 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
+ 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a,
+ 0x02, 0x82, 0x01, 0x01, 0x00, 0xd4, 0xd2, 0x53, 0xed, 0x7a, 0x69, 0xb1, 0x84, 0xc9,
+ 0xfb, 0x70, 0x30, 0x0c, 0x51, 0xb1, 0x8f, 0x89, 0x6c, 0xb1, 0x31, 0x6d, 0x87, 0xbe,
+ 0xe1, 0xc7, 0xf7, 0xb0, 0x4f, 0xe7, 0x27, 0xa7, 0xb7, 0x7c, 0x55, 0x20, 0x37, 0xa8,
+ 0xac, 0x40, 0xf4, 0xbc, 0x59, 0xc4, 0x92, 0x8f, 0x13, 0x5b, 0x5e, 0xa7, 0x18, 0x05,
+ 0xcc, 0xd7, 0x9c, 0xfb, 0x88, 0x6c, 0xf1, 0xbc, 0x6b, 0x1b, 0x8d, 0xb7, 0x8d, 0x2d,
+ 0xaa, 0xcb, 0xee, 0xdb, 0xab, 0x49, 0x36, 0x77, 0xe5, 0xd1, 0x84, 0xa1, 0x40, 0x3f,
+ 0xf6, 0xf7, 0x98, 0x6c, 0xaa, 0x24, 0x48, 0x30, 0x44, 0xdc, 0x68, 0xbd, 0x9e, 0x74,
+ 0x37, 0xaf, 0x27, 0x12, 0x90, 0x74, 0x0d, 0x9e, 0x3c, 0xa5, 0x3a, 0x1d, 0xb8, 0x54,
+ 0x92, 0xd4, 0x6d, 0x1f, 0xf9, 0x39, 0xb8, 0x1d, 0x8a, 0x5e, 0xbe, 0x12, 0xbd, 0xe2,
+ 0x9c, 0xf2, 0x5a, 0x48, 0x5d, 0x71, 0x2c, 0x71, 0x72, 0x6d, 0xd2, 0xcb, 0x37, 0xb1,
+ 0xe6, 0x2f, 0x76, 0x43, 0xda, 0xca, 0x44, 0x30, 0x7b, 0x28, 0xe7, 0xe4, 0xec, 0xa9,
+ 0xc9, 0x1a, 0x5f, 0xe5, 0x51, 0x03, 0x25, 0x60, 0x7c, 0x5a, 0x69, 0x12, 0x4d, 0x50,
+ 0xfd, 0xb2, 0xb8, 0x6e, 0x13, 0xb2, 0x92, 0xda, 0x0e, 0x31, 0xc9, 0xf1, 0x9c, 0xde,
+ 0x17, 0x63, 0xe4, 0xcb, 0xac, 0xd5, 0xee, 0x84, 0x06, 0xde, 0x67, 0x2d, 0xb8, 0xd2,
+ 0xe1, 0x4b, 0xbb, 0x49, 0xea, 0x45, 0xd4, 0xa1, 0x7f, 0x46, 0xf2, 0xd6, 0x0c, 0x05,
+ 0x9d, 0x1d, 0x1a, 0x99, 0x41, 0x20, 0x5e, 0x1a, 0xa4, 0xcc, 0x21, 0x44, 0x58, 0x8b,
+ 0xcd, 0x98, 0xe4, 0x3d, 0x53, 0x20, 0xfc, 0xfc, 0x7b, 0x9f, 0x43, 0x35, 0xfb, 0x38,
+ 0x37, 0x23, 0xd0, 0x76, 0xe3, 0x3d, 0x4f, 0x89, 0x9b, 0x89, 0x32, 0x81, 0x89, 0xed,
+ 0x58, 0xc0, 0x80, 0x18, 0x83, 0x5b, 0xaf, 0x5a, 0xa5, 0x02, 0x03, 0x01, 0x00, 0x01,
+ ];
+ let rsa_key = COSERSAKey {
+ e: vec![1, 0, 1],
+ n: SAMPLE_RSA_MODULUS.to_vec(),
+ };
+ let cose_key: COSEKey = COSEKey {
+ alg: COSEAlgorithm::RS256,
+ key: COSEKeyType::RSA(rsa_key),
+ };
+ let actual = cose_key.der_spki().expect("Failed to serialize to SPKI");
+ assert_eq!(expected, &actual);
+ }
+
+ #[test]
+ fn test_rsa_key_to_cbor() {
+ let key = COSERSAKey {
+ e: vec![1, 0, 1],
+ n: SAMPLE_RSA_MODULUS.to_vec(),
+ };
+ let cose_key: COSEKey = COSEKey {
+ alg: COSEAlgorithm::RS256,
+ key: COSEKeyType::RSA(key),
+ };
+ let cose_key_cbor = serde_cbor::to_vec(&cose_key).expect("Failed to serialize key");
+ let actual = serde_cbor::from_slice(&cose_key_cbor).expect("Failed to deserialize key");
+ assert_eq!(cose_key, actual);
+ }
+
+ #[test]
+ fn test_ec2_key_to_der_spki() {
+ // $ ascii2der | xxd -i
+ // SEQUENCE {
+ // SEQUENCE {
+ // # ecPublicKey
+ // OBJECT_IDENTIFIER { 1.2.840.10045.2.1 }
+ // # secp256r1
+ // OBJECT_IDENTIFIER { 1.2.840.10045.3.1.7 }
+ // }
+ // BIT_STRING { `00` `04fc9ed36f7c1aa915ce3ea177f07567f07f16f9479d95ad8ed4971d3305e31a8050b733af8c0b0ee1da8de0acf9d8e13282f063b7b30d73d4d32c9aad6dfa8b27` }
+ // }
+ let expected = [
+ 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06,
+ 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xfc,
+ 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0, 0x75, 0x67,
+ 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d, 0x33, 0x05,
+ 0xe3, 0x1a, 0x80, 0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda, 0x8d, 0xe0,
+ 0xac, 0xf9, 0xd8, 0xe1, 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73, 0xd4, 0xd3,
+ 0x2c, 0x9a, 0xad, 0x6d, 0xfa, 0x8b, 0x27,
+ ];
+ let ec2_key = COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: SAMPLE_P256_X.to_vec(),
+ y: SAMPLE_P256_Y.to_vec(),
+ };
+ let cose_key = COSEKey {
+ alg: COSEAlgorithm::EDDSA,
+ key: COSEKeyType::EC2(ec2_key),
+ };
+ let actual = cose_key.der_spki().expect("Failed to serialize key");
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn test_ec2_key_to_cbor() {
+ let ec2_key = COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: SAMPLE_P256_X.to_vec(),
+ y: SAMPLE_P256_Y.to_vec(),
+ };
+ let cose_key = COSEKey {
+ alg: COSEAlgorithm::EDDSA,
+ key: COSEKeyType::EC2(ec2_key),
+ };
+ let cose_key_cbor = serde_cbor::to_vec(&cose_key).expect("Failed to serialize key");
+ let actual = serde_cbor::from_slice(&cose_key_cbor).expect("Failed to deserialize key");
+ assert_eq!(cose_key, actual);
+ }
+
+ #[test]
+ fn test_okp_key_to_der_spki() {
+ // RFC 8410 Example 10.1
+ let expected = [
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, 0x19, 0xbf,
+ 0x44, 0x09, 0x69, 0x84, 0xcd, 0xfe, 0x85, 0x41, 0xba, 0xc1, 0x67, 0xdc, 0x3b, 0x96,
+ 0xc8, 0x50, 0x86, 0xaa, 0x30, 0xb6, 0xb6, 0xcb, 0x0c, 0x5c, 0x38, 0xad, 0x70, 0x31,
+ 0x66, 0xe1,
+ ];
+ let okp_key = COSEOKPKey {
+ curve: Curve::Ed25519,
+ x: SAMPLE_ED25519_KEY.to_vec(),
+ };
+ let cose_key = COSEKey {
+ alg: COSEAlgorithm::EDDSA,
+ key: COSEKeyType::OKP(okp_key),
+ };
+ let actual = cose_key.der_spki().expect("Failed to serialize key");
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn test_okp_key_to_cbor() {
+ let okp_key = COSEOKPKey {
+ curve: Curve::Ed25519,
+ x: SAMPLE_ED25519_KEY.to_vec(),
+ };
+ let cose_key = COSEKey {
+ alg: COSEAlgorithm::EDDSA,
+ key: COSEKeyType::OKP(okp_key),
+ };
+ let cose_key_cbor = serde_cbor::to_vec(&cose_key).expect("Failed to serialize key");
+ let actual = serde_cbor::from_slice(&cose_key_cbor).expect("Failed to deserialize key");
+ assert_eq!(cose_key, actual);
+ }
+
+ #[test]
+ fn test_parse_es256_serialize_key() {
+ // Test values taken from https://github.com/Yubico/python-fido2/blob/master/test/test_cose.py
+ let key_data = decode_hex("A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C");
+ let key: COSEKey = from_slice(&key_data).unwrap();
+ assert_eq!(key.alg, COSEAlgorithm::ES256);
+ if let COSEKeyType::EC2(ec2key) = &key.key {
+ assert_eq!(ec2key.curve, Curve::SECP256R1);
+ assert_eq!(
+ ec2key.x,
+ decode_hex("A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1")
+ );
+ assert_eq!(
+ ec2key.y,
+ decode_hex("FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C")
+ );
+ } else {
+ panic!("Wrong key type!");
+ }
+
+ let serialized = serde_cbor::to_vec(&key).expect("Failed to serialize key");
+ assert_eq!(key_data, serialized);
+ }
+
+ #[test]
+ #[allow(non_snake_case)]
+ fn test_shared_secret() {
+ // Test values taken from https://github.com/Yubico/python-fido2/blob/main/tests/test_ctap2.py
+ let EC_PRIV =
+ decode_hex("7452E599FEE739D8A653F6A507343D12D382249108A651402520B72F24FE7684");
+ let EC_PUB_X =
+ decode_hex("44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F");
+ let EC_PUB_Y =
+ decode_hex("EC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9");
+ let DEV_PUB_X =
+ decode_hex("0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168");
+ let DEV_PUB_Y =
+ decode_hex("D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47");
+ let SHARED = decode_hex("c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c");
+ let TOKEN_ENC = decode_hex("7A9F98E31B77BE90F9C64D12E9635040");
+ let TOKEN = decode_hex("aff12c6dcfbf9df52f7a09211e8865cd");
+ let PIN_HASH_ENC = decode_hex("afe8327ce416da8ee3d057589c2ce1a9");
+
+ let client_ec2_key = COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: EC_PUB_X.clone(),
+ y: EC_PUB_Y.clone(),
+ };
+
+ let peer_ec2_key = COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: DEV_PUB_X,
+ y: DEV_PUB_Y,
+ };
+
+ // We are using `test_cose_ec2_p256_ecdh_sha256()` here, because we need a way to hand in
+ // the private key which would be generated on the fly otherwise (ephemeral keys),
+ // to predict the outputs
+ let peer_spki = peer_ec2_key.der_spki().unwrap();
+ let shared_point = test_ecdh_p256_raw(&peer_spki, &EC_PUB_X, &EC_PUB_Y, &EC_PRIV).unwrap();
+ let shared_secret = SharedSecret {
+ pin_protocol: PinUvAuthProtocol(Box::new(PinUvAuth1 {})),
+ key: sha256(&shared_point).unwrap(),
+ inputs: PublicInputs {
+ client: COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(client_ec2_key),
+ },
+ peer: COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(peer_ec2_key),
+ },
+ },
+ };
+ assert_eq!(shared_secret.key, SHARED);
+
+ let token_enc = shared_secret.encrypt(&TOKEN).unwrap();
+ assert_eq!(token_enc, TOKEN_ENC);
+
+ let token = shared_secret.decrypt(&TOKEN_ENC).unwrap();
+ assert_eq!(token, TOKEN);
+
+ let pin = Pin::new("1234");
+ let pin_hash_enc = shared_secret.encrypt(&pin.for_pin_token()).unwrap();
+ assert_eq!(pin_hash_enc, PIN_HASH_ENC);
+ }
+
+ #[test]
+ fn test_pin_uv_auth2_kdf() {
+ // We don't pull a complete HKDF implementation from the crypto backend, so we need to
+ // check that PinUvAuth2::kdf makes the right sequence of HMAC-SHA256 calls.
+ //
+ // ```python
+ // from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+ // from cryptography.hazmat.primitives import hashes
+ // from cryptography.hazmat.backends import default_backend
+ //
+ // Z = b"\xFF" * 32
+ //
+ // hmac_key = HKDF(
+ // algorithm=hashes.SHA256(),
+ // length=32,
+ // salt=b"\x00" * 32,
+ // info=b"CTAP2 HMAC key",
+ // ).derive(Z)
+ //
+ // aes_key = HKDF(
+ // algorithm=hashes.SHA256(),
+ // length=32,
+ // salt=b"\x00" * 32,
+ // info=b"CTAP2 AES key",
+ // ).derive(Z)
+ //
+ // print((hmac_key+aes_key).hex())
+ // ```
+ let input = decode_hex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
+ let expected = decode_hex("570B4ED82AA5DFB49DB79DBEAF4B315D8ABB1A9867B245F3367026987C0D47A17D9A93C39BAEC741D141C6238D8E1846DE323D8EED022CB397D19A73B98945E2");
+ let output = PinUvAuth2 {}.kdf(&input).unwrap();
+ assert_eq!(&expected, &output);
+ }
+
+ #[test]
+ fn test_hmac_sha256() {
+ let key = "key";
+ let message = "The quick brown fox jumps over the lazy dog";
+ let expected =
+ decode_hex("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8");
+
+ let result = hmac_sha256(key.as_bytes(), message.as_bytes()).expect("HMAC-SHA256 failed");
+ assert_eq!(result, expected);
+
+ let key = "The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog";
+ let message = "message";
+ let expected =
+ decode_hex("5597b93a2843078cbb0c920ae41dfe20f1685e10c67e423c11ab91adfc319d12");
+
+ let result = hmac_sha256(key.as_bytes(), message.as_bytes()).expect("HMAC-SHA256 failed");
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_pin_encryption_and_hashing() {
+ let pin = "1234";
+
+ let shared_secret = vec![
+ 0x82, 0xE3, 0xD8, 0x41, 0xE2, 0x5C, 0x5C, 0x13, 0x46, 0x2C, 0x12, 0x3C, 0xC3, 0xD3,
+ 0x98, 0x78, 0x65, 0xBA, 0x3D, 0x20, 0x46, 0x74, 0xFB, 0xED, 0xD4, 0x7E, 0xF5, 0xAB,
+ 0xAB, 0x8D, 0x13, 0x72,
+ ];
+ let expected_new_pin_enc = vec![
+ 0x70, 0x66, 0x4B, 0xB5, 0x81, 0xE2, 0x57, 0x45, 0x1A, 0x3A, 0xB9, 0x1B, 0xF1, 0xAA,
+ 0xD8, 0xE4, 0x5F, 0x6C, 0xE9, 0xB5, 0xC3, 0xB0, 0xF3, 0x2B, 0x5E, 0xCD, 0x62, 0xD0,
+ 0xBA, 0x3B, 0x60, 0x5F, 0xD9, 0x18, 0x31, 0x66, 0xF6, 0xC5, 0xFA, 0xF3, 0xE4, 0xDA,
+ 0x24, 0x81, 0x50, 0x2C, 0xD0, 0xCE, 0xE0, 0x15, 0x8B, 0x35, 0x1F, 0xC3, 0x92, 0x08,
+ 0xA7, 0x7C, 0xB2, 0x74, 0x4B, 0xD4, 0x3C, 0xF9,
+ ];
+ let expected_pin_auth = vec![
+ 0x8E, 0x7F, 0x01, 0x69, 0x97, 0xF3, 0xB0, 0xA2, 0x7B, 0xA4, 0x34, 0x7A, 0x0E, 0x49,
+ 0xFD, 0xF5,
+ ];
+
+ let mut input = vec![0x00; 64];
+ {
+ let pin_bytes = pin.as_bytes();
+ let (head, _) = input.split_at_mut(pin_bytes.len());
+ head.copy_from_slice(pin_bytes);
+ }
+
+ let new_pin_enc = PinUvAuth1 {}
+ .encrypt(&shared_secret, &input)
+ .expect("Failed to encrypt pin");
+ assert_eq!(new_pin_enc, expected_new_pin_enc);
+
+ let pin_auth = PinUvAuth1 {}
+ .authenticate(&shared_secret, &new_pin_enc)
+ .expect("HMAC-SHA256 failed");
+ assert_eq!(pin_auth[0..16], expected_pin_auth);
+ }
+
+ #[test]
+ fn test_pin_protocol() {
+ let mut info = AuthenticatorInfo {
+ versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
+ extensions: vec![],
+ aaguid: AAGuid(AAGUID_RAW),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(false),
+ user_presence: true,
+ ..Default::default()
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: None,
+ ..Default::default()
+ };
+
+ // Valid pin_protocols
+ info.pin_protocols = Some(vec![1, 2]);
+ let pin = PinUvAuthProtocol::try_from(&info).unwrap();
+ assert_eq!(pin.id(), 1); // The one listed first
+
+ // Invalid pin_protocols
+ info.pin_protocols = Some(vec![0, 10]);
+ PinUvAuthProtocol::try_from(&info).unwrap_err();
+
+ info.pin_protocols = None;
+ // No PIN protocols. CTAP1 - not supported
+ info.versions = vec![AuthenticatorVersion::U2F_V2];
+ PinUvAuthProtocol::try_from(&info).unwrap_err();
+
+ // No PIN protocols. CTAP2.0 - Fallback to 1
+ info.versions = vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0];
+ let pin = PinUvAuthProtocol::try_from(&info).unwrap();
+ assert_eq!(pin.id(), 1);
+
+ // No PIN protocols. CTAP2.1 - Fallback to 2
+ info.versions = vec![AuthenticatorVersion::FIDO_2_1];
+ let pin = PinUvAuthProtocol::try_from(&info).unwrap();
+ assert_eq!(pin.id(), 2);
+
+ // No PIN protocols. CTAP2.1_PRE - Fallback to 2
+ info.versions = vec![
+ AuthenticatorVersion::FIDO_2_0,
+ AuthenticatorVersion::FIDO_2_1_PRE,
+ ];
+ let pin = PinUvAuthProtocol::try_from(&info).unwrap();
+ assert_eq!(pin.id(), 2);
+ }
+
+ #[test]
+ #[cfg(feature = "crypto_nss")]
+ fn test_sign() {
+ let (good_private, good_public) =
+ COSEKey::generate(COSEAlgorithm::ES256).expect("could not generate a key pair");
+ let good_spki = match good_public.key {
+ COSEKeyType::EC2(ref x) => x.der_spki().expect("could not serialize public key"),
+ _ => unreachable!(),
+ };
+
+ let good_data = vec![1, 2, 3, 4, 5, 6, 7, 8];
+ let good_signature =
+ ecdsa_p256_sha256_sign_raw(&good_private, &good_data).expect("could not sign");
+ let good_signature2 =
+ ecdsa_p256_sha256_sign_raw(&good_private, &good_data).expect("could not sign");
+
+ // Signing is randomized
+ assert_ne!(good_signature, good_signature2);
+
+ // Good signature verifies
+ assert!(test_ecdsa_p256_sha256_verify_raw(&good_spki, &good_signature, &good_data).is_ok());
+
+ // Wrong data does not verify
+ let other_data = vec![0, 0, 0, 0, 5, 6, 7, 8];
+ assert!(
+ test_ecdsa_p256_sha256_verify_raw(&good_spki, &good_signature, &other_data).is_err()
+ );
+
+ // Wrong signature does not verify
+ let other_signature =
+ ecdsa_p256_sha256_sign_raw(&good_private, &other_data).expect("could not sign");
+ assert!(
+ test_ecdsa_p256_sha256_verify_raw(&good_spki, &other_signature, &good_data).is_err()
+ );
+
+ // Wrong key does not verify
+ let (_, other_public) =
+ COSEKey::generate(COSEAlgorithm::ES256).expect("could not generate a key pair");
+ let other_spki = match other_public.key {
+ COSEKeyType::EC2(ref x) => x.der_spki().expect("could not serialize public key"),
+ _ => unreachable!(),
+ };
+ assert!(
+ test_ecdsa_p256_sha256_verify_raw(&other_spki, &good_signature, &good_data).is_err()
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/crypto/nss.rs b/third_party/rust/authenticator/src/crypto/nss.rs
new file mode 100644
index 0000000000..56c23db5dd
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/nss.rs
@@ -0,0 +1,481 @@
+use super::CryptoError;
+use nss_gk_api::p11::{
+ PK11Origin, PK11_CreateContextBySymKey, PK11_Decrypt, PK11_DigestFinal, PK11_DigestOp,
+ PK11_Encrypt, PK11_ExportDERPrivateKeyInfo, PK11_GenerateKeyPairWithOpFlags,
+ PK11_GenerateRandom, PK11_HashBuf, PK11_ImportDERPrivateKeyInfoAndReturnKey, PK11_ImportSymKey,
+ PK11_PubDeriveWithKDF, PK11_SignWithMechanism, PrivateKey, PublicKey,
+ SECKEY_DecodeDERSubjectPublicKeyInfo, SECKEY_ExtractPublicKey, SECOidTag, Slot,
+ SubjectPublicKeyInfo, AES_BLOCK_SIZE, PK11_ATTR_EXTRACTABLE, PK11_ATTR_INSENSITIVE,
+ PK11_ATTR_SESSION, SHA256_LENGTH,
+};
+use nss_gk_api::{IntoResult, SECItem, SECItemBorrowed, ScopedSECItem, PR_FALSE};
+use pkcs11_bindings::{
+ CKA_DERIVE, CKA_ENCRYPT, CKA_SIGN, CKD_NULL, CKF_DERIVE, CKM_AES_CBC, CKM_ECDH1_DERIVE,
+ CKM_ECDSA_SHA256, CKM_EC_KEY_PAIR_GEN, CKM_SHA256_HMAC, CKM_SHA512_HMAC,
+};
+use std::convert::TryFrom;
+use std::os::raw::{c_int, c_uint};
+use std::ptr;
+
+use super::der;
+
+#[cfg(test)]
+use nss_gk_api::p11::PK11_VerifyWithMechanism;
+
+impl From<nss_gk_api::Error> for CryptoError {
+ fn from(e: nss_gk_api::Error) -> Self {
+ CryptoError::Backend(format!("{e}"))
+ }
+}
+
+pub type Result<T> = std::result::Result<T, CryptoError>;
+
+fn nss_public_key_from_der_spki(spki: &[u8]) -> Result<PublicKey> {
+ // TODO: replace this with an nss-gk-api function
+ // https://github.com/mozilla/nss-gk-api/issues/7
+ let mut spki_item = SECItemBorrowed::wrap(spki);
+ let spki_item_ptr: *mut SECItem = spki_item.as_mut();
+ let nss_spki = unsafe {
+ SubjectPublicKeyInfo::from_ptr(SECKEY_DecodeDERSubjectPublicKeyInfo(spki_item_ptr))?
+ };
+ let public_key = unsafe { PublicKey::from_ptr(SECKEY_ExtractPublicKey(*nss_spki))? };
+ Ok(public_key)
+}
+
+/// ECDH using NSS types. Computes the x coordinate of scalar multiplication of `peer_public` by
+/// `client_private`.
+fn ecdh_nss_raw(client_private: PrivateKey, peer_public: PublicKey) -> Result<Vec<u8>> {
+ let ecdh_x_coord = unsafe {
+ PK11_PubDeriveWithKDF(
+ *client_private,
+ *peer_public,
+ PR_FALSE,
+ std::ptr::null_mut(),
+ std::ptr::null_mut(),
+ CKM_ECDH1_DERIVE,
+ CKM_SHA512_HMAC, // unused
+ CKA_DERIVE, // unused
+ 0,
+ CKD_NULL,
+ std::ptr::null_mut(),
+ std::ptr::null_mut(),
+ )
+ .into_result()?
+ };
+ let ecdh_x_coord_bytes = ecdh_x_coord.as_bytes()?;
+ Ok(ecdh_x_coord_bytes.to_vec())
+}
+
+fn generate_p256_nss() -> Result<(PrivateKey, PublicKey)> {
+ // Hard-coding the P256 OID here is easier than extracting a group name from peer_public and
+ // comparing it with P256. We'll fail in `PK11_GenerateKeyPairWithOpFlags` if peer_public is on
+ // the wrong curve.
+ let oid_bytes = der::object_id(der::OID_SECP256R1_BYTES)?;
+ let mut oid = SECItemBorrowed::wrap(&oid_bytes);
+ let oid_ptr: *mut SECItem = oid.as_mut();
+
+ let slot = Slot::internal()?;
+
+ let mut client_public_ptr = ptr::null_mut();
+
+ // We have to be careful with error handling between the `PK11_GenerateKeyPairWithOpFlags` and
+ // `PublicKey::from_ptr` calls here, so I've wrapped them in the same unsafe block as a
+ // warning. TODO(jms) Replace this once there is a safer alternative.
+ // https://github.com/mozilla/nss-gk-api/issues/1
+ unsafe {
+ let client_private =
+ // Type of `param` argument depends on mechanism. For EC keygen it is
+ // `SECKEYECParams *` which is a typedef for `SECItem *`.
+ PK11_GenerateKeyPairWithOpFlags(
+ *slot,
+ CKM_EC_KEY_PAIR_GEN,
+ oid_ptr.cast(),
+ &mut client_public_ptr,
+ PK11_ATTR_EXTRACTABLE | PK11_ATTR_INSENSITIVE | PK11_ATTR_SESSION,
+ CKF_DERIVE,
+ CKF_DERIVE,
+ ptr::null_mut(),
+ )
+ .into_result()?;
+
+ let client_public = PublicKey::from_ptr(client_public_ptr)?;
+
+ Ok((client_private, client_public))
+ }
+}
+
+/// This returns a PKCS#8 ECPrivateKey and an uncompressed SEC1 public key.
+pub fn gen_p256() -> Result<(Vec<u8>, Vec<u8>)> {
+ nss_gk_api::init();
+
+ let (client_private, client_public) = generate_p256_nss()?;
+
+ let pkcs8_priv = unsafe {
+ let pkcs8_priv_item: ScopedSECItem =
+ PK11_ExportDERPrivateKeyInfo(*client_private, ptr::null_mut()).into_result()?;
+ pkcs8_priv_item.into_vec()
+ };
+
+ let sec1_pub = client_public.key_data()?;
+
+ Ok((pkcs8_priv, sec1_pub))
+}
+
+pub fn ecdsa_p256_sha256_sign_raw(private: &[u8], data: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ let slot = Slot::internal()?;
+
+ let imported_private: PrivateKey = unsafe {
+ let mut imported_private_ptr = ptr::null_mut();
+ PK11_ImportDERPrivateKeyInfoAndReturnKey(
+ *slot,
+ SECItemBorrowed::wrap(private).as_mut(),
+ ptr::null_mut(),
+ ptr::null_mut(),
+ PR_FALSE,
+ PR_FALSE,
+ 255, /* todo: expose KU_ flags in nss-gk-api */
+ &mut imported_private_ptr,
+ ptr::null_mut(),
+ );
+ imported_private_ptr.into_result()?
+ };
+
+ let signature_buf = vec![0; 64];
+ unsafe {
+ PK11_SignWithMechanism(
+ *imported_private,
+ CKM_ECDSA_SHA256,
+ ptr::null_mut(),
+ SECItemBorrowed::wrap(&signature_buf).as_mut(),
+ SECItemBorrowed::wrap(data).as_mut(),
+ )
+ .into_result()?;
+ }
+
+ let (r, s) = signature_buf.split_at(32);
+ der::sequence(&[&der::integer(r)?, &der::integer(s)?])
+}
+
+/// Ephemeral ECDH over P256. Takes a DER SubjectPublicKeyInfo that encodes a public key. Generates
+/// an ephemeral P256 key pair. Returns
+/// 1) the x coordinate of the shared point, and
+/// 2) the uncompressed SEC 1 encoding of the ephemeral public key.
+pub fn ecdhe_p256_raw(peer_spki: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
+ nss_gk_api::init();
+
+ let peer_public = nss_public_key_from_der_spki(peer_spki)?;
+
+ let (client_private, client_public) = generate_p256_nss()?;
+
+ let shared_point = ecdh_nss_raw(client_private, peer_public)?;
+
+ Ok((shared_point, client_public.key_data()?))
+}
+
+/// AES-256-CBC encryption for data that is a multiple of the AES block size (16 bytes) in length.
+/// Uses the zero IV if `iv` is None.
+pub fn encrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ if key.len() != 32 {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]);
+
+ if iv.len() != AES_BLOCK_SIZE {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ let in_len = match c_uint::try_from(data.len()) {
+ Ok(in_len) => in_len,
+ _ => return Err(CryptoError::LibraryFailure),
+ };
+
+ if data.len() % AES_BLOCK_SIZE != 0 {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ let slot = Slot::internal()?;
+
+ let sym_key = unsafe {
+ PK11_ImportSymKey(
+ *slot,
+ CKM_AES_CBC,
+ PK11Origin::PK11_OriginUnwrap,
+ CKA_ENCRYPT,
+ SECItemBorrowed::wrap(key).as_mut(),
+ ptr::null_mut(),
+ )
+ .into_result()?
+ };
+
+ let mut params = SECItemBorrowed::wrap(iv);
+ let params_ptr: *mut SECItem = params.as_mut();
+ let mut out_len: c_uint = 0;
+ let mut out = vec![0; data.len()];
+ unsafe {
+ PK11_Encrypt(
+ *sym_key,
+ CKM_AES_CBC,
+ params_ptr,
+ out.as_mut_ptr(),
+ &mut out_len,
+ in_len,
+ data.as_ptr(),
+ in_len,
+ )
+ .into_result()?
+ }
+ // CKM_AES_CBC should have output length equal to input length.
+ debug_assert_eq!(out_len, in_len);
+
+ Ok(out)
+}
+
+/// AES-256-CBC decryption for data that is a multiple of the AES block size (16 bytes) in length.
+/// Uses the zero IV if `iv` is None.
+pub fn decrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ if key.len() != 32 {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]);
+
+ if iv.len() != AES_BLOCK_SIZE {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ let in_len = match c_uint::try_from(data.len()) {
+ Ok(in_len) => in_len,
+ _ => return Err(CryptoError::LibraryFailure),
+ };
+
+ if data.len() % AES_BLOCK_SIZE != 0 {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ let slot = Slot::internal()?;
+
+ let sym_key = unsafe {
+ PK11_ImportSymKey(
+ *slot,
+ CKM_AES_CBC,
+ PK11Origin::PK11_OriginUnwrap,
+ CKA_ENCRYPT,
+ SECItemBorrowed::wrap(key).as_mut(),
+ ptr::null_mut(),
+ )
+ .into_result()?
+ };
+
+ let mut params = SECItemBorrowed::wrap(iv);
+ let params_ptr: *mut SECItem = params.as_mut();
+ let mut out_len: c_uint = 0;
+ let mut out = vec![0; data.len()];
+ unsafe {
+ PK11_Decrypt(
+ *sym_key,
+ CKM_AES_CBC,
+ params_ptr,
+ out.as_mut_ptr(),
+ &mut out_len,
+ in_len,
+ data.as_ptr(),
+ in_len,
+ )
+ .into_result()?
+ }
+ // CKM_AES_CBC should have output length equal to input length.
+ debug_assert_eq!(out_len, in_len);
+
+ Ok(out)
+}
+
+/// Textbook HMAC-SHA256
+pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ let data_len = match u32::try_from(data.len()) {
+ Ok(data_len) => data_len,
+ _ => return Err(CryptoError::LibraryFailure),
+ };
+
+ let slot = Slot::internal()?;
+ let sym_key = unsafe {
+ PK11_ImportSymKey(
+ *slot,
+ CKM_SHA256_HMAC,
+ PK11Origin::PK11_OriginUnwrap,
+ CKA_SIGN,
+ SECItemBorrowed::wrap(key).as_mut(),
+ ptr::null_mut(),
+ )
+ .into_result()?
+ };
+ let param = SECItemBorrowed::make_empty();
+ let context = unsafe {
+ PK11_CreateContextBySymKey(CKM_SHA256_HMAC, CKA_SIGN, *sym_key, param.as_ref())
+ .into_result()?
+ };
+ unsafe { PK11_DigestOp(*context, data.as_ptr(), data_len).into_result()? };
+ let mut digest = vec![0u8; SHA256_LENGTH];
+ let mut digest_len = 0u32;
+ unsafe {
+ PK11_DigestFinal(
+ *context,
+ digest.as_mut_ptr(),
+ &mut digest_len,
+ digest.len() as u32,
+ )
+ .into_result()?
+ }
+ assert_eq!(digest_len as usize, SHA256_LENGTH);
+ Ok(digest)
+}
+
+/// Textbook SHA256
+pub fn sha256(data: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ let data_len: i32 = match i32::try_from(data.len()) {
+ Ok(data_len) => data_len,
+ _ => return Err(CryptoError::LibraryFailure),
+ };
+ let mut digest = vec![0u8; SHA256_LENGTH];
+ unsafe {
+ PK11_HashBuf(
+ SECOidTag::SEC_OID_SHA256,
+ digest.as_mut_ptr(),
+ data.as_ptr(),
+ data_len,
+ )
+ .into_result()?
+ };
+ Ok(digest)
+}
+
+pub fn random_bytes(count: usize) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ let count_cint: c_int = match c_int::try_from(count) {
+ Ok(c) => c,
+ _ => return Err(CryptoError::LibraryFailure),
+ };
+
+ let mut out = vec![0u8; count];
+ unsafe { PK11_GenerateRandom(out.as_mut_ptr(), count_cint).into_result()? };
+ Ok(out)
+}
+
+#[cfg(test)]
+pub fn test_ecdh_p256_raw(
+ peer_spki: &[u8],
+ client_public_x: &[u8],
+ client_public_y: &[u8],
+ client_private: &[u8],
+) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ let peer_public = nss_public_key_from_der_spki(peer_spki)?;
+
+ // NSS has no mechanism to import a raw elliptic curve coordinate as a private key.
+ // We need to encode it in an RFC 5208 PrivateKeyInfo:
+ //
+ // PrivateKeyInfo ::= SEQUENCE {
+ // version Version,
+ // privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+ // privateKey PrivateKey,
+ // attributes [0] IMPLICIT Attributes OPTIONAL }
+ //
+ // Version ::= INTEGER
+ // PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
+ // PrivateKey ::= OCTET STRING
+ // Attributes ::= SET OF Attribute
+ //
+ // The privateKey field will contain an RFC 5915 ECPrivateKey:
+ // ECPrivateKey ::= SEQUENCE {
+ // version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+ // privateKey OCTET STRING,
+ // parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
+ // publicKey [1] BIT STRING OPTIONAL
+ // }
+
+ // PrivateKeyInfo
+ let priv_key_info = der::sequence(&[
+ // version
+ &der::integer(&[0x00])?,
+ // privateKeyAlgorithm
+ &der::sequence(&[
+ &der::object_id(der::OID_EC_PUBLIC_KEY_BYTES)?,
+ &der::object_id(der::OID_SECP256R1_BYTES)?,
+ ])?,
+ // privateKey
+ &der::octet_string(
+ // ECPrivateKey
+ &der::sequence(&[
+ // version
+ &der::integer(&[0x01])?,
+ // privateKey
+ &der::octet_string(client_private)?,
+ // publicKey
+ &der::context_specific_explicit_tag(
+ 1, // publicKey
+ &der::bit_string(&[&[0x04], client_public_x, client_public_y].concat())?,
+ )?,
+ ])?,
+ )?,
+ ])?;
+
+ // Now we can import the private key.
+ let slot = Slot::internal()?;
+ let mut priv_key_info_item = SECItemBorrowed::wrap(&priv_key_info);
+ let priv_key_info_item_ptr: *mut SECItem = priv_key_info_item.as_mut();
+ let mut client_private_ptr = ptr::null_mut();
+ unsafe {
+ PK11_ImportDERPrivateKeyInfoAndReturnKey(
+ *slot,
+ priv_key_info_item_ptr,
+ ptr::null_mut(),
+ ptr::null_mut(),
+ PR_FALSE,
+ PR_FALSE,
+ 255, /* todo: expose KU_ flags in nss-gk-api */
+ &mut client_private_ptr,
+ ptr::null_mut(),
+ )
+ };
+ let client_private = unsafe { PrivateKey::from_ptr(client_private_ptr) }?;
+
+ let shared_point = ecdh_nss_raw(client_private, peer_public)?;
+
+ Ok(shared_point)
+}
+
+#[cfg(test)]
+pub fn test_ecdsa_p256_sha256_verify_raw(
+ public: &[u8],
+ signature: &[u8],
+ data: &[u8],
+) -> Result<()> {
+ nss_gk_api::init();
+
+ let signature = der::read_p256_sig(signature)?;
+ let public = nss_public_key_from_der_spki(public)?;
+ unsafe {
+ PK11_VerifyWithMechanism(
+ *public,
+ CKM_ECDSA_SHA256,
+ ptr::null_mut(),
+ SECItemBorrowed::wrap(&signature).as_mut(),
+ SECItemBorrowed::wrap(data).as_mut(),
+ ptr::null_mut(),
+ )
+ .into_result()?
+ }
+ Ok(())
+}
diff --git a/third_party/rust/authenticator/src/crypto/openssl.rs b/third_party/rust/authenticator/src/crypto/openssl.rs
new file mode 100644
index 0000000000..065ea201a5
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/openssl.rs
@@ -0,0 +1,183 @@
+use super::CryptoError;
+use openssl::bn::BigNumContext;
+use openssl::derive::Deriver;
+use openssl::ec::{EcGroup, EcKey, PointConversionForm};
+use openssl::error::ErrorStack;
+use openssl::hash::{hash, MessageDigest};
+use openssl::nid::Nid;
+use openssl::pkey::{PKey, Private, Public};
+use openssl::rand::rand_bytes;
+use openssl::sign::Signer;
+use openssl::symm::{Cipher, Crypter, Mode};
+use std::os::raw::c_int;
+
+#[cfg(test)]
+use openssl::ec::EcPoint;
+
+#[cfg(test)]
+use openssl::bn::BigNum;
+
+const AES_BLOCK_SIZE: usize = 16;
+
+impl From<ErrorStack> for CryptoError {
+ fn from(e: ErrorStack) -> Self {
+ CryptoError::Backend(format!("{e}"))
+ }
+}
+
+impl From<&ErrorStack> for CryptoError {
+ fn from(e: &ErrorStack) -> Self {
+ CryptoError::Backend(format!("{e}"))
+ }
+}
+
+pub type Result<T> = std::result::Result<T, CryptoError>;
+
+/// ECDH using OpenSSL types. Computes the x coordinate of scalar multiplication of `peer_public`
+/// by `client_private`.
+fn ecdh_openssl_raw(client_private: EcKey<Private>, peer_public: EcKey<Public>) -> Result<Vec<u8>> {
+ let client_pkey = PKey::from_ec_key(client_private)?;
+ let peer_pkey = PKey::from_ec_key(peer_public)?;
+ let mut deriver = Deriver::new(&client_pkey)?;
+ deriver.set_peer(&peer_pkey)?;
+ let shared_point = deriver.derive_to_vec()?;
+ Ok(shared_point)
+}
+
+/// Ephemeral ECDH over P256. Takes a DER SubjectPublicKeyInfo that encodes a public key. Generates
+/// an ephemeral P256 key pair. Returns
+/// 1) the x coordinate of the shared point, and
+/// 2) the uncompressed SEC 1 encoding of the ephemeral public key.
+pub fn ecdhe_p256_raw(peer_spki: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
+ let peer_public = EcKey::public_key_from_der(peer_spki)?;
+
+ // Hard-coding the P256 group here is easier than extracting a group name from peer_public and
+ // comparing it with P256. We'll fail in key derivation if peer_public is on the wrong curve.
+ let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?;
+
+ let mut bn_ctx = BigNumContext::new()?;
+ let client_private = EcKey::generate(&group)?;
+ let client_public_sec1 = client_private.public_key().to_bytes(
+ &group,
+ PointConversionForm::UNCOMPRESSED,
+ &mut bn_ctx,
+ )?;
+
+ let shared_point = ecdh_openssl_raw(client_private, peer_public)?;
+
+ Ok((shared_point, client_public_sec1))
+}
+
+/// AES-256-CBC encryption for data that is a multiple of the AES block size (16 bytes) in length.
+/// Uses the zero IV if `iv` is None.
+pub fn encrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result<Vec<u8>> {
+ let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]);
+
+ let mut encrypter = Crypter::new(Cipher::aes_256_cbc(), Mode::Encrypt, key, Some(iv))?;
+ encrypter.pad(false);
+
+ let in_len = data.len();
+ if in_len % AES_BLOCK_SIZE != 0 {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ // OpenSSL would panic if we didn't allocate an extra block here.
+ let mut out = vec![0; in_len + AES_BLOCK_SIZE];
+ let mut out_len = 0;
+ out_len += encrypter.update(data, out.as_mut_slice())?;
+ out_len += encrypter.finalize(out.as_mut_slice())?;
+ debug_assert_eq!(in_len, out_len);
+
+ out.truncate(out_len);
+ Ok(out)
+}
+
+/// AES-256-CBC decryption for data that is a multiple of the AES block size (16 bytes) in length.
+/// Uses the zero IV if `iv` is None.
+pub fn decrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result<Vec<u8>> {
+ let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]);
+
+ let mut encrypter = Crypter::new(Cipher::aes_256_cbc(), Mode::Decrypt, key, Some(iv))?;
+ encrypter.pad(false);
+
+ let in_len = data.len();
+ if in_len % AES_BLOCK_SIZE != 0 {
+ return Err(CryptoError::LibraryFailure);
+ }
+
+ // OpenSSL would panic if we didn't allocate an extra block here.
+ let mut out = vec![0; in_len + AES_BLOCK_SIZE];
+ let mut out_len = 0;
+ out_len += encrypter.update(data, out.as_mut_slice())?;
+ out_len += encrypter.finalize(out.as_mut_slice())?;
+ debug_assert_eq!(in_len, out_len);
+
+ out.truncate(out_len);
+ Ok(out)
+}
+
+/// Textbook HMAC-SHA256
+pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
+ let key = PKey::hmac(key)?;
+ let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
+ signer.update(data)?;
+ Ok(signer.sign_to_vec()?)
+}
+
+pub fn sha256(data: &[u8]) -> Result<Vec<u8>> {
+ let digest = hash(MessageDigest::sha256(), data)?;
+ Ok(digest.as_ref().to_vec())
+}
+
+pub fn random_bytes(count: usize) -> Result<Vec<u8>> {
+ if count > c_int::MAX as usize {
+ return Err(CryptoError::LibraryFailure);
+ }
+ let mut out = vec![0u8; count];
+ rand_bytes(&mut out)?;
+ Ok(out)
+}
+
+#[cfg(test)]
+pub fn test_ecdh_p256_raw(
+ peer_spki: &[u8],
+ client_public_x: &[u8],
+ client_public_y: &[u8],
+ client_private: &[u8],
+) -> Result<Vec<u8>> {
+ let peer_public = EcKey::public_key_from_der(peer_spki)?;
+ let group = peer_public.group();
+
+ let mut client_pub_sec1 = vec![];
+ client_pub_sec1.push(0x04); // SEC 1 encoded uncompressed point
+ client_pub_sec1.extend_from_slice(&client_public_x);
+ client_pub_sec1.extend_from_slice(&client_public_y);
+
+ let mut ctx = BigNumContext::new()?;
+ let client_pub_point = EcPoint::from_bytes(&group, &client_pub_sec1, &mut ctx)?;
+ let client_priv_bignum = BigNum::from_slice(client_private)?;
+ let client_private =
+ EcKey::from_private_components(&group, &client_priv_bignum, &client_pub_point)?;
+
+ let shared_point = ecdh_openssl_raw(client_private, peer_public)?;
+
+ Ok(shared_point)
+}
+
+pub fn gen_p256() -> Result<(Vec<u8>, Vec<u8>)> {
+ unimplemented!()
+}
+
+pub fn ecdsa_p256_sha256_sign_raw(_private: &[u8], _data: &[u8]) -> Result<Vec<u8>> {
+ unimplemented!()
+}
+
+#[allow(dead_code)]
+#[cfg(test)]
+pub fn test_ecdsa_p256_sha256_verify_raw(
+ _public: &[u8],
+ _signature: &[u8],
+ _data: &[u8],
+) -> Result<()> {
+ unimplemented!()
+}
diff --git a/third_party/rust/authenticator/src/ctap2/attestation.rs b/third_party/rust/authenticator/src/ctap2/attestation.rs
new file mode 100644
index 0000000000..af33b159b7
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/attestation.rs
@@ -0,0 +1,1129 @@
+use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte};
+use crate::crypto::COSEAlgorithm;
+use crate::ctap2::server::{CredentialProtectionPolicy, RpIdHash};
+use crate::ctap2::utils::serde_parse_err;
+use crate::{crypto::COSEKey, errors::AuthenticatorError};
+use base64::Engine;
+use serde::ser::{Error as SerError, SerializeMap, Serializer};
+use serde::{
+ de::{Error as SerdeError, Unexpected, Visitor},
+ Deserialize, Deserializer, Serialize,
+};
+use serde_cbor;
+use std::fmt;
+use std::io::{Cursor, Read};
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum HmacSecretResponse {
+ /// This is returned by MakeCredential calls to display if CredRandom was
+ /// successfully generated
+ Confirmed(bool),
+ /// This is returned by GetAssertion:
+ /// AES256-CBC(shared_secret, HMAC-SHA265(CredRandom, salt1) || HMAC-SHA265(CredRandom, salt2))
+ Secret(Vec<u8>),
+}
+
+impl Serialize for HmacSecretResponse {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match self {
+ HmacSecretResponse::Confirmed(x) => serializer.serialize_bool(*x),
+ HmacSecretResponse::Secret(x) => serializer.serialize_bytes(x),
+ }
+ }
+}
+impl<'de> Deserialize<'de> for HmacSecretResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct HmacSecretResponseVisitor;
+
+ impl<'de> Visitor<'de> for HmacSecretResponseVisitor {
+ type Value = HmacSecretResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array or a boolean")
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ Ok(HmacSecretResponse::Secret(v.to_vec()))
+ }
+
+ fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ Ok(HmacSecretResponse::Confirmed(v))
+ }
+ }
+ deserializer.deserialize_any(HmacSecretResponseVisitor)
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
+pub struct Extension {
+ #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")]
+ pub cred_protect: Option<CredentialProtectionPolicy>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<HmacSecretResponse>,
+ #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")]
+ pub min_pin_length: Option<u64>,
+}
+
+impl Extension {
+ pub fn has_some(&self) -> bool {
+ self.min_pin_length.is_some() || self.hmac_secret.is_some() || self.cred_protect.is_some()
+ }
+}
+
+#[derive(Serialize, PartialEq, Default, Eq, Clone)]
+pub struct AAGuid(pub [u8; 16]);
+
+impl AAGuid {
+ pub fn from(src: &[u8]) -> Result<AAGuid, AuthenticatorError> {
+ let mut payload = [0u8; 16];
+ if src.len() != payload.len() {
+ Err(AuthenticatorError::InternalError(String::from(
+ "Failed to parse AAGuid",
+ )))
+ } else {
+ payload.copy_from_slice(src);
+ Ok(AAGuid(payload))
+ }
+ }
+}
+
+impl fmt::Debug for AAGuid {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "AAGuid({:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x})",
+ self.0[0],
+ self.0[1],
+ self.0[2],
+ self.0[3],
+ self.0[4],
+ self.0[5],
+ self.0[6],
+ self.0[7],
+ self.0[8],
+ self.0[9],
+ self.0[10],
+ self.0[11],
+ self.0[12],
+ self.0[13],
+ self.0[14],
+ self.0[15]
+ )
+ }
+}
+
+impl<'de> Deserialize<'de> for AAGuid {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AAGuidVisitor;
+
+ impl<'de> Visitor<'de> for AAGuidVisitor {
+ type Value = AAGuid;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ let mut buf = [0u8; 16];
+ if v.len() != buf.len() {
+ return Err(E::invalid_length(v.len(), &"16"));
+ }
+
+ buf.copy_from_slice(v);
+
+ Ok(AAGuid(buf))
+ }
+ }
+
+ deserializer.deserialize_bytes(AAGuidVisitor)
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct AttestedCredentialData {
+ pub aaguid: AAGuid,
+ pub credential_id: Vec<u8>,
+ pub credential_public_key: COSEKey,
+}
+
+fn parse_attested_cred_data<R: Read, E: SerdeError>(
+ data: &mut R,
+) -> Result<AttestedCredentialData, E> {
+ let mut aaguid_raw = [0u8; 16];
+ data.read_exact(&mut aaguid_raw)
+ .map_err(|_| serde_parse_err("AAGuid"))?;
+ let aaguid = AAGuid(aaguid_raw);
+ let cred_len = read_be_u16(data)?;
+ let mut credential_id = vec![0u8; cred_len as usize];
+ data.read_exact(&mut credential_id)
+ .map_err(|_| serde_parse_err("CredentialId"))?;
+ let credential_public_key = from_slice_stream(data)?;
+ Ok(AttestedCredentialData {
+ aaguid,
+ credential_id,
+ credential_public_key,
+ })
+}
+
+bitflags! {
+ // Defining an exhaustive list of flags here ensures that `from_bits_truncate` is lossless and
+ // that `from_bits` never returns None.
+ pub struct AuthenticatorDataFlags: u8 {
+ const USER_PRESENT = 0x01;
+ const RESERVED_1 = 0x02;
+ const USER_VERIFIED = 0x04;
+ const RESERVED_3 = 0x08;
+ const RESERVED_4 = 0x10;
+ const RESERVED_5 = 0x20;
+ const ATTESTED = 0x40;
+ const EXTENSION_DATA = 0x80;
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct AuthenticatorData {
+ pub rp_id_hash: RpIdHash,
+ pub flags: AuthenticatorDataFlags,
+ pub counter: u32,
+ pub credential_data: Option<AttestedCredentialData>,
+ pub extensions: Extension,
+}
+
+impl AuthenticatorData {
+ pub fn to_vec(&self) -> Vec<u8> {
+ match serde_cbor::value::to_value(self) {
+ Ok(serde_cbor::value::Value::Bytes(out)) => out,
+ _ => unreachable!(), // Serialize is guaranteed to produce bytes
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for AuthenticatorData {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AuthenticatorDataVisitor;
+
+ impl<'de> Visitor<'de> for AuthenticatorDataVisitor {
+ type Value = AuthenticatorData;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_bytes<E>(self, input: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ let mut cursor = Cursor::new(input);
+ let mut rp_id_hash_raw = [0u8; 32];
+ cursor
+ .read_exact(&mut rp_id_hash_raw)
+ .map_err(|_| serde_parse_err("32 bytes"))?;
+ let rp_id_hash = RpIdHash(rp_id_hash_raw);
+
+ // preserve the flags, even if some reserved values are set.
+ let flags = AuthenticatorDataFlags::from_bits_truncate(read_byte(&mut cursor)?);
+ let counter = read_be_u32(&mut cursor)?;
+ let mut credential_data = None;
+ if flags.contains(AuthenticatorDataFlags::ATTESTED) {
+ credential_data = Some(parse_attested_cred_data(&mut cursor)?);
+ }
+
+ let extensions = if flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) {
+ from_slice_stream(&mut cursor)?
+ } else {
+ Default::default()
+ };
+
+ // TODO(baloo): we should check for end of buffer and raise a parse
+ // parse error if data is still in the buffer
+ Ok(AuthenticatorData {
+ rp_id_hash,
+ flags,
+ counter,
+ credential_data,
+ extensions,
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(AuthenticatorDataVisitor)
+ }
+}
+
+impl Serialize for AuthenticatorData {
+ // see https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data
+ // Authenticator Data
+ // Name Length (in bytes)
+ // rpIdHash 32
+ // flags 1
+ // signCount 4
+ // attestedCredentialData variable (if present)
+ // extensions variable (if present)
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut data = Vec::new();
+ data.extend(self.rp_id_hash.0); // (1) "rpIDHash", len=32
+ data.extend([self.flags.bits()]); // (2) "flags", len=1 (u8)
+ data.extend(self.counter.to_be_bytes()); // (3) "signCount", len=4, 32-bit unsigned big-endian integer.
+
+ if let Some(cred) = &self.credential_data {
+ // see https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data
+ // Attested Credential Data
+ // Name Length (in bytes)
+ // aaguid 16
+ // credentialIdLength 2
+ // credentialId L
+ // credentialPublicKey variable
+ data.extend(cred.aaguid.0); // (1) "aaguid", len=16
+ data.extend((cred.credential_id.len() as u16).to_be_bytes()); // (2) "credentialIdLength", len=2, 16-bit unsigned big-endian integer
+ data.extend(&cred.credential_id); // (3) "credentialId", len= see (2)
+ data.extend(
+ // (4) "credentialPublicKey", len=variable
+ &serde_cbor::to_vec(&cred.credential_public_key)
+ .map_err(|_| SerError::custom("Failed to serialize auth_data"))?,
+ );
+ }
+ // If we have parsed extension data, then we should serialize it even if the authenticator
+ // failed to set the extension data flag.
+ // If we don't have parsed extension data, then what we output depends on the flag.
+ // If the flag is set, we output the empty CBOR map. If it is not set, we output nothing.
+ if self.extensions.has_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA)
+ {
+ data.extend(
+ // (5) "extensions", len=variable
+ &serde_cbor::to_vec(&self.extensions)
+ .map_err(|_| SerError::custom("Failed to serialize auth_data"))?,
+ );
+ }
+
+ serializer.serialize_bytes(&data)
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+/// x509 encoded attestation certificate
+pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub Vec<u8>);
+
+impl AsRef<[u8]> for AttestationCertificate {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq)]
+pub struct Signature(#[serde(with = "serde_bytes")] pub Vec<u8>);
+
+impl fmt::Debug for Signature {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.0);
+ write!(f, "Signature({value})")
+ }
+}
+
+impl AsRef<[u8]> for Signature {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+impl From<&[u8]> for Signature {
+ fn from(sig: &[u8]) -> Signature {
+ Signature(sig.to_vec())
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Deserialize)]
+// The tag and content attributes here are really for AttestationObject, which contains an
+// "internally tagged" AttestationStatement.
+#[serde(tag = "fmt", content = "attStmt", rename_all = "lowercase")]
+pub enum AttestationStatement {
+ #[serde(deserialize_with = "deserialize_none_att_stmt")]
+ None,
+ Packed(AttestationStatementPacked),
+ #[serde(rename = "fido-u2f")]
+ FidoU2F(AttestationStatementFidoU2F),
+ // The remaining attestation statement formats are deserialized as serde_cbor::Values---we do
+ // not perform any validation of their contents. These are expected to be used primarily when
+ // anonymizing attestation objects that contain attestation statements in these formats.
+ #[serde(rename = "android-key")]
+ AndroidKey(serde_cbor::Value),
+ #[serde(rename = "android-safetynet")]
+ AndroidSafetyNet(serde_cbor::Value),
+ Apple(serde_cbor::Value),
+ Tpm(serde_cbor::Value),
+}
+
+// AttestationStatement::None is serialized as the empty map. We need to enforce
+// the emptyness condition manually while deserializing.
+fn deserialize_none_att_stmt<'de, D>(deserializer: D) -> Result<(), D::Error>
+where
+ D: Deserializer<'de>,
+{
+ let map = <std::collections::BTreeMap<(), ()>>::deserialize(deserializer)?;
+
+ if !map.is_empty() {
+ return Err(D::Error::invalid_value(Unexpected::Map, &"the empty map"));
+ }
+
+ Ok(())
+}
+
+// Not all crypto-backends currently provide "crypto::verify()", so we do not implement it yet.
+// Also not sure, if we really need it. Would be a sanity-check only, to verify the signature is valid,
+// before sendig it out.
+// impl AttestationStatement {
+// pub fn verify(&self, data: &[u8]) -> Result<bool, AuthenticatorError> {
+// match self {
+// AttestationStatement::None => Ok(true),
+// AttestationStatement::Unparsed(_) => Err(AuthenticatorError::Custom(
+// "Unparsed attestation object can't be used to verify signature.".to_string(),
+// )),
+// AttestationStatement::FidoU2F(att) => {
+// let res = crypto::verify(
+// crypto::SignatureAlgorithm::ES256,
+// &att.attestation_cert[0].as_ref(),
+// att.sig.as_ref(),
+// data,
+// )?;
+// Ok(res)
+// }
+// AttestationStatement::Packed(att) => {
+// if att.alg != Alg::ES256 {
+// return Err(AuthenticatorError::Custom(
+// "Verification only supported for ES256".to_string(),
+// ));
+// }
+// let res = crypto::verify(
+// crypto::SignatureAlgorithm::ES256,
+// att.attestation_cert[0].as_ref(),
+// att.sig.as_ref(),
+// data,
+// )?;
+// Ok(res)
+// }
+// }
+// }
+// }
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+// See https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation
+// u2fStmtFormat = {
+// x5c: [ attestnCert: bytes ],
+// sig: bytes
+// }
+pub struct AttestationStatementFidoU2F {
+ /// Certificate chain in x509 format
+ #[serde(rename = "x5c")]
+ pub attestation_cert: Vec<AttestationCertificate>, // (1) "x5c"
+ pub sig: Signature, // (2) "sig"
+}
+
+impl AttestationStatementFidoU2F {
+ pub fn new(cert: &[u8], signature: &[u8]) -> Self {
+ AttestationStatementFidoU2F {
+ attestation_cert: vec![AttestationCertificate(Vec::from(cert))],
+ sig: Signature::from(signature),
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+// https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation
+// packedStmtFormat = {
+// alg: COSEAlgorithmIdentifier,
+// sig: bytes,
+// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
+// } //
+// {
+// alg: COSEAlgorithmIdentifier
+// sig: bytes,
+// }
+pub struct AttestationStatementPacked {
+ pub alg: COSEAlgorithm, // (1) "alg"
+ pub sig: Signature, // (2) "sig"
+ /// Certificate chain in x509 format
+ #[serde(rename = "x5c", skip_serializing_if = "Vec::is_empty", default)]
+ pub attestation_cert: Vec<AttestationCertificate>, // (3) "x5c"
+}
+
+// A WebAuthn attestation object is a CBOR map with keys "fmt", "attStmt", and "authData". The
+// "fmt" field determines the type of "attStmt". The flatten attribute here turns the tag and
+// content attributes on AttestationStatement (defined above) into expected keys for
+// AttestationObject, which allows us to derive Deserialize. Like many of our other structs, the
+// derived Deserialize implementation is permissive: it does not enforce CTAP2 canonical CBOR
+// encoding and it allows repeated keys (the last one wins).
+#[derive(Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AttestationObject {
+ pub auth_data: AuthenticatorData,
+ #[serde(flatten)]
+ pub att_stmt: AttestationStatement,
+}
+
+impl AttestationObject {
+ pub fn anonymize(&mut self) {
+ // Remove the attestation statement and the AAGUID from the authenticator data.
+ self.att_stmt = AttestationStatement::None;
+ if let Some(credential_data) = self.auth_data.credential_data.as_mut() {
+ credential_data.aaguid = AAGuid::default();
+ }
+ }
+}
+
+impl Serialize for AttestationObject {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let map_len = 3;
+ let mut map = serializer.serialize_map(Some(map_len))?;
+
+ // CTAP2 canonical CBOR order for these entries is ("fmt", "attStmt", "authData")
+ // as strings are sorted by length and then lexically.
+ // see https://www.w3.org/TR/webauthn-2/#attestation-object
+ match self.att_stmt {
+ AttestationStatement::None => {
+ map.serialize_entry(&"fmt", &"none")?; // (1) "fmt"
+ let v = std::collections::BTreeMap::<(), ()>::new();
+ map.serialize_entry(&"attStmt", &v)?; // (2) "attStmt"
+ }
+ AttestationStatement::Packed(ref v) => {
+ map.serialize_entry(&"fmt", &"packed")?; // (1) "fmt"
+ map.serialize_entry(&"attStmt", v)?; // (2) "attStmt"
+ }
+ AttestationStatement::FidoU2F(ref v) => {
+ map.serialize_entry(&"fmt", &"fido-u2f")?; // (1) "fmt"
+ map.serialize_entry(&"attStmt", v)?; // (2) "attStmt"
+ }
+ AttestationStatement::AndroidKey(ref v) => {
+ map.serialize_entry(&"fmt", &"android-key")?; // (1) "fmt"
+ map.serialize_entry(&"attStmt", v)?; // (2) "attStmt"
+ }
+ AttestationStatement::AndroidSafetyNet(ref v) => {
+ map.serialize_entry(&"fmt", &"android-safetynet")?; // (1) "fmt"
+ map.serialize_entry(&"attStmt", v)?; // (2) "attStmt"
+ }
+ AttestationStatement::Apple(ref v) => {
+ map.serialize_entry(&"fmt", &"apple")?; // (1) "fmt"
+ map.serialize_entry(&"attStmt", v)?; // (2) "attStmt"
+ }
+ AttestationStatement::Tpm(ref v) => {
+ map.serialize_entry(&"fmt", &"tpm")?; // (1) "fmt"
+ map.serialize_entry(&"attStmt", v)?; // (2) "attStmt"
+ }
+ }
+ map.serialize_entry(&"authData", &self.auth_data)?; // (3) "authData"
+ map.end()
+ }
+}
+
+#[cfg(test)]
+pub mod test {
+ use super::super::utils::from_slice_stream;
+ use super::*;
+ use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve};
+ use serde_cbor::{from_slice, to_vec};
+
+ const SAMPLE_ATTESTATION_STMT_NONE: [u8; 19] = [
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x66, 0x6d, 0x74, // "fmt"
+ 0x64, // text(4)
+ 0x6e, 0x6f, 0x6e, 0x65, // "none"
+ 0x67, // text(7)
+ 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, // "attStmt"
+ 0xa0, // map(0)
+ ];
+
+ const SAMPLE_ATTESTATION_STMT_FIDO_U2F: [u8; 840] = [
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x66, 0x6d, 0x74, // "fmt"
+ 0x68, // text(8)
+ 0x66, 0x69, 0x64, 0x6f, 0x2d, 0x75, 0x32, 0x66, // "fido-u2f"
+ 0x67, // text(7)
+ 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, // "attStmt"
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x78, 0x35, 0x63, // "x5c"
+ 0x81, // array(1)
+ 0x59, 0x02, 0xdd, // bytes(733)
+ 0x30, 0x82, 0x02, 0xd9, 0x30, 0x82, 0x01, 0xc1, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09,
+ 0x00, 0xdf, 0x92, 0xd9, 0xc4, 0xe2, 0xed, 0x66, 0x0a, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86,
+ 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a,
+ 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55,
+ 0x32, 0x46, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69,
+ 0x61, 0x6c, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17,
+ 0x0d, 0x31, 0x34, 0x30, 0x38, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18,
+ 0x0f, 0x32, 0x30, 0x35, 0x30, 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x5a, 0x30, 0x6f, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x53,
+ 0x45, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x59, 0x75, 0x62,
+ 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b,
+ 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72,
+ 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x28, 0x30,
+ 0x26, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1f, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20,
+ 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x31,
+ 0x31, 0x35, 0x35, 0x31, 0x30, 0x39, 0x35, 0x39, 0x39, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07,
+ 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03,
+ 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x0a, 0x18, 0x6c, 0x6e, 0x4d, 0x0a, 0x6a, 0x52, 0x8a,
+ 0x44, 0x90, 0x9a, 0x7a, 0x24, 0x23, 0x68, 0x70, 0x28, 0xd4, 0xc5, 0x7e, 0xcc, 0xb7, 0x17,
+ 0xba, 0x12, 0x80, 0xb8, 0x5c, 0x2f, 0xc1, 0xe4, 0xe0, 0x61, 0x66, 0x8c, 0x3c, 0x20, 0xae,
+ 0xf3, 0x33, 0x50, 0xd1, 0x96, 0x45, 0x23, 0x8a, 0x2c, 0x39, 0x0b, 0xf5, 0xdf, 0xfa, 0x34,
+ 0xff, 0x25, 0x50, 0x2f, 0x47, 0x0f, 0x3d, 0x40, 0xb8, 0x88, 0xa3, 0x81, 0x81, 0x30, 0x7f,
+ 0x30, 0x13, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x0d, 0x01, 0x04,
+ 0x05, 0x04, 0x03, 0x05, 0x04, 0x03, 0x30, 0x22, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01,
+ 0x82, 0xc4, 0x0a, 0x02, 0x04, 0x15, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34,
+ 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34, 0x38, 0x32, 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x06,
+ 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03,
+ 0x02, 0x04, 0x30, 0x30, 0x21, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c,
+ 0x01, 0x01, 0x04, 0x04, 0x12, 0x04, 0x10, 0x2f, 0xc0, 0x57, 0x9f, 0x81, 0x13, 0x47, 0xea,
+ 0xb1, 0x16, 0xbb, 0x5a, 0x8d, 0xb9, 0x20, 0x2a, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13,
+ 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
+ 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x82, 0xac, 0xaf,
+ 0x11, 0x30, 0xa9, 0x9b, 0xd1, 0x43, 0x27, 0xd2, 0xf8, 0xf9, 0xb0, 0x41, 0xa2, 0xa0, 0x4a,
+ 0x66, 0x85, 0x27, 0x24, 0x22, 0xe5, 0x7b, 0x14, 0xb0, 0xb8, 0xf8, 0x3b, 0x6f, 0x15, 0x45,
+ 0x66, 0x4b, 0xbf, 0x55, 0x68, 0x1e, 0xaf, 0x01, 0x58, 0x72, 0x2a, 0xbf, 0xce, 0xd2, 0xe4,
+ 0xac, 0x63, 0x3c, 0xec, 0x09, 0x59, 0x56, 0x45, 0x24, 0xb0, 0xf2, 0xe5, 0x17, 0xdd, 0x97,
+ 0x10, 0x98, 0xb9, 0x89, 0x15, 0x17, 0xec, 0xd0, 0xc5, 0x53, 0xa2, 0xe4, 0x73, 0x9f, 0x9d,
+ 0xe1, 0x3d, 0xaf, 0xd0, 0xd5, 0xd7, 0xb8, 0xac, 0x4a, 0x37, 0xf4, 0xf2, 0xcc, 0x30, 0xef,
+ 0x25, 0xcb, 0x00, 0x65, 0x2d, 0x19, 0xdb, 0x69, 0xd7, 0xda, 0x57, 0xbd, 0x1a, 0x9c, 0x1d,
+ 0x8e, 0xd8, 0x7d, 0x46, 0xd8, 0x0d, 0x2b, 0x3b, 0xdf, 0xd1, 0xd9, 0xef, 0x9d, 0x2b, 0x68,
+ 0x32, 0xd4, 0xad, 0x5b, 0xcd, 0x74, 0x21, 0x4c, 0xe6, 0xa6, 0x14, 0x1d, 0x16, 0xb2, 0xe9,
+ 0x3a, 0xcb, 0x2c, 0x88, 0xf6, 0x0a, 0x3e, 0xb6, 0xd5, 0xf6, 0x14, 0x71, 0x97, 0x59, 0x09,
+ 0x37, 0x3b, 0xc6, 0x77, 0x90, 0x23, 0x24, 0x57, 0x1a, 0x57, 0x3f, 0x60, 0xf0, 0x7b, 0xbe,
+ 0xd1, 0x7b, 0x92, 0xc8, 0xb5, 0x9f, 0xa2, 0x82, 0x10, 0xbf, 0xa8, 0xc6, 0x01, 0x22, 0x93,
+ 0x00, 0x1b, 0x39, 0xef, 0xe5, 0x7b, 0xf9, 0xcb, 0x1e, 0x3a, 0xca, 0x8a, 0x41, 0x30, 0xf8,
+ 0x3a, 0xf8, 0x66, 0x8f, 0x73, 0xde, 0xf2, 0x71, 0x1b, 0x20, 0xdc, 0x99, 0xe8, 0xa8, 0x04,
+ 0xee, 0xa3, 0xf7, 0x42, 0x71, 0x97, 0xb6, 0xb4, 0x51, 0xb3, 0x73, 0x5c, 0x23, 0xbc, 0x9b,
+ 0x1b, 0xe2, 0x74, 0xc2, 0x6d, 0x3b, 0xf9, 0x19, 0x6f, 0x8c, 0x4a, 0x4b, 0x71, 0x5f, 0x4b,
+ 0x95, 0xc4, 0xdb, 0x7b, 0x97, 0xe7, 0x59, 0x4e, 0xb4, 0x65, 0x64, 0x8c, 0x1c, 0x63, 0x73,
+ 0x69, 0x67, // "sig"
+ 0x58, 0x46, // bytes(70)
+ 0x30, 0x44, 0x02, 0x20, 0x48, 0x5a, 0x72, 0x40, 0xdf, 0x2c, 0x1e, 0x31, 0xa5, 0xb3, 0x0b,
+ 0x3b, 0x2c, 0xd1, 0xad, 0xd0, 0x8d, 0xae, 0x8d, 0x7a, 0x25, 0x3e, 0xf5, 0xa6, 0x25, 0xdb,
+ 0x2e, 0x22, 0x1b, 0x71, 0xe5, 0x78, 0x02, 0x20, 0x45, 0xbd, 0xdc, 0x30, 0xde, 0xf4, 0x05,
+ 0x97, 0x5c, 0xac, 0x72, 0x58, 0x96, 0xa6, 0x00, 0x94, 0x57, 0x3a, 0xa5, 0xe8, 0x1e, 0xf4,
+ 0xfd, 0x30, 0xd3, 0x88, 0x11, 0x8b, 0x49, 0x97, 0xdf, 0x34,
+ ];
+
+ const SAMPLE_ATTESTATION_OBJ_PACKED: [u8; 677] = [
+ 0xa3, // map(3)
+ 0x63, // text(3)
+ 0x66, 0x6D, 0x74, // "fmt"
+ 0x66, // text(6)
+ 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed"
+ 0x67, // text(7)
+ 0x61, 0x74, 0x74, 0x53, 0x74, 0x6D, 0x74, // "attStmt"
+ 0xa3, // map(3)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x26, // -7 (ES256)
+ 0x63, // text(3)
+ 0x73, 0x69, 0x67, // "sig"
+ 0x58, 0x47, // bytes(71)
+ 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c,
+ 0xc9, // signature
+ 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, 0xf0, 0x56, 0x12,
+ 0x35, // ..
+ 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, 0x90, 0x35, 0x7f, 0xf9, 0x10,
+ 0xcc, // ..
+ 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2,
+ 0xb7, // ..
+ 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, // ..
+ 0x63, // text(3)
+ 0x78, 0x35, 0x63, // "x5c"
+ 0x81, // array(1)
+ 0x59, 0x01, 0x97, // bytes(407)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, //certificate...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b,
+ 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30,
+ 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b,
+ 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72,
+ 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17,
+ 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17,
+ 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30,
+ 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b,
+ 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72,
+ 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30,
+ 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48,
+ 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52,
+ 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf,
+ 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff,
+ 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79,
+ 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d,
+ 0x30, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a,
+ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46,
+ 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, 0x10,
+ 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, 0xda, 0x1f, 0xd2,
+ 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, 0x34, 0x45, 0xa8, 0x20,
+ 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5,
+ 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, 0xf3, 0x68, // text(8)
+ 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, // "authData"
+ 0x58, 0x94, // bytes(148)
+ // authData
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84,
+ 0x27, // rp_id_hash
+ 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a,
+ 0x87, // rp_id_hash
+ 0x05, 0x1d, // rp_id_hash
+ 0x41, // authData Flags
+ 0x00, 0x00, 0x00, 0x0b, // authData counter
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d, // AAGUID
+ 0x00, 0x10, // credential id length
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c,
+ 0x6f, // credential id
+ // credential public key
+ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1,
+ 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97,
+ 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20,
+ 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c,
+ 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e,
+ 0xa6, 0x1c,
+ ];
+
+ const SAMPLE_CERT_CHAIN: [u8; 709] = [
+ 0x81, 0x59, 0x2, 0xc1, 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1,
+ 0x2, 0x2, 0x4, 0x18, 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7,
+ 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3,
+ 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f,
+ 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35,
+ 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38,
+ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30,
+ 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb,
+ 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6,
+ 0x3, 0x55, 0x4, 0xa, 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31,
+ 0x22, 0x30, 0x20, 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e,
+ 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61,
+ 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59,
+ 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65,
+ 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30,
+ 0x59, 0x30, 0x13, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86,
+ 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49,
+ 0x70, 0x10, 0x62, 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83,
+ 0xf1, 0x0, 0xbe, 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e,
+ 0xcf, 0xca, 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e,
+ 0xbd, 0x37, 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30,
+ 0x6a, 0x30, 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15,
+ 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34,
+ 0x38, 0x32, 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82,
+ 0xe5, 0x1c, 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6,
+ 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48,
+ 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc,
+ 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a,
+ 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d,
+ 0x3, 0x97, 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22,
+ 0xfa, 0xa7, 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1,
+ 0x8a, 0x48, 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9,
+ 0xb1, 0xce, 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1,
+ 0xcb, 0xdd, 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15,
+ 0x9e, 0x7f, 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a,
+ 0xea, 0x17, 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7,
+ 0x53, 0xd7, 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d,
+ 0x43, 0x6, 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69,
+ 0x24, 0x22, 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a,
+ 0xa7, 0x15, 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1,
+ 0xc3, 0xb4, 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4,
+ 0x1a, 0xcb, 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84,
+ 0xbd, 0xdd, 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5,
+ 0x95, 0x27, 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd,
+ 0x19, 0x11, 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f,
+ 0x3a, 0xef, 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79,
+ ];
+
+ const SAMPLE_AUTH_DATA_MAKE_CREDENTIAL: [u8; 164] = [
+ 0x58, 0xA2, // bytes(162)
+ // authData
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84,
+ 0x27, // rp_id_hash
+ 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a,
+ 0x87, // rp_id_hash
+ 0x05, 0x1d, // rp_id_hash
+ 0xC1, // authData Flags
+ 0x00, 0x00, 0x00, 0x0b, // authData counter
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d, // AAGUID
+ 0x00, 0x10, // credential id length
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c,
+ 0x6f, // credential id
+ // credential public key
+ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1,
+ 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97,
+ 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20,
+ 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c,
+ 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e,
+ 0xa6, 0x1c, // pub key end
+ // Extensions
+ 0xA1, // map(1)
+ 0x6B, // text(11)
+ 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0xF5, // true
+ ];
+
+ const SAMPLE_AUTH_DATA_GET_ASSERTION: [u8; 229] = [
+ 0x58, 0xE3, // bytes(227)
+ // authData
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84,
+ 0x27, // rp_id_hash
+ 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a,
+ 0x87, // rp_id_hash
+ 0x05, 0x1d, // rp_id_hash
+ 0xC1, // authData Flags
+ 0x00, 0x00, 0x00, 0x0b, // authData counter
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d, // AAGUID
+ 0x00, 0x10, // credential id length
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c,
+ 0x6f, // credential id
+ // credential public key
+ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1,
+ 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97,
+ 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20,
+ 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c,
+ 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e,
+ 0xa6, 0x1c, // pub key end
+ // Extensions
+ 0xA1, // map(1)
+ 0x6B, // text(11)
+ 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0x58, 0x40, // bytes(64)
+ 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB, 0x87,
+ 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22, 0x69, 0xAF,
+ 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95, 0xA6, 0x8E, 0x79,
+ 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58, 0x87, 0xD6, 0x29, 0x0F,
+ 0xD4, 0x7A, 0x40, 0xC4,
+ ];
+
+ pub fn create_attestation_obj() -> AttestationObject {
+ AttestationObject {
+ auth_data: AuthenticatorData {
+ rp_id_hash: RpIdHash::from(&[
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d,
+ 0x84, 0x27, 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65,
+ 0xbe, 0x59, 0x7a, 0x87, 0x5, 0x1d,
+ ])
+ .unwrap(),
+ flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED,
+ counter: 11,
+ credential_data: Some(AttestedCredentialData {
+ aaguid: AAGuid::from(&[
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11,
+ 0x1f, 0x9e, 0xdc, 0x7d,
+ ])
+ .unwrap(),
+ credential_id: vec![
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6,
+ 0xd9, 0x43, 0x5c, 0x6f,
+ ],
+ credential_public_key: COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: vec![
+ 0xA5, 0xFD, 0x5C, 0xE1, 0xB1, 0xC4, 0x58, 0xC5, 0x30, 0xA5, 0x4F,
+ 0xA6, 0x1B, 0x31, 0xBF, 0x6B, 0x04, 0xBE, 0x8B, 0x97, 0xAF, 0xDE,
+ 0x54, 0xDD, 0x8C, 0xBB, 0x69, 0x27, 0x5A, 0x8A, 0x1B, 0xE1,
+ ],
+ y: vec![
+ 0xFA, 0x3A, 0x32, 0x31, 0xDD, 0x9D, 0xEE, 0xD9, 0xD1, 0x89, 0x7B,
+ 0xE5, 0xA6, 0x22, 0x8C, 0x59, 0x50, 0x1E, 0x4B, 0xCD, 0x12, 0x97,
+ 0x5D, 0x3D, 0xFF, 0x73, 0x0F, 0x01, 0x27, 0x8E, 0xA6, 0x1C,
+ ],
+ }),
+ },
+ }),
+ extensions: Default::default(),
+ },
+ att_stmt: AttestationStatement::Packed(AttestationStatementPacked {
+ alg: COSEAlgorithm::ES256,
+ sig: Signature(vec![
+ 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1,
+ 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5,
+ 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00,
+ 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19,
+ 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78,
+ 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29,
+ ]),
+ attestation_cert: vec![AttestationCertificate(vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02,
+ 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, 0x4c, 0x29, 0x30, 0x0a,
+ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x47, 0x31,
+ 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62,
+ 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06,
+ 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74,
+ 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x36, 0x31, 0x32,
+ 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x36,
+ 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x47,
+ 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53,
+ 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75,
+ 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20,
+ 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e,
+ 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73,
+ 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a,
+ 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d,
+ 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52,
+ 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4,
+ 0xe1, 0xaf, 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13,
+ 0xc3, 0xd5, 0x04, 0xff, 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96,
+ 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8,
+ 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, 0x30, 0x0b, 0x30, 0x09, 0x06,
+ 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, 0x06, 0x08, 0x2a,
+ 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02,
+ 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e,
+ 0x10, 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f,
+ 0xda, 0x1f, 0xd2, 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa,
+ 0xec, 0x34, 0x45, 0xa8, 0x20, 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe,
+ 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d,
+ 0xa2, 0x37, 0x23, 0xf3,
+ ])],
+ }),
+ }
+ }
+
+ #[test]
+ fn parse_cert_chain() {
+ let cert: AttestationCertificate = from_slice(&SAMPLE_CERT_CHAIN[1..]).unwrap();
+ assert_eq!(&cert.0, &SAMPLE_CERT_CHAIN[4..]);
+
+ let _cert: Vec<AttestationCertificate> = from_slice(&SAMPLE_CERT_CHAIN).unwrap();
+ }
+
+ #[test]
+ fn parse_attestation_statement() {
+ let actual: AttestationStatement = from_slice(&SAMPLE_ATTESTATION_STMT_NONE).unwrap();
+ let expected = AttestationStatement::None;
+ assert_eq!(expected, actual);
+
+ let actual: AttestationStatement = from_slice(&SAMPLE_ATTESTATION_STMT_FIDO_U2F).unwrap();
+ let expected = AttestationStatement::FidoU2F(AttestationStatementFidoU2F {
+ attestation_cert: vec![AttestationCertificate(vec![
+ 0x30, 0x82, 0x02, 0xd9, 0x30, 0x82, 0x01, 0xc1, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02,
+ 0x09, 0x00, 0xdf, 0x92, 0xd9, 0xc4, 0xe2, 0xed, 0x66, 0x0a, 0x30, 0x0d, 0x06, 0x09,
+ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x2e, 0x31,
+ 0x2c, 0x30, 0x2a, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x23, 0x59, 0x75, 0x62, 0x69,
+ 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41,
+ 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30, 0x30,
+ 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0d, 0x31, 0x34, 0x30, 0x38, 0x30, 0x31, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0x0f, 0x32, 0x30, 0x35, 0x30, 0x30, 0x39,
+ 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6f, 0x31, 0x0b, 0x30,
+ 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10,
+ 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20,
+ 0x41, 0x42, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41,
+ 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41,
+ 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x28, 0x30, 0x26,
+ 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1f, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20,
+ 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20,
+ 0x31, 0x31, 0x35, 0x35, 0x31, 0x30, 0x39, 0x35, 0x39, 0x39, 0x30, 0x59, 0x30, 0x13,
+ 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48,
+ 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x0a, 0x18, 0x6c, 0x6e, 0x4d,
+ 0x0a, 0x6a, 0x52, 0x8a, 0x44, 0x90, 0x9a, 0x7a, 0x24, 0x23, 0x68, 0x70, 0x28, 0xd4,
+ 0xc5, 0x7e, 0xcc, 0xb7, 0x17, 0xba, 0x12, 0x80, 0xb8, 0x5c, 0x2f, 0xc1, 0xe4, 0xe0,
+ 0x61, 0x66, 0x8c, 0x3c, 0x20, 0xae, 0xf3, 0x33, 0x50, 0xd1, 0x96, 0x45, 0x23, 0x8a,
+ 0x2c, 0x39, 0x0b, 0xf5, 0xdf, 0xfa, 0x34, 0xff, 0x25, 0x50, 0x2f, 0x47, 0x0f, 0x3d,
+ 0x40, 0xb8, 0x88, 0xa3, 0x81, 0x81, 0x30, 0x7f, 0x30, 0x13, 0x06, 0x0a, 0x2b, 0x06,
+ 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x0d, 0x01, 0x04, 0x05, 0x04, 0x03, 0x05, 0x04,
+ 0x03, 0x30, 0x22, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x02,
+ 0x04, 0x15, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e,
+ 0x34, 0x31, 0x34, 0x38, 0x32, 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x06, 0x0b, 0x2b,
+ 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02,
+ 0x04, 0x30, 0x30, 0x21, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c,
+ 0x01, 0x01, 0x04, 0x04, 0x12, 0x04, 0x10, 0x2f, 0xc0, 0x57, 0x9f, 0x81, 0x13, 0x47,
+ 0xea, 0xb1, 0x16, 0xbb, 0x5a, 0x8d, 0xb9, 0x20, 0x2a, 0x30, 0x0c, 0x06, 0x03, 0x55,
+ 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a,
+ 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01,
+ 0x00, 0x82, 0xac, 0xaf, 0x11, 0x30, 0xa9, 0x9b, 0xd1, 0x43, 0x27, 0xd2, 0xf8, 0xf9,
+ 0xb0, 0x41, 0xa2, 0xa0, 0x4a, 0x66, 0x85, 0x27, 0x24, 0x22, 0xe5, 0x7b, 0x14, 0xb0,
+ 0xb8, 0xf8, 0x3b, 0x6f, 0x15, 0x45, 0x66, 0x4b, 0xbf, 0x55, 0x68, 0x1e, 0xaf, 0x01,
+ 0x58, 0x72, 0x2a, 0xbf, 0xce, 0xd2, 0xe4, 0xac, 0x63, 0x3c, 0xec, 0x09, 0x59, 0x56,
+ 0x45, 0x24, 0xb0, 0xf2, 0xe5, 0x17, 0xdd, 0x97, 0x10, 0x98, 0xb9, 0x89, 0x15, 0x17,
+ 0xec, 0xd0, 0xc5, 0x53, 0xa2, 0xe4, 0x73, 0x9f, 0x9d, 0xe1, 0x3d, 0xaf, 0xd0, 0xd5,
+ 0xd7, 0xb8, 0xac, 0x4a, 0x37, 0xf4, 0xf2, 0xcc, 0x30, 0xef, 0x25, 0xcb, 0x00, 0x65,
+ 0x2d, 0x19, 0xdb, 0x69, 0xd7, 0xda, 0x57, 0xbd, 0x1a, 0x9c, 0x1d, 0x8e, 0xd8, 0x7d,
+ 0x46, 0xd8, 0x0d, 0x2b, 0x3b, 0xdf, 0xd1, 0xd9, 0xef, 0x9d, 0x2b, 0x68, 0x32, 0xd4,
+ 0xad, 0x5b, 0xcd, 0x74, 0x21, 0x4c, 0xe6, 0xa6, 0x14, 0x1d, 0x16, 0xb2, 0xe9, 0x3a,
+ 0xcb, 0x2c, 0x88, 0xf6, 0x0a, 0x3e, 0xb6, 0xd5, 0xf6, 0x14, 0x71, 0x97, 0x59, 0x09,
+ 0x37, 0x3b, 0xc6, 0x77, 0x90, 0x23, 0x24, 0x57, 0x1a, 0x57, 0x3f, 0x60, 0xf0, 0x7b,
+ 0xbe, 0xd1, 0x7b, 0x92, 0xc8, 0xb5, 0x9f, 0xa2, 0x82, 0x10, 0xbf, 0xa8, 0xc6, 0x01,
+ 0x22, 0x93, 0x00, 0x1b, 0x39, 0xef, 0xe5, 0x7b, 0xf9, 0xcb, 0x1e, 0x3a, 0xca, 0x8a,
+ 0x41, 0x30, 0xf8, 0x3a, 0xf8, 0x66, 0x8f, 0x73, 0xde, 0xf2, 0x71, 0x1b, 0x20, 0xdc,
+ 0x99, 0xe8, 0xa8, 0x04, 0xee, 0xa3, 0xf7, 0x42, 0x71, 0x97, 0xb6, 0xb4, 0x51, 0xb3,
+ 0x73, 0x5c, 0x23, 0xbc, 0x9b, 0x1b, 0xe2, 0x74, 0xc2, 0x6d, 0x3b, 0xf9, 0x19, 0x6f,
+ 0x8c, 0x4a, 0x4b, 0x71, 0x5f, 0x4b, 0x95, 0xc4, 0xdb, 0x7b, 0x97, 0xe7, 0x59, 0x4e,
+ 0xb4, 0x65, 0x64, 0x8c, 0x1c,
+ ])],
+ sig: Signature(vec![
+ 0x30, 0x44, 0x02, 0x20, 0x48, 0x5a, 0x72, 0x40, 0xdf, 0x2c, 0x1e, 0x31, 0xa5, 0xb3,
+ 0x0b, 0x3b, 0x2c, 0xd1, 0xad, 0xd0, 0x8d, 0xae, 0x8d, 0x7a, 0x25, 0x3e, 0xf5, 0xa6,
+ 0x25, 0xdb, 0x2e, 0x22, 0x1b, 0x71, 0xe5, 0x78, 0x02, 0x20, 0x45, 0xbd, 0xdc, 0x30,
+ 0xde, 0xf4, 0x05, 0x97, 0x5c, 0xac, 0x72, 0x58, 0x96, 0xa6, 0x00, 0x94, 0x57, 0x3a,
+ 0xa5, 0xe8, 0x1e, 0xf4, 0xfd, 0x30, 0xd3, 0x88, 0x11, 0x8b, 0x49, 0x97, 0xdf, 0x34,
+ ]),
+ });
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn parse_attestation_object() {
+ let actual: AttestationObject = from_slice(&SAMPLE_ATTESTATION_OBJ_PACKED).unwrap();
+ let expected = create_attestation_obj();
+
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn test_anonymize_att_obj() {
+ // Anonymize should prevent identifying data in the attestation statement from being
+ // serialized.
+ let mut att_obj = create_attestation_obj();
+
+ // This test assumes that the sample attestation object contains identifying information
+ assert_ne!(att_obj.att_stmt, AttestationStatement::None);
+ assert_ne!(
+ att_obj
+ .auth_data
+ .credential_data
+ .as_ref()
+ .expect("credential_data should be Some")
+ .aaguid,
+ AAGuid::default()
+ );
+
+ att_obj.anonymize();
+
+ // Write the attestation object out to bytes and read it back. The result should not
+ // have an attestation statement, and it should have the default AAGUID.
+ let encoded_att_obj = to_vec(&att_obj).expect("could not serialize anonymized att_obj");
+ let att_obj: AttestationObject =
+ from_slice(&encoded_att_obj).expect("could not deserialize anonymized att_obj");
+
+ assert_eq!(att_obj.att_stmt, AttestationStatement::None);
+ assert_eq!(
+ att_obj
+ .auth_data
+ .credential_data
+ .as_ref()
+ .expect("credential_data should be Some")
+ .aaguid,
+ AAGuid::default()
+ );
+ }
+
+ #[test]
+ fn parse_reader() {
+ let v: Vec<u8> = vec![
+ 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72,
+ ];
+ let mut data = Cursor::new(v);
+ let value: String = from_slice_stream::<_, _, serde_cbor::Error>(&mut data).unwrap();
+ assert_eq!(value, "foobar");
+ let mut remaining = Vec::new();
+ data.read_to_end(&mut remaining).unwrap();
+ assert_eq!(
+ remaining.as_slice(),
+ &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]
+ );
+ let mut data = Cursor::new(remaining);
+ let value: String = from_slice_stream::<_, _, serde_cbor::Error>(&mut data).unwrap();
+ assert_eq!(value, "foobar");
+ let mut remaining = Vec::new();
+ data.read_to_end(&mut remaining).unwrap();
+ assert!(remaining.is_empty());
+ }
+
+ #[test]
+ fn parse_extensions() {
+ let auth_make: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap();
+ assert_eq!(
+ auth_make.extensions.hmac_secret,
+ Some(HmacSecretResponse::Confirmed(true))
+ );
+ let auth_get: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_GET_ASSERTION).unwrap();
+ assert_eq!(
+ auth_get.extensions.hmac_secret,
+ Some(HmacSecretResponse::Secret(vec![
+ 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB,
+ 0x87, 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22,
+ 0x69, 0xAF, 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95,
+ 0xA6, 0x8E, 0x79, 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58,
+ 0x87, 0xD6, 0x29, 0x0F, 0xD4, 0x7A, 0x40, 0xC4,
+ ]))
+ );
+ }
+
+ #[test]
+ fn test_empty_extension_data() {
+ let mut parsed_auth_data: AuthenticatorData =
+ from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap();
+ assert!(parsed_auth_data
+ .flags
+ .contains(AuthenticatorDataFlags::EXTENSION_DATA));
+
+ // Remove the extension data but keep the extension data flag set.
+ parsed_auth_data.extensions = Default::default();
+ let with_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data");
+ // The serialized auth data should end with an empty map (CBOR 0xA0).
+ assert_eq!(with_flag[with_flag.len() - 1], 0xA0);
+
+ // Remove the extension data flag.
+ parsed_auth_data
+ .flags
+ .remove(AuthenticatorDataFlags::EXTENSION_DATA);
+ let without_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data");
+ // The serialized auth data should be one byte shorter.
+ assert!(with_flag.len() == without_flag.len() + 1);
+ }
+
+ /// See: https://github.com/mozilla/authenticator-rs/issues/187
+ #[test]
+ fn test_aaguid_output() {
+ let input = [
+ 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf0, 0x00, 0x39, 0x93, 0xec, 0x0a, 0x27, 0x29, 0xa1,
+ 0x54, 0xa8,
+ ];
+ let expected = "AAGuid(cb69481e-8ff0-0039-93ec-0a2729a154a8)";
+ let result = AAGuid::from(&input).expect("Failed to parse AAGuid");
+ let res_str = format!("{result:?}");
+ assert_eq!(expected, &res_str);
+ }
+
+ #[test]
+ fn test_ad_flags_from_bits() {
+ // Check that AuthenticatorDataFlags is defined on the entire u8 range and that
+ // `from_bits_truncate` is lossless
+ for x in 0..=u8::MAX {
+ assert_eq!(
+ AuthenticatorDataFlags::from_bits(x),
+ Some(AuthenticatorDataFlags::from_bits_truncate(x))
+ );
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/client_data.rs b/third_party/rust/authenticator/src/ctap2/client_data.rs
new file mode 100644
index 0000000000..872a119e0c
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/client_data.rs
@@ -0,0 +1,339 @@
+use super::commands::CommandError;
+use crate::transport::errors::HIDError;
+use base64::Engine;
+use serde::de::{self, Deserializer, Error as SerdeError, MapAccess, Visitor};
+use serde::ser::SerializeMap;
+use serde::{Deserialize, Serialize, Serializer};
+use serde_json as json;
+use sha2::{Digest, Sha256};
+use std::fmt;
+
+/// https://w3c.github.io/webauthn/#dom-collectedclientdata-tokenbinding
+// tokenBinding, of type TokenBinding
+//
+// This OPTIONAL member contains information about the state of the Token
+// Binding protocol [TokenBinding] used when communicating with the Relying
+// Party. Its absence indicates that the client doesn’t support token
+// binding.
+//
+// status, of type TokenBindingStatus
+//
+// This member is one of the following:
+//
+// supported
+//
+// Indicates the client supports token binding, but it was not
+// negotiated when communicating with the Relying Party.
+//
+// present
+//
+// Indicates token binding was used when communicating with the
+// Relying Party. In this case, the id member MUST be present.
+//
+// id, of type DOMString
+//
+// This member MUST be present if status is present, and MUST be a
+// base64url encoding of the Token Binding ID that was used when
+// communicating with the Relying Party.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum TokenBinding {
+ Present(String),
+ Supported,
+}
+
+impl Serialize for TokenBinding {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(2))?;
+ match *self {
+ TokenBinding::Supported => {
+ map.serialize_entry(&"status", &"supported")?;
+ }
+ TokenBinding::Present(ref v) => {
+ map.serialize_entry(&"status", "present")?;
+ // Verify here, that `v` is valid base64 encoded?
+ // base64::decode_config(&v, base64::URL_SAFE_NO_PAD);
+ // For now: Let the token do that.
+ map.serialize_entry(&"id", &v)?;
+ }
+ }
+ map.end()
+ }
+}
+
+impl<'de> Deserialize<'de> for TokenBinding {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct TokenBindingVisitor;
+
+ impl<'de> Visitor<'de> for TokenBindingVisitor {
+ type Value = TokenBinding;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte string")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut id = None;
+ let mut status = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ "status" => {
+ status = Some(map.next_value()?);
+ }
+ "id" => {
+ id = Some(map.next_value()?);
+ }
+ k => {
+ return Err(M::Error::custom(format!("unexpected key: {k:?}")));
+ }
+ }
+ }
+
+ if let Some(stat) = status {
+ match stat {
+ "present" => {
+ if let Some(id) = id {
+ Ok(TokenBinding::Present(id))
+ } else {
+ Err(SerdeError::missing_field("id"))
+ }
+ }
+ "supported" => Ok(TokenBinding::Supported),
+ k => Err(M::Error::custom(format!("unexpected status key: {k:?}"))),
+ }
+ } else {
+ Err(SerdeError::missing_field("status"))
+ }
+ }
+ }
+
+ deserializer.deserialize_map(TokenBindingVisitor)
+ }
+}
+
+/// https://w3c.github.io/webauthn/#dom-collectedclientdata-type
+// type, of type DOMString
+//
+// This member contains the string "webauthn.create" when creating new
+// credentials, and "webauthn.get" when getting an assertion from an
+// existing credential. The purpose of this member is to prevent certain
+// types of signature confusion attacks (where an attacker substitutes one
+// legitimate signature for another).
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum WebauthnType {
+ Create,
+ Get,
+}
+
+impl Serialize for WebauthnType {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match *self {
+ WebauthnType::Create => serializer.serialize_str("webauthn.create"),
+ WebauthnType::Get => serializer.serialize_str("webauthn.get"),
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for WebauthnType {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct WebauthnTypeVisitor;
+
+ impl<'de> Visitor<'de> for WebauthnTypeVisitor {
+ type Value = WebauthnType;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a string")
+ }
+
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ match v {
+ "webauthn.create" => Ok(WebauthnType::Create),
+ "webauthn.get" => Ok(WebauthnType::Get),
+ _ => Err(E::custom("unexpected webauthn_type")),
+ }
+ }
+ }
+
+ deserializer.deserialize_str(WebauthnTypeVisitor)
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
+pub struct Challenge(pub String);
+
+impl Challenge {
+ pub fn new(input: Vec<u8>) -> Self {
+ let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input);
+ Challenge(value)
+ }
+}
+
+impl From<Vec<u8>> for Challenge {
+ fn from(v: Vec<u8>) -> Challenge {
+ Challenge::new(v)
+ }
+}
+
+impl AsRef<[u8]> for Challenge {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_bytes()
+ }
+}
+
+pub type Origin = String;
+
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
+pub struct CollectedClientData {
+ #[serde(rename = "type")]
+ pub webauthn_type: WebauthnType,
+ pub challenge: Challenge,
+ pub origin: Origin,
+ // It is optional, according to https://www.w3.org/TR/webauthn/#collectedclientdata-hash-of-the-serialized-client-data
+ // But we are serializing it, so we *have to* set crossOrigin (if not given, we have to set it to false)
+ // Thus, on our side, it is not optional. For deserializing, we provide a default (bool's default == False)
+ #[serde(rename = "crossOrigin", default)]
+ pub cross_origin: bool,
+ #[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")]
+ pub token_binding: Option<TokenBinding>,
+}
+
+impl CollectedClientData {
+ pub fn hash(&self) -> Result<ClientDataHash, HIDError> {
+ // WebIDL's dictionary definition specifies that the order of the struct
+ // is exactly as the WebIDL specification declares it, with an algorithm
+ // for partial dictionaries, so that's how interop works for these
+ // things.
+ // See: https://heycam.github.io/webidl/#dfn-dictionary
+ let json = json::to_vec(&self).map_err(CommandError::Json)?;
+ let digest = Sha256::digest(json);
+ Ok(ClientDataHash(digest.into()))
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ClientDataHash(pub [u8; 32]);
+
+impl PartialEq<[u8]> for ClientDataHash {
+ fn eq(&self, other: &[u8]) -> bool {
+ self.0.eq(other)
+ }
+}
+
+impl AsRef<[u8]> for ClientDataHash {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
+}
+
+impl Serialize for ClientDataHash {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_bytes(&self.0)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType};
+ use serde_json as json;
+
+ #[test]
+ fn test_token_binding_status() {
+ let tok = TokenBinding::Present("AAECAw".to_string());
+
+ let json_value = json::to_string(&tok).unwrap();
+ assert_eq!(json_value, "{\"status\":\"present\",\"id\":\"AAECAw\"}");
+
+ let tok = TokenBinding::Supported;
+
+ let json_value = json::to_string(&tok).unwrap();
+ assert_eq!(json_value, "{\"status\":\"supported\"}");
+ }
+
+ #[test]
+ fn test_webauthn_type() {
+ let t = WebauthnType::Create;
+
+ let json_value = json::to_string(&t).unwrap();
+ assert_eq!(json_value, "\"webauthn.create\"");
+
+ let t = WebauthnType::Get;
+ let json_value = json::to_string(&t).unwrap();
+ assert_eq!(json_value, "\"webauthn.get\"");
+ }
+
+ #[test]
+ fn test_collected_client_data_parsing() {
+ let original_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
+ let parsed: CollectedClientData = serde_json::from_str(original_str).unwrap();
+ let expected = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
+ };
+ assert_eq!(parsed, expected);
+
+ let back_again = serde_json::to_string(&expected).unwrap();
+ assert_eq!(back_again, original_str);
+ }
+
+ #[test]
+ fn test_collected_client_data_defaults() {
+ let cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
+ let no_cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
+ let parsed: CollectedClientData = serde_json::from_str(no_cross_origin_str).unwrap();
+ let expected = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
+ };
+ assert_eq!(parsed, expected);
+
+ let back_again = serde_json::to_string(&expected).unwrap();
+ assert_eq!(back_again, cross_origin_str);
+ }
+
+ #[test]
+ fn test_collected_client_data() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
+ };
+ assert_eq!(
+ client_data.hash().expect("failed to serialize client data"),
+ // echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t
+ ClientDataHash([
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32,
+ 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87,
+ 0x54, 0xc3, 0x2d, 0x80
+ ])
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs b/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs
new file mode 100644
index 0000000000..f72640d701
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs
@@ -0,0 +1,225 @@
+use super::{Command, CommandError, PinUvAuthCommand, RequestCtap2, StatusCode};
+use crate::{
+ crypto::{PinUvAuthParam, PinUvAuthToken},
+ ctap2::server::UserVerificationRequirement,
+ errors::AuthenticatorError,
+ transport::errors::HIDError,
+ AuthenticatorInfo, FidoDevice,
+};
+use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
+use serde_cbor::{de::from_slice, to_vec, Value};
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct SetMinPINLength {
+ /// Minimum PIN length in code points
+ pub new_min_pin_length: Option<u64>,
+ /// RP IDs which are allowed to get this information via the minPinLength extension.
+ /// This parameter MUST NOT be used unless the minPinLength extension is supported.
+ pub min_pin_length_rpids: Option<Vec<String>>,
+ /// The authenticator returns CTAP2_ERR_PIN_POLICY_VIOLATION until changePIN is successful.
+ pub force_change_pin: Option<bool>,
+}
+
+impl Serialize for SetMinPINLength {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map_len = 0;
+ if self.new_min_pin_length.is_some() {
+ map_len += 1;
+ }
+ if self.min_pin_length_rpids.is_some() {
+ map_len += 1;
+ }
+ if self.force_change_pin.is_some() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ if let Some(new_min_pin_length) = self.new_min_pin_length {
+ map.serialize_entry(&0x01, &new_min_pin_length)?;
+ }
+ if let Some(min_pin_length_rpids) = &self.min_pin_length_rpids {
+ map.serialize_entry(&0x02, &min_pin_length_rpids)?;
+ }
+ if let Some(force_change_pin) = self.force_change_pin {
+ map.serialize_entry(&0x03, &force_change_pin)?;
+ }
+ map.end()
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub enum AuthConfigCommand {
+ EnableEnterpriseAttestation,
+ ToggleAlwaysUv,
+ SetMinPINLength(SetMinPINLength),
+}
+
+impl AuthConfigCommand {
+ fn has_params(&self) -> bool {
+ match self {
+ AuthConfigCommand::EnableEnterpriseAttestation => false,
+ AuthConfigCommand::ToggleAlwaysUv => false,
+ AuthConfigCommand::SetMinPINLength(..) => true,
+ }
+ }
+}
+
+#[derive(Debug, Serialize)]
+pub enum AuthConfigResult {
+ Success(AuthenticatorInfo),
+}
+
+#[derive(Debug)]
+pub struct AuthenticatorConfig {
+ subcommand: AuthConfigCommand, // subCommand currently being requested
+ pin_uv_auth_param: Option<PinUvAuthParam>, // First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken.
+}
+
+impl AuthenticatorConfig {
+ pub(crate) fn new(subcommand: AuthConfigCommand) -> Self {
+ Self {
+ subcommand,
+ pin_uv_auth_param: None,
+ }
+ }
+}
+
+impl Serialize for AuthenticatorConfig {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 1;
+ if self.pin_uv_auth_param.is_some() {
+ map_len += 2;
+ }
+ if self.subcommand.has_params() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+
+ match &self.subcommand {
+ AuthConfigCommand::EnableEnterpriseAttestation => {
+ map.serialize_entry(&0x01, &0x01)?;
+ }
+ AuthConfigCommand::ToggleAlwaysUv => {
+ map.serialize_entry(&0x01, &0x02)?;
+ }
+ AuthConfigCommand::SetMinPINLength(params) => {
+ map.serialize_entry(&0x01, &0x03)?;
+ map.serialize_entry(&0x02, &params)?;
+ }
+ }
+
+ if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param {
+ map.serialize_entry(&0x03, &pin_uv_auth_param.pin_protocol.id())?;
+ map.serialize_entry(&0x04, pin_uv_auth_param)?;
+ }
+
+ map.end()
+ }
+}
+
+impl RequestCtap2 for AuthenticatorConfig {
+ type Output = ();
+
+ fn command(&self) -> Command {
+ Command::AuthenticatorConfig
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ let output = to_vec(&self).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: FidoDevice,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if status.is_ok() {
+ Ok(())
+ } else {
+ let msg = if input.len() > 1 {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Some(data)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, msg).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: crate::VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ unimplemented!()
+ }
+}
+
+impl PinUvAuthCommand for AuthenticatorConfig {
+ fn set_pin_uv_auth_param(
+ &mut self,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+ ) -> Result<(), AuthenticatorError> {
+ let mut param = None;
+ if let Some(token) = pin_uv_auth_token {
+ // pinUvAuthParam (0x04): the result of calling
+ // authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams).
+ let mut data = vec![0xff; 32];
+ data.push(0x0D);
+ match &self.subcommand {
+ AuthConfigCommand::EnableEnterpriseAttestation => {
+ data.push(0x01);
+ }
+ AuthConfigCommand::ToggleAlwaysUv => {
+ data.push(0x02);
+ }
+ AuthConfigCommand::SetMinPINLength(params) => {
+ data.push(0x03);
+ data.extend(to_vec(params).map_err(CommandError::Serializing)?);
+ }
+ }
+ param = Some(token.derive(&data).map_err(CommandError::Crypto)?);
+ }
+ self.pin_uv_auth_param = param;
+ Ok(())
+ }
+
+ fn can_skip_user_verification(
+ &mut self,
+ authinfo: &AuthenticatorInfo,
+ _uv_req: UserVerificationRequirement,
+ ) -> bool {
+ !authinfo.device_is_protected()
+ }
+
+ fn set_uv_option(&mut self, _uv: Option<bool>) {
+ /* No-op */
+ }
+
+ fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> {
+ self.pin_uv_auth_param.as_ref()
+ }
+
+ fn get_rp_id(&self) -> Option<&String> {
+ None
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs b/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs
new file mode 100644
index 0000000000..817bb5ac51
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs
@@ -0,0 +1,660 @@
+use crate::{
+ crypto::{PinUvAuthParam, PinUvAuthToken},
+ ctap2::server::UserVerificationRequirement,
+ errors::AuthenticatorError,
+ transport::errors::HIDError,
+ AuthenticatorInfo, FidoDevice,
+};
+use serde::{
+ de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor::{from_slice, to_vec, Value};
+use std::fmt;
+
+use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode};
+
+#[derive(Debug, Clone, Copy)]
+pub enum BioEnrollmentModality {
+ Fingerprint,
+ Other(u8),
+}
+
+impl From<u8> for BioEnrollmentModality {
+ fn from(value: u8) -> Self {
+ match value {
+ 0x01 => BioEnrollmentModality::Fingerprint,
+ x => BioEnrollmentModality::Other(x),
+ }
+ }
+}
+
+impl From<BioEnrollmentModality> for u8 {
+ fn from(value: BioEnrollmentModality) -> Self {
+ match value {
+ BioEnrollmentModality::Fingerprint => 0x01,
+ BioEnrollmentModality::Other(x) => x,
+ }
+ }
+}
+
+impl Serialize for BioEnrollmentModality {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_u8((*self).into())
+ }
+}
+
+impl<'de> Deserialize<'de> for BioEnrollmentModality {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct BioEnrollmentModalityVisitor;
+
+ impl<'de> Visitor<'de> for BioEnrollmentModalityVisitor {
+ type Value = BioEnrollmentModality;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("an integer")
+ }
+
+ fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ Ok(BioEnrollmentModality::from(v))
+ }
+ }
+
+ deserializer.deserialize_u8(BioEnrollmentModalityVisitor)
+ }
+}
+
+pub type BioTemplateId = Vec<u8>;
+#[derive(Debug, Clone, Deserialize, Default)]
+struct BioEnrollmentParams {
+ template_id: Option<BioTemplateId>, // Template Identifier.
+ template_friendly_name: Option<String>, // Template Friendly Name.
+ timeout_milliseconds: Option<u64>, // Timeout in milliSeconds.
+}
+
+impl BioEnrollmentParams {
+ fn has_some(&self) -> bool {
+ self.template_id.is_some()
+ || self.template_friendly_name.is_some()
+ || self.timeout_milliseconds.is_some()
+ }
+}
+
+impl Serialize for BioEnrollmentParams {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map_len = 0;
+ if self.template_id.is_some() {
+ map_len += 1;
+ }
+ if self.template_friendly_name.is_some() {
+ map_len += 1;
+ }
+ if self.timeout_milliseconds.is_some() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ if let Some(template_id) = &self.template_id {
+ map.serialize_entry(&0x01, &ByteBuf::from(template_id.as_slice()))?;
+ }
+ if let Some(template_friendly_name) = &self.template_friendly_name {
+ map.serialize_entry(&0x02, template_friendly_name)?;
+ }
+ if let Some(timeout_milliseconds) = &self.timeout_milliseconds {
+ map.serialize_entry(&0x03, timeout_milliseconds)?;
+ }
+ map.end()
+ }
+}
+
+#[derive(Debug)]
+pub enum BioEnrollmentCommand {
+ EnrollBegin(Option<u64>),
+ EnrollCaptureNextSample((BioTemplateId, Option<u64>)),
+ CancelCurrentEnrollment,
+ EnumerateEnrollments,
+ SetFriendlyName((BioTemplateId, String)),
+ RemoveEnrollment(BioTemplateId),
+ GetFingerprintSensorInfo,
+}
+
+impl BioEnrollmentCommand {
+ fn to_id_and_param(&self) -> (u8, BioEnrollmentParams) {
+ let mut params = BioEnrollmentParams::default();
+ match &self {
+ BioEnrollmentCommand::EnrollBegin(timeout) => {
+ params.timeout_milliseconds = *timeout;
+ (0x01, params)
+ }
+ BioEnrollmentCommand::EnrollCaptureNextSample((id, timeout)) => {
+ params.template_id = Some(id.clone());
+ params.timeout_milliseconds = *timeout;
+ (0x02, params)
+ }
+ BioEnrollmentCommand::CancelCurrentEnrollment => (0x03, params),
+ BioEnrollmentCommand::EnumerateEnrollments => (0x04, params),
+ BioEnrollmentCommand::SetFriendlyName((id, name)) => {
+ params.template_id = Some(id.clone());
+ params.template_friendly_name = Some(name.clone());
+ (0x05, params)
+ }
+ BioEnrollmentCommand::RemoveEnrollment(id) => {
+ params.template_id = Some(id.clone());
+ (0x06, params)
+ }
+ BioEnrollmentCommand::GetFingerprintSensorInfo => (0x07, params),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct BioEnrollment {
+ /// The user verification modality being requested
+ modality: BioEnrollmentModality,
+ /// The authenticator user verification sub command currently being requested
+ pub(crate) subcommand: BioEnrollmentCommand,
+ /// First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken.
+ pin_uv_auth_param: Option<PinUvAuthParam>,
+ use_legacy_preview: bool,
+}
+
+impl BioEnrollment {
+ pub(crate) fn new(subcommand: BioEnrollmentCommand, use_legacy_preview: bool) -> Self {
+ Self {
+ modality: BioEnrollmentModality::Fingerprint, // As per spec: Currently always "Fingerprint"
+ subcommand,
+ pin_uv_auth_param: None,
+ use_legacy_preview,
+ }
+ }
+}
+
+impl Serialize for BioEnrollment {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 2;
+ let (id, params) = self.subcommand.to_id_and_param();
+ if params.has_some() {
+ map_len += 1;
+ }
+ if self.pin_uv_auth_param.is_some() {
+ map_len += 2;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+
+ map.serialize_entry(&0x01, &self.modality)?; // Per spec currently always Fingerprint
+ map.serialize_entry(&0x02, &id)?;
+ if params.has_some() {
+ map.serialize_entry(&0x03, &params)?;
+ }
+
+ if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param {
+ map.serialize_entry(&0x04, &pin_uv_auth_param.pin_protocol.id())?;
+ map.serialize_entry(&0x05, pin_uv_auth_param)?;
+ }
+
+ map.end()
+ }
+}
+
+impl PinUvAuthCommand for BioEnrollment {
+ fn get_rp_id(&self) -> Option<&String> {
+ None
+ }
+
+ fn set_pin_uv_auth_param(
+ &mut self,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+ ) -> Result<(), AuthenticatorError> {
+ let mut param = None;
+ if let Some(token) = pin_uv_auth_token {
+ // pinUvAuthParam (0x04): the result of calling
+ // authenticate(pinUvAuthToken, fingerprint (0x01) || uint8(subCommand) || subCommandParams).
+ let (id, params) = self.subcommand.to_id_and_param();
+ let modality = self.modality.into();
+ let mut data = vec![modality, id];
+ if params.has_some() {
+ data.extend(to_vec(&params).map_err(CommandError::Serializing)?);
+ }
+ param = Some(token.derive(&data).map_err(CommandError::Crypto)?);
+ }
+ self.pin_uv_auth_param = param;
+ Ok(())
+ }
+
+ fn can_skip_user_verification(
+ &mut self,
+ _info: &crate::AuthenticatorInfo,
+ _uv: UserVerificationRequirement,
+ ) -> bool {
+ // "discouraged" does not exist for BioEnrollment
+ false
+ }
+
+ fn set_uv_option(&mut self, _uv: Option<bool>) {
+ /* No-op */
+ }
+
+ fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> {
+ self.pin_uv_auth_param.as_ref()
+ }
+}
+
+impl RequestCtap2 for BioEnrollment {
+ type Output = BioEnrollmentResponse;
+
+ fn command(&self) -> Command {
+ if self.use_legacy_preview {
+ Command::BioEnrollmentPreview
+ } else {
+ Command::BioEnrollment
+ }
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ let output = to_vec(&self).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: FidoDevice,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+ if status.is_ok() {
+ if input.len() > 1 {
+ trace!("parsing bio enrollment response data: {:#04X?}", &input);
+ let bio_enrollment =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Ok(bio_enrollment)
+ } else {
+ // Some subcommands return only an OK-status without any data
+ Ok(BioEnrollmentResponse::default())
+ }
+ } else {
+ let data: Option<Value> = if input.len() > 1 {
+ Some(from_slice(&input[1..]).map_err(CommandError::Deserializing)?)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, data).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: crate::VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ unimplemented!()
+ }
+}
+
+#[derive(Debug, Copy, Clone, Serialize)]
+pub enum LastEnrollmentSampleStatus {
+ /// Good fingerprint capture.
+ Ctap2EnrollFeedbackFpGood,
+ /// Fingerprint was too high.
+ Ctap2EnrollFeedbackFpTooHigh,
+ /// Fingerprint was too low.
+ Ctap2EnrollFeedbackFpTooLow,
+ /// Fingerprint was too left.
+ Ctap2EnrollFeedbackFpTooLeft,
+ /// Fingerprint was too right.
+ Ctap2EnrollFeedbackFpTooRight,
+ /// Fingerprint was too fast.
+ Ctap2EnrollFeedbackFpTooFast,
+ /// Fingerprint was too slow.
+ Ctap2EnrollFeedbackFpTooSlow,
+ /// Fingerprint was of poor quality.
+ Ctap2EnrollFeedbackFpPoorQuality,
+ /// Fingerprint was too skewed.
+ Ctap2EnrollFeedbackFpTooSkewed,
+ /// Fingerprint was too short.
+ Ctap2EnrollFeedbackFpTooShort,
+ /// Merge failure of the capture.
+ Ctap2EnrollFeedbackFpMergeFailure,
+ /// Fingerprint already exists.
+ Ctap2EnrollFeedbackFpExists,
+ /// (this error number is available)
+ Unused,
+ /// User did not touch/swipe the authenticator.
+ Ctap2EnrollFeedbackNoUserActivity,
+ /// User did not lift the finger off the sensor.
+ Ctap2EnrollFeedbackNoUserPresenceTransition,
+ /// Other possible failure cases that are not (yet) defined by the spec
+ Ctap2EnrollFeedbackOther(u8),
+}
+
+impl<'de> Deserialize<'de> for LastEnrollmentSampleStatus {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct LastEnrollmentSampleStatusVisitor;
+
+ impl<'de> Visitor<'de> for LastEnrollmentSampleStatusVisitor {
+ type Value = LastEnrollmentSampleStatus;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("an integer")
+ }
+
+ fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ match v {
+ 0x00 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpGood),
+ 0x01 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooHigh),
+ 0x02 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLow),
+ 0x03 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLeft),
+ 0x04 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooRight),
+ 0x05 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooFast),
+ 0x06 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSlow),
+ 0x07 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpPoorQuality),
+ 0x08 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSkewed),
+ 0x09 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooShort),
+ 0x0A => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpMergeFailure),
+ 0x0B => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpExists),
+ 0x0C => Ok(LastEnrollmentSampleStatus::Unused),
+ 0x0D => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserActivity),
+ 0x0E => {
+ Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserPresenceTransition)
+ }
+ x => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackOther(x)),
+ }
+ }
+ }
+
+ deserializer.deserialize_u8(LastEnrollmentSampleStatusVisitor)
+ }
+}
+
+#[derive(Debug, Copy, Clone, Serialize)]
+pub enum FingerprintKind {
+ TouchSensor,
+ SwipeSensor,
+ // Not (yet) defined by the spec
+ Other(u8),
+}
+
+impl<'de> Deserialize<'de> for FingerprintKind {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct FingerprintKindVisitor;
+
+ impl<'de> Visitor<'de> for FingerprintKindVisitor {
+ type Value = FingerprintKind;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("an integer")
+ }
+
+ fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ match v {
+ 0x01 => Ok(FingerprintKind::TouchSensor),
+ 0x02 => Ok(FingerprintKind::SwipeSensor),
+ x => Ok(FingerprintKind::Other(x)),
+ }
+ }
+ }
+
+ deserializer.deserialize_u8(FingerprintKindVisitor)
+ }
+}
+
+#[derive(Debug, Serialize)]
+pub(crate) struct BioTemplateInfo {
+ template_id: BioTemplateId,
+ template_friendly_name: Option<String>,
+}
+
+impl<'de> Deserialize<'de> for BioTemplateInfo {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct BioTemplateInfoResponseVisitor;
+
+ impl<'de> Visitor<'de> for BioTemplateInfoResponseVisitor {
+ type Value = BioTemplateInfo;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut template_id = None; // (0x01)
+ let mut template_friendly_name = None; // (0x02)
+ while let Some(key) = map.next_key()? {
+ match key {
+ 0x01 => {
+ if template_id.is_some() {
+ return Err(SerdeError::duplicate_field("template_id"));
+ }
+ template_id = Some(map.next_value::<ByteBuf>()?.into_vec());
+ }
+ 0x02 => {
+ if template_friendly_name.is_some() {
+ return Err(SerdeError::duplicate_field("template_friendly_name"));
+ }
+ template_friendly_name = Some(map.next_value()?);
+ }
+ k => {
+ warn!("BioTemplateInfo: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ continue;
+ }
+ }
+ }
+
+ if let Some(template_id) = template_id {
+ Ok(BioTemplateInfo {
+ template_id,
+ template_friendly_name,
+ })
+ } else {
+ Err(SerdeError::missing_field("template_id"))
+ }
+ }
+ }
+ deserializer.deserialize_bytes(BioTemplateInfoResponseVisitor)
+ }
+}
+
+#[derive(Default, Debug)]
+pub struct BioEnrollmentResponse {
+ /// The user verification modality.
+ pub(crate) modality: Option<BioEnrollmentModality>,
+ /// Indicates the type of fingerprint sensor. For touch type sensor, its value is 1. For swipe type sensor its value is 2.
+ pub(crate) fingerprint_kind: Option<FingerprintKind>,
+ /// Indicates the maximum good samples required for enrollment.
+ pub(crate) max_capture_samples_required_for_enroll: Option<u64>,
+ /// Template Identifier.
+ pub(crate) template_id: Option<BioTemplateId>,
+ /// Last enrollment sample status.
+ pub(crate) last_enroll_sample_status: Option<LastEnrollmentSampleStatus>,
+ /// Number of more sample required for enrollment to complete
+ pub(crate) remaining_samples: Option<u64>,
+ /// Array of templateInfo’s
+ pub(crate) template_infos: Vec<BioTemplateInfo>,
+ /// Indicates the maximum number of bytes the authenticator will accept as a templateFriendlyName.
+ pub(crate) max_template_friendly_name: Option<u64>,
+}
+
+impl CtapResponse for BioEnrollmentResponse {}
+
+impl<'de> Deserialize<'de> for BioEnrollmentResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct BioEnrollmentResponseVisitor;
+
+ impl<'de> Visitor<'de> for BioEnrollmentResponseVisitor {
+ type Value = BioEnrollmentResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut modality = None; // (0x01)
+ let mut fingerprint_kind = None; // (0x02)
+ let mut max_capture_samples_required_for_enroll = None; // (0x03)
+ let mut template_id = None; // (0x04)
+ let mut last_enroll_sample_status = None; // (0x05)
+ let mut remaining_samples = None; // (0x06)
+ let mut template_infos = None; // (0x07)
+ let mut max_template_friendly_name = None; // (0x08)
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ 0x01 => {
+ if modality.is_some() {
+ return Err(SerdeError::duplicate_field("modality"));
+ }
+ modality = Some(map.next_value()?);
+ }
+ 0x02 => {
+ if fingerprint_kind.is_some() {
+ return Err(SerdeError::duplicate_field("fingerprint_kind"));
+ }
+ fingerprint_kind = Some(map.next_value()?);
+ }
+ 0x03 => {
+ if max_capture_samples_required_for_enroll.is_some() {
+ return Err(SerdeError::duplicate_field(
+ "max_capture_samples_required_for_enroll",
+ ));
+ }
+ max_capture_samples_required_for_enroll = Some(map.next_value()?);
+ }
+ 0x04 => {
+ if template_id.is_some() {
+ return Err(SerdeError::duplicate_field("template_id"));
+ }
+ template_id = Some(map.next_value::<ByteBuf>()?.into_vec());
+ }
+ 0x05 => {
+ if last_enroll_sample_status.is_some() {
+ return Err(SerdeError::duplicate_field(
+ "last_enroll_sample_status",
+ ));
+ }
+ last_enroll_sample_status = Some(map.next_value()?);
+ }
+ 0x06 => {
+ if remaining_samples.is_some() {
+ return Err(SerdeError::duplicate_field("remaining_samples"));
+ }
+ remaining_samples = Some(map.next_value()?);
+ }
+ 0x07 => {
+ if template_infos.is_some() {
+ return Err(SerdeError::duplicate_field("template_infos"));
+ }
+ template_infos = Some(map.next_value()?);
+ }
+ 0x08 => {
+ if max_template_friendly_name.is_some() {
+ return Err(SerdeError::duplicate_field(
+ "max_template_friendly_name",
+ ));
+ }
+ max_template_friendly_name = Some(map.next_value()?);
+ }
+ k => {
+ warn!("BioEnrollmentResponse: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ continue;
+ }
+ }
+ }
+
+ Ok(BioEnrollmentResponse {
+ modality,
+ fingerprint_kind,
+ max_capture_samples_required_for_enroll,
+ template_id,
+ last_enroll_sample_status,
+ remaining_samples,
+ template_infos: template_infos.unwrap_or_default(),
+ max_template_friendly_name,
+ })
+ }
+ }
+ deserializer.deserialize_bytes(BioEnrollmentResponseVisitor)
+ }
+}
+
+#[derive(Debug, Serialize)]
+pub struct EnrollmentInfo {
+ pub template_id: Vec<u8>,
+ pub template_friendly_name: Option<String>,
+}
+
+impl From<&BioTemplateInfo> for EnrollmentInfo {
+ fn from(value: &BioTemplateInfo) -> Self {
+ Self {
+ template_id: value.template_id.to_vec(),
+ template_friendly_name: value.template_friendly_name.clone(),
+ }
+ }
+}
+
+#[derive(Debug, Serialize)]
+pub struct FingerprintSensorInfo {
+ pub fingerprint_kind: FingerprintKind,
+ pub max_capture_samples_required_for_enroll: u64,
+ pub max_template_friendly_name: Option<u64>,
+}
+
+#[derive(Debug, Serialize)]
+pub enum BioEnrollmentResult {
+ EnrollmentList(Vec<EnrollmentInfo>),
+ DeleteSuccess(AuthenticatorInfo),
+ UpdateSuccess,
+ AddSuccess(AuthenticatorInfo),
+ FingerprintSensorInfo(FingerprintSensorInfo),
+ SampleStatus(LastEnrollmentSampleStatus, u64),
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs
new file mode 100644
index 0000000000..eb3b713655
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs
@@ -0,0 +1,851 @@
+#![allow(non_upper_case_globals)]
+use super::CtapResponse;
+// Note: Needed for PinUvAuthTokenPermission
+// The current version of `bitflags` doesn't seem to allow
+// to set this for an individual bitflag-struct.
+use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode};
+use crate::crypto::{COSEKey, CryptoError, PinUvAuthProtocol, SharedSecret};
+use crate::transport::errors::HIDError;
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use serde::{
+ de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::{ByteBuf, Bytes};
+use serde_cbor::de::from_slice;
+use serde_cbor::ser::to_vec;
+use serde_cbor::Value;
+use sha2::{Digest, Sha256};
+use std::convert::TryFrom;
+use std::error::Error as StdErrorT;
+use std::fmt;
+
+#[derive(Debug, Copy, Clone)]
+#[repr(u8)]
+pub enum PINSubcommand {
+ GetPinRetries = 0x01,
+ GetKeyAgreement = 0x02,
+ SetPIN = 0x03,
+ ChangePIN = 0x04,
+ GetPINToken = 0x05, // superseded by GetPinUvAuth*
+ GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
+ GetUvRetries = 0x07,
+ GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // Yes, 0x08 is missing
+}
+
+bitflags! {
+ pub struct PinUvAuthTokenPermission: u8 {
+ const MakeCredential = 0x01; // rp_id required
+ const GetAssertion = 0x02; // rp_id required
+ const CredentialManagement = 0x04; // rp_id optional
+ const BioEnrollment = 0x08; // rp_id ignored
+ const LargeBlobWrite = 0x10; // rp_id ignored
+ const AuthenticatorConfiguration = 0x20; // rp_id ignored
+ }
+}
+
+impl Default for PinUvAuthTokenPermission {
+ fn default() -> Self {
+ // CTAP 2.1 spec:
+ // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions
+ // of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken.
+ PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion
+ }
+}
+
+#[derive(Debug)]
+pub struct ClientPIN {
+ pub pin_protocol: Option<PinUvAuthProtocol>,
+ pub subcommand: PINSubcommand,
+ pub key_agreement: Option<COSEKey>,
+ pub pin_auth: Option<Vec<u8>>,
+ pub new_pin_enc: Option<Vec<u8>>,
+ pub pin_hash_enc: Option<Vec<u8>>,
+ pub permissions: Option<u8>,
+ pub rp_id: Option<String>,
+}
+
+impl Default for ClientPIN {
+ fn default() -> Self {
+ ClientPIN {
+ pin_protocol: None,
+ subcommand: PINSubcommand::GetPinRetries,
+ key_agreement: None,
+ pin_auth: None,
+ new_pin_enc: None,
+ pin_hash_enc: None,
+ permissions: None,
+ rp_id: None,
+ }
+ }
+}
+
+impl Serialize for ClientPIN {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 1;
+ if self.pin_protocol.is_some() {
+ map_len += 1;
+ }
+ if self.key_agreement.is_some() {
+ map_len += 1;
+ }
+ if self.pin_auth.is_some() {
+ map_len += 1;
+ }
+ if self.new_pin_enc.is_some() {
+ map_len += 1;
+ }
+ if self.pin_hash_enc.is_some() {
+ map_len += 1;
+ }
+ if self.permissions.is_some() {
+ map_len += 1;
+ }
+ if self.rp_id.is_some() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ if let Some(ref pin_protocol) = self.pin_protocol {
+ map.serialize_entry(&1, &pin_protocol.id())?;
+ }
+ let command: u8 = self.subcommand as u8;
+ map.serialize_entry(&2, &command)?;
+ if let Some(ref key_agreement) = self.key_agreement {
+ map.serialize_entry(&3, key_agreement)?;
+ }
+ if let Some(ref pin_auth) = self.pin_auth {
+ map.serialize_entry(&4, Bytes::new(pin_auth))?;
+ }
+ if let Some(ref new_pin_enc) = self.new_pin_enc {
+ map.serialize_entry(&5, Bytes::new(new_pin_enc))?;
+ }
+ if let Some(ref pin_hash_enc) = self.pin_hash_enc {
+ map.serialize_entry(&6, Bytes::new(pin_hash_enc))?;
+ }
+ if let Some(ref permissions) = self.permissions {
+ map.serialize_entry(&9, permissions)?;
+ }
+ if let Some(ref rp_id) = self.rp_id {
+ map.serialize_entry(&0x0A, rp_id)?;
+ }
+
+ map.end()
+ }
+}
+
+pub trait ClientPINSubCommand {
+ type Output;
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError>;
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError>;
+}
+
+#[derive(Default, Debug, PartialEq, Eq)]
+pub struct ClientPinResponse {
+ pub key_agreement: Option<COSEKey>,
+ pub pin_token: Option<Vec<u8>>,
+ /// Number of PIN attempts remaining before lockout.
+ pub pin_retries: Option<u8>,
+ pub power_cycle_state: Option<bool>,
+ pub uv_retries: Option<u8>,
+}
+
+impl CtapResponse for ClientPinResponse {}
+
+impl<'de> Deserialize<'de> for ClientPinResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct ClientPinResponseVisitor;
+
+ impl<'de> Visitor<'de> for ClientPinResponseVisitor {
+ type Value = ClientPinResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut key_agreement = None;
+ let mut pin_token = None;
+ let mut pin_retries = None;
+ let mut power_cycle_state = None;
+ let mut uv_retries = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ 0x01 => {
+ if key_agreement.is_some() {
+ return Err(SerdeError::duplicate_field("key_agreement"));
+ }
+ key_agreement = map.next_value()?;
+ }
+ 0x02 => {
+ if pin_token.is_some() {
+ return Err(SerdeError::duplicate_field("pin_token"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ pin_token = Some(value.into_vec());
+ }
+ 0x03 => {
+ if pin_retries.is_some() {
+ return Err(SerdeError::duplicate_field("pin_retries"));
+ }
+ pin_retries = Some(map.next_value()?);
+ }
+ 0x04 => {
+ if power_cycle_state.is_some() {
+ return Err(SerdeError::duplicate_field("power_cycle_state"));
+ }
+ power_cycle_state = Some(map.next_value()?);
+ }
+ 0x05 => {
+ if uv_retries.is_some() {
+ return Err(SerdeError::duplicate_field("uv_retries"));
+ }
+ uv_retries = Some(map.next_value()?);
+ }
+ k => {
+ warn!("ClientPinResponse: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ continue;
+ }
+ }
+ }
+ Ok(ClientPinResponse {
+ key_agreement,
+ pin_token,
+ pin_retries,
+ power_cycle_state,
+ uv_retries,
+ })
+ }
+ }
+ deserializer.deserialize_bytes(ClientPinResponseVisitor)
+ }
+}
+
+#[derive(Debug)]
+pub struct GetKeyAgreement {
+ pin_protocol: PinUvAuthProtocol,
+}
+
+impl GetKeyAgreement {
+ pub fn new(pin_protocol: PinUvAuthProtocol) -> Self {
+ GetKeyAgreement { pin_protocol }
+ }
+}
+
+impl ClientPINSubCommand for GetKeyAgreement {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ Ok(ClientPIN {
+ pin_protocol: Some(self.pin_protocol.clone()),
+ subcommand: PINSubcommand::GetKeyAgreement,
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ if get_pin_response.key_agreement.is_none() {
+ return Err(CommandError::MissingRequiredField("key_agreement"));
+ }
+ Ok(get_pin_response)
+ }
+}
+
+#[derive(Debug)]
+/// Superseded by GetPinUvAuthTokenUsingUvWithPermissions or
+/// GetPinUvAuthTokenUsingPinWithPermissions, thus for backwards compatibility only
+pub struct GetPinToken<'sc, 'pin> {
+ shared_secret: &'sc SharedSecret,
+ pin: &'pin Pin,
+}
+
+impl<'sc, 'pin> GetPinToken<'sc, 'pin> {
+ pub fn new(shared_secret: &'sc SharedSecret, pin: &'pin Pin) -> Self {
+ GetPinToken { shared_secret, pin }
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ let input = self.pin.for_pin_token();
+ trace!("pin_hash = {:#04X?}", &input);
+ let pin_hash_enc = self.shared_secret.encrypt(&input)?;
+ trace!("pin_hash_enc = {:#04X?}", &pin_hash_enc);
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.shared_secret.pin_protocol.clone()),
+ subcommand: PINSubcommand::GetPINToken,
+ key_agreement: Some(self.shared_secret.client_input().clone()),
+ pin_hash_enc: Some(pin_hash_enc),
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ if get_pin_response.pin_token.is_none() {
+ return Err(CommandError::MissingRequiredField("pin_token"));
+ }
+ Ok(get_pin_response)
+ }
+}
+
+#[derive(Debug)]
+pub struct GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
+ shared_secret: &'sc SharedSecret,
+ pin: &'pin Pin,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+}
+
+impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
+ pub fn new(
+ shared_secret: &'sc SharedSecret,
+ pin: &'pin Pin,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+ ) -> Self {
+ GetPinUvAuthTokenUsingPinWithPermissions {
+ shared_secret,
+ pin,
+ permissions,
+ rp_id,
+ }
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ let input = self.pin.for_pin_token();
+ let pin_hash_enc = self.shared_secret.encrypt(&input)?;
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.shared_secret.pin_protocol.clone()),
+ subcommand: PINSubcommand::GetPinUvAuthTokenUsingPinWithPermissions,
+ key_agreement: Some(self.shared_secret.client_input().clone()),
+ pin_hash_enc: Some(pin_hash_enc),
+ permissions: Some(self.permissions.bits()),
+ rp_id: self.rp_id.clone(), /* TODO: This could probably be done less wasteful with
+ * &str all the way */
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ if get_pin_response.pin_token.is_none() {
+ return Err(CommandError::MissingRequiredField("pin_token"));
+ }
+ Ok(get_pin_response)
+ }
+}
+
+macro_rules! implementRetries {
+ ($name:ident, $getter:ident) => {
+ #[derive(Debug, Default)]
+ pub struct $name {}
+
+ impl $name {
+ pub fn new() -> Self {
+ Self {}
+ }
+ }
+
+ impl ClientPINSubCommand for $name {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ Ok(ClientPIN {
+ subcommand: PINSubcommand::$name,
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ if get_pin_response.$getter.is_none() {
+ return Err(CommandError::MissingRequiredField(stringify!($getter)));
+ }
+ Ok(get_pin_response)
+ }
+ }
+ };
+}
+
+implementRetries!(GetPinRetries, pin_retries);
+implementRetries!(GetUvRetries, uv_retries);
+
+#[derive(Debug)]
+pub struct GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
+ shared_secret: &'sc SharedSecret,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+}
+
+impl<'sc> GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
+ pub fn new(
+ shared_secret: &'sc SharedSecret,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+ ) -> Self {
+ GetPinUvAuthTokenUsingUvWithPermissions {
+ shared_secret,
+ permissions,
+ rp_id,
+ }
+ }
+}
+
+impl<'sc> ClientPINSubCommand for GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ Ok(ClientPIN {
+ pin_protocol: Some(self.shared_secret.pin_protocol.clone()),
+ subcommand: PINSubcommand::GetPinUvAuthTokenUsingUvWithPermissions,
+ key_agreement: Some(self.shared_secret.client_input().clone()),
+ permissions: Some(self.permissions.bits()),
+ rp_id: self.rp_id.clone(), /* TODO: This could probably be done less wasteful with
+ * &str all the way */
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ if get_pin_response.pin_token.is_none() {
+ return Err(CommandError::MissingRequiredField("pin_token"));
+ }
+ Ok(get_pin_response)
+ }
+}
+
+#[derive(Debug)]
+pub struct SetNewPin<'sc, 'pin> {
+ shared_secret: &'sc SharedSecret,
+ new_pin: &'pin Pin,
+}
+
+impl<'sc, 'pin> SetNewPin<'sc, 'pin> {
+ pub fn new(shared_secret: &'sc SharedSecret, new_pin: &'pin Pin) -> Self {
+ SetNewPin {
+ shared_secret,
+ new_pin,
+ }
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for SetNewPin<'sc, 'pin> {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ if self.new_pin.as_bytes().len() > 63 {
+ return Err(CommandError::StatusCode(
+ StatusCode::PinPolicyViolation,
+ None,
+ ));
+ }
+
+ // newPinEnc: the result of calling encrypt(shared secret, paddedPin) where paddedPin is
+ // newPin padded on the right with 0x00 bytes to make it 64 bytes long. (Since the maximum
+ // length of newPin is 63 bytes, there is always at least one byte of padding.)
+ let new_pin_padded = self.new_pin.padded();
+ let new_pin_enc = self.shared_secret.encrypt(&new_pin_padded)?;
+
+ // pinUvAuthParam: the result of calling authenticate(shared secret, newPinEnc).
+ let pin_auth = self.shared_secret.authenticate(&new_pin_enc)?;
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.shared_secret.pin_protocol.clone()),
+ subcommand: PINSubcommand::SetPIN,
+ key_agreement: Some(self.shared_secret.client_input().clone()),
+ new_pin_enc: Some(new_pin_enc),
+ pin_auth: Some(pin_auth),
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ // Should be an empty response or a valid cbor-value (which we ignore)
+ if !input.is_empty() {
+ let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ }
+ Ok(ClientPinResponse::default())
+ }
+}
+
+#[derive(Debug)]
+pub struct ChangeExistingPin<'sc, 'pin> {
+ pin_protocol: PinUvAuthProtocol,
+ shared_secret: &'sc SharedSecret,
+ current_pin: &'pin Pin,
+ new_pin: &'pin Pin,
+}
+
+impl<'sc, 'pin> ChangeExistingPin<'sc, 'pin> {
+ pub fn new(
+ info: &AuthenticatorInfo,
+ shared_secret: &'sc SharedSecret,
+ current_pin: &'pin Pin,
+ new_pin: &'pin Pin,
+ ) -> Result<Self, CommandError> {
+ Ok(ChangeExistingPin {
+ pin_protocol: PinUvAuthProtocol::try_from(info)?,
+ shared_secret,
+ current_pin,
+ new_pin,
+ })
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for ChangeExistingPin<'sc, 'pin> {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ if self.new_pin.as_bytes().len() > 63 {
+ return Err(CommandError::StatusCode(
+ StatusCode::PinPolicyViolation,
+ None,
+ ));
+ }
+
+ // newPinEnc: the result of calling encrypt(shared secret, paddedPin) where paddedPin is
+ // newPin padded on the right with 0x00 bytes to make it 64 bytes long. (Since the maximum
+ // length of newPin is 63 bytes, there is always at least one byte of padding.)
+ let new_pin_padded = self.new_pin.padded();
+ let new_pin_enc = self.shared_secret.encrypt(&new_pin_padded)?;
+
+ let current_pin_hash = self.current_pin.for_pin_token();
+ let pin_hash_enc = self.shared_secret.encrypt(current_pin_hash.as_ref())?;
+
+ let pin_auth = self
+ .shared_secret
+ .authenticate(&[new_pin_enc.as_slice(), pin_hash_enc.as_slice()].concat())?;
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.shared_secret.pin_protocol.clone()),
+ subcommand: PINSubcommand::ChangePIN,
+ key_agreement: Some(self.shared_secret.client_input().clone()),
+ new_pin_enc: Some(new_pin_enc),
+ pin_hash_enc: Some(pin_hash_enc),
+ pin_auth: Some(pin_auth),
+ permissions: None,
+ rp_id: None,
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ // Should be an empty response or a valid cbor-value (which we ignore)
+ if !input.is_empty() {
+ let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ }
+ Ok(ClientPinResponse::default())
+ }
+}
+
+impl<T> RequestCtap2 for T
+where
+ T: ClientPINSubCommand<Output = ClientPinResponse>,
+ T: fmt::Debug,
+{
+ type Output = <T as ClientPINSubCommand>::Output;
+
+ fn command(&self) -> Command {
+ Command::ClientPin
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ let client_pin = self.as_client_pin()?;
+ let output = to_vec(&client_pin).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ trace!("Client pin subcomand response:{:04X?}", &input);
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!("response status code: {:?}", status);
+ if status.is_ok() {
+ <T as ClientPINSubCommand>::parse_response_payload(self, &input[1..])
+ .map_err(HIDError::Command)
+ } else {
+ let add_data = if input.len() > 1 {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Some(data)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, add_data).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<ClientPinResponse, HIDError> {
+ dev.client_pin(&self.as_client_pin()?)
+ }
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+pub struct Pin(String);
+
+impl fmt::Debug for Pin {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "Pin(redacted)")
+ }
+}
+
+impl Pin {
+ pub fn new(value: &str) -> Pin {
+ Pin(String::from(value))
+ }
+
+ pub fn for_pin_token(&self) -> Vec<u8> {
+ let mut hasher = Sha256::new();
+ hasher.update(self.0.as_bytes());
+
+ let mut output = [0u8; 16];
+ let len = output.len();
+ output.copy_from_slice(&hasher.finalize().as_slice()[..len]);
+
+ output.to_vec()
+ }
+
+ pub fn padded(&self) -> Vec<u8> {
+ let mut out = self.0.as_bytes().to_vec();
+ out.resize(64, 0x00);
+ out
+ }
+
+ pub fn as_bytes(&self) -> &[u8] {
+ self.0.as_bytes()
+ }
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub enum PinError {
+ PinRequired,
+ PinIsTooShort,
+ PinIsTooLong(usize),
+ InvalidPin(Option<u8>),
+ InvalidUv(Option<u8>),
+ PinAuthBlocked,
+ PinBlocked,
+ PinNotSet,
+ UvBlocked,
+ /// Used for CTAP2.0 UV (fingerprints)
+ PinAuthInvalid,
+ Crypto(CryptoError),
+}
+
+impl fmt::Display for PinError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ PinError::PinRequired => write!(f, "Pin required."),
+ PinError::PinIsTooShort => write!(f, "pin is too short"),
+ PinError::PinIsTooLong(len) => write!(f, "pin is too long ({len})"),
+ PinError::InvalidPin(ref e) => {
+ let mut res = write!(f, "Invalid Pin.");
+ if let Some(pin_retries) = e {
+ res = write!(f, " Retries left: {pin_retries:?}")
+ }
+ res
+ }
+ PinError::InvalidUv(ref e) => {
+ let mut res = write!(f, "Invalid Uv.");
+ if let Some(uv_retries) = e {
+ res = write!(f, " Retries left: {uv_retries:?}")
+ }
+ res
+ }
+ PinError::PinAuthBlocked => {
+ write!(f, "Pin authentication blocked. Device needs power cycle.")
+ }
+ PinError::PinBlocked => write!(f, "No retries left. Pin blocked. Device needs reset."),
+ PinError::PinNotSet => write!(f, "Pin needed but not set on device."),
+ PinError::UvBlocked => write!(f, "No retries left. Uv blocked. Device needs reset."),
+ PinError::PinAuthInvalid => write!(f, "PinAuth invalid."),
+ PinError::Crypto(ref e) => write!(f, "Crypto backend error: {e:?}"),
+ }
+ }
+}
+
+impl StdErrorT for PinError {}
+
+impl From<CryptoError> for PinError {
+ fn from(e: CryptoError) -> Self {
+ PinError::Crypto(e)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::ClientPinResponse;
+ use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve};
+ use serde_cbor::de::from_slice;
+
+ #[test]
+ fn test_get_key_agreement() {
+ let reference = [
+ 161, 1, 165, 1, 2, 3, 56, 24, 32, 1, 33, 88, 32, 115, 222, 167, 5, 88, 238, 119, 202,
+ 121, 23, 241, 150, 9, 48, 197, 136, 174, 0, 17, 90, 190, 83, 65, 103, 237, 97, 41, 213,
+ 128, 111, 7, 106, 34, 88, 32, 248, 204, 9, 26, 82, 96, 25, 72, 5, 82, 251, 185, 22, 39,
+ 246, 149, 54, 246, 255, 225, 52, 102, 67, 221, 113, 194, 236, 213, 199, 147, 180, 81,
+ ];
+ let expected = ClientPinResponse {
+ key_agreement: Some(COSEKey {
+ alg: COSEAlgorithm::ECDH_ES_HKDF256,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: vec![
+ 115, 222, 167, 5, 88, 238, 119, 202, 121, 23, 241, 150, 9, 48, 197, 136,
+ 174, 0, 17, 90, 190, 83, 65, 103, 237, 97, 41, 213, 128, 111, 7, 106,
+ ],
+ y: vec![
+ 248, 204, 9, 26, 82, 96, 25, 72, 5, 82, 251, 185, 22, 39, 246, 149, 54,
+ 246, 255, 225, 52, 102, 67, 221, 113, 194, 236, 213, 199, 147, 180, 81,
+ ],
+ }),
+ }),
+ pin_token: None,
+ pin_retries: None,
+ power_cycle_state: None,
+ uv_retries: None,
+ };
+ let result: ClientPinResponse =
+ from_slice(&reference).expect("could not deserialize reference");
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_get_pin_retries() {
+ let reference = [161, 3, 7];
+ let expected = ClientPinResponse {
+ key_agreement: None,
+ pin_token: None,
+ pin_retries: Some(7),
+ power_cycle_state: None,
+ uv_retries: None,
+ };
+ let result: ClientPinResponse =
+ from_slice(&reference).expect("could not deserialize reference");
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_get_uv_retries() {
+ let reference = [161, 5, 2];
+ let expected = ClientPinResponse {
+ key_agreement: None,
+ pin_token: None,
+ pin_retries: None,
+ power_cycle_state: None,
+ uv_retries: Some(2),
+ };
+ let result: ClientPinResponse =
+ from_slice(&reference).expect("could not deserialize reference");
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_get_pin_token() {
+ let reference = [
+ 161, 2, 88, 48, 173, 244, 214, 87, 128, 57, 25, 99, 142, 140, 41, 25, 94, 60, 75, 163,
+ 240, 187, 211, 138, 11, 208, 74, 117, 180, 181, 97, 31, 79, 252, 191, 244, 49, 13, 201,
+ 217, 204, 219, 122, 3, 101, 4, 70, 26, 14, 41, 150, 148,
+ ];
+ let expected = ClientPinResponse {
+ key_agreement: None,
+ pin_token: Some(vec![
+ 173, 244, 214, 87, 128, 57, 25, 99, 142, 140, 41, 25, 94, 60, 75, 163, 240, 187,
+ 211, 138, 11, 208, 74, 117, 180, 181, 97, 31, 79, 252, 191, 244, 49, 13, 201, 217,
+ 204, 219, 122, 3, 101, 4, 70, 26, 14, 41, 150, 148,
+ ]),
+ pin_retries: None,
+ power_cycle_state: None,
+ uv_retries: None,
+ };
+ let result: ClientPinResponse =
+ from_slice(&reference).expect("could not deserialize reference");
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_get_puat_using_uv() {
+ let reference = [
+ 161, 2, 88, 48, 94, 109, 192, 236, 90, 161, 77, 153, 23, 146, 179, 189, 133, 106, 76,
+ 150, 17, 238, 155, 102, 107, 201, 98, 232, 184, 33, 153, 224, 203, 87, 147, 10, 21, 20,
+ 85, 184, 109, 61, 240, 58, 236, 198, 171, 48, 242, 165, 221, 214,
+ ];
+ let expected = ClientPinResponse {
+ key_agreement: None,
+ pin_token: Some(vec![
+ 94, 109, 192, 236, 90, 161, 77, 153, 23, 146, 179, 189, 133, 106, 76, 150, 17, 238,
+ 155, 102, 107, 201, 98, 232, 184, 33, 153, 224, 203, 87, 147, 10, 21, 20, 85, 184,
+ 109, 61, 240, 58, 236, 198, 171, 48, 242, 165, 221, 214,
+ ]),
+ pin_retries: None,
+ power_cycle_state: None,
+ uv_retries: None,
+ };
+ let result: ClientPinResponse =
+ from_slice(&reference).expect("could not deserialize reference");
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_get_puat_using_pin() {
+ let reference = [
+ 161, 2, 88, 48, 143, 174, 68, 241, 186, 39, 106, 238, 129, 15, 181, 102, 112, 130, 239,
+ 96, 106, 235, 3, 10, 61, 173, 106, 252, 38, 236, 44, 112, 91, 34, 218, 136, 139, 118,
+ 162, 178, 172, 227, 82, 103, 136, 91, 136, 178, 170, 233, 156, 62,
+ ];
+ let expected = ClientPinResponse {
+ key_agreement: None,
+ pin_token: Some(vec![
+ 143, 174, 68, 241, 186, 39, 106, 238, 129, 15, 181, 102, 112, 130, 239, 96, 106,
+ 235, 3, 10, 61, 173, 106, 252, 38, 236, 44, 112, 91, 34, 218, 136, 139, 118, 162,
+ 178, 172, 227, 82, 103, 136, 91, 136, 178, 170, 233, 156, 62,
+ ]),
+ pin_retries: None,
+ power_cycle_state: None,
+ uv_retries: None,
+ };
+ let result: ClientPinResponse =
+ from_slice(&reference).expect("could not deserialize reference");
+ assert_eq!(expected, result);
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs b/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs
new file mode 100644
index 0000000000..2a58bb1791
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs
@@ -0,0 +1,457 @@
+use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode};
+use crate::{
+ crypto::{COSEKey, PinUvAuthParam, PinUvAuthToken},
+ ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash,
+ UserVerificationRequirement,
+ },
+ errors::AuthenticatorError,
+ transport::errors::HIDError,
+ FidoDevice,
+};
+use serde::{
+ de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor::{de::from_slice, to_vec, Value};
+use std::fmt;
+
+#[derive(Debug, Clone, Deserialize, Default)]
+struct CredManagementParams {
+ rp_id_hash: Option<RpIdHash>, // RP ID SHA-256 hash
+ credential_id: Option<PublicKeyCredentialDescriptor>, // Credential Identifier
+ user: Option<PublicKeyCredentialUserEntity>, // User Entity
+}
+
+impl CredManagementParams {
+ fn has_some(&self) -> bool {
+ self.rp_id_hash.is_some() || self.credential_id.is_some() || self.user.is_some()
+ }
+}
+
+impl Serialize for CredManagementParams {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map_len = 0;
+ if self.rp_id_hash.is_some() {
+ map_len += 1;
+ }
+ if self.credential_id.is_some() {
+ map_len += 1;
+ }
+ if self.user.is_some() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ if let Some(rp_id_hash) = &self.rp_id_hash {
+ map.serialize_entry(&0x01, &ByteBuf::from(rp_id_hash.as_ref()))?;
+ }
+ if let Some(credential_id) = &self.credential_id {
+ map.serialize_entry(&0x02, credential_id)?;
+ }
+ if let Some(user) = &self.user {
+ map.serialize_entry(&0x03, user)?;
+ }
+ map.end()
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub(crate) enum CredManagementCommand {
+ GetCredsMetadata,
+ EnumerateRPsBegin,
+ EnumerateRPsGetNextRP,
+ EnumerateCredentialsBegin(RpIdHash),
+ EnumerateCredentialsGetNextCredential,
+ DeleteCredential(PublicKeyCredentialDescriptor),
+ UpdateUserInformation((PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity)),
+}
+
+impl CredManagementCommand {
+ fn to_id_and_param(&self) -> (u8, CredManagementParams) {
+ let mut params = CredManagementParams::default();
+ match &self {
+ CredManagementCommand::GetCredsMetadata => (0x01, params),
+ CredManagementCommand::EnumerateRPsBegin => (0x02, params),
+ CredManagementCommand::EnumerateRPsGetNextRP => (0x03, params),
+ CredManagementCommand::EnumerateCredentialsBegin(rp_id_hash) => {
+ params.rp_id_hash = Some(rp_id_hash.clone());
+ (0x04, params)
+ }
+ CredManagementCommand::EnumerateCredentialsGetNextCredential => (0x05, params),
+ CredManagementCommand::DeleteCredential(cred_id) => {
+ params.credential_id = Some(cred_id.clone());
+ (0x06, params)
+ }
+ CredManagementCommand::UpdateUserInformation((cred_id, user)) => {
+ params.credential_id = Some(cred_id.clone());
+ params.user = Some(user.clone());
+ (0x07, params)
+ }
+ }
+ }
+}
+#[derive(Debug)]
+pub struct CredentialManagement {
+ pub(crate) subcommand: CredManagementCommand, // subCommand currently being requested
+ pin_uv_auth_param: Option<PinUvAuthParam>, // First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken.
+ use_legacy_preview: bool,
+}
+
+impl CredentialManagement {
+ pub(crate) fn new(subcommand: CredManagementCommand, use_legacy_preview: bool) -> Self {
+ Self {
+ subcommand,
+ pin_uv_auth_param: None,
+ use_legacy_preview,
+ }
+ }
+}
+
+impl Serialize for CredentialManagement {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 1;
+ let (id, params) = self.subcommand.to_id_and_param();
+ if params.has_some() {
+ map_len += 1;
+ }
+ if self.pin_uv_auth_param.is_some() {
+ map_len += 2;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+
+ map.serialize_entry(&0x01, &id)?;
+ if params.has_some() {
+ map.serialize_entry(&0x02, &params)?;
+ }
+
+ if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param {
+ map.serialize_entry(&0x03, &pin_uv_auth_param.pin_protocol.id())?;
+ map.serialize_entry(&0x04, pin_uv_auth_param)?;
+ }
+
+ map.end()
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct CredentialManagementResponse {
+ /// Number of existing discoverable credentials present on the authenticator.
+ pub existing_resident_credentials_count: Option<u64>,
+ /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator.
+ pub max_possible_remaining_resident_credentials_count: Option<u64>,
+ /// RP Information
+ pub rp: Option<RelyingParty>,
+ /// RP ID SHA-256 hash
+ pub rp_id_hash: Option<RpIdHash>,
+ /// Total number of RPs present on the authenticator
+ pub total_rps: Option<u64>,
+ /// User Information
+ pub user: Option<PublicKeyCredentialUserEntity>,
+ /// Credential ID
+ pub credential_id: Option<PublicKeyCredentialDescriptor>,
+ /// Public key of the credential.
+ pub public_key: Option<COSEKey>,
+ /// Total number of credentials present on the authenticator for the RP in question
+ pub total_credentials: Option<u64>,
+ /// Credential protection policy.
+ pub cred_protect: Option<u64>,
+ /// Large blob encryption key.
+ pub large_blob_key: Option<Vec<u8>>,
+}
+
+impl CtapResponse for CredentialManagementResponse {}
+
+#[derive(Debug, PartialEq, Eq, Serialize)]
+pub struct CredentialRpListEntry {
+ /// RP Information
+ pub rp: RelyingParty,
+ /// RP ID SHA-256 hash
+ pub rp_id_hash: RpIdHash,
+ pub credentials: Vec<CredentialListEntry>,
+}
+
+#[derive(Debug, PartialEq, Eq, Serialize)]
+pub struct CredentialListEntry {
+ /// User Information
+ pub user: PublicKeyCredentialUserEntity,
+ /// Credential ID
+ pub credential_id: PublicKeyCredentialDescriptor,
+ /// Public key of the credential.
+ pub public_key: COSEKey,
+ /// Credential protection policy.
+ pub cred_protect: u64,
+ /// Large blob encryption key.
+ pub large_blob_key: Option<Vec<u8>>,
+}
+
+#[derive(Debug, Serialize)]
+pub enum CredentialManagementResult {
+ CredentialList(CredentialList),
+ DeleteSucess,
+ UpdateSuccess,
+}
+
+#[derive(Debug, Default, Serialize)]
+pub struct CredentialList {
+ /// Number of existing discoverable credentials present on the authenticator.
+ pub existing_resident_credentials_count: u64,
+ /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator.
+ pub max_possible_remaining_resident_credentials_count: u64,
+ /// The found credentials
+ pub credential_list: Vec<CredentialRpListEntry>,
+}
+
+impl CredentialList {
+ pub fn new() -> Self {
+ Default::default()
+ }
+}
+
+impl<'de> Deserialize<'de> for CredentialManagementResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct CredentialManagementResponseVisitor;
+
+ impl<'de> Visitor<'de> for CredentialManagementResponseVisitor {
+ type Value = CredentialManagementResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut existing_resident_credentials_count = None; // (0x01) Unsigned Integer Number of existing discoverable credentials present on the authenticator.
+ let mut max_possible_remaining_resident_credentials_count = None; // (0x02) Unsigned Integer Number of maximum possible remaining discoverable credentials which can be created on the authenticator.
+ let mut rp = None; // (0x03) PublicKeyCredentialRpEntity RP Information
+ let mut rp_id_hash = None; // (0x04) Byte String RP ID SHA-256 hash
+ let mut total_rps = None; // (0x05) Unsigned Integer total number of RPs present on the authenticator
+ let mut user = None; // (0x06) PublicKeyCredentialUserEntity User Information
+ let mut credential_id = None; // (0x07) PublicKeyCredentialDescriptor PublicKeyCredentialDescriptor
+ let mut public_key = None; // (0x08) COSE_Key Public key of the credential.
+ let mut total_credentials = None; // (0x09) Unsigned Integer Total number of credentials present on the authenticator for the RP in question
+ let mut cred_protect = None; // (0x0A) Unsigned Integer Credential protection policy.
+ let mut large_blob_key = None; // (0x0B) Byte string Large blob encryption key.
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ 0x01 => {
+ if existing_resident_credentials_count.is_some() {
+ return Err(SerdeError::duplicate_field(
+ "existing_resident_credentials_count",
+ ));
+ }
+ existing_resident_credentials_count = Some(map.next_value()?);
+ }
+ 0x02 => {
+ if max_possible_remaining_resident_credentials_count.is_some() {
+ return Err(SerdeError::duplicate_field(
+ "max_possible_remaining_resident_credentials_count",
+ ));
+ }
+ max_possible_remaining_resident_credentials_count =
+ Some(map.next_value()?);
+ }
+ 0x03 => {
+ if rp.is_some() {
+ return Err(SerdeError::duplicate_field("rp"));
+ }
+ rp = Some(map.next_value()?);
+ }
+ 0x04 => {
+ if rp_id_hash.is_some() {
+ return Err(SerdeError::duplicate_field("rp_id_hash"));
+ }
+ let rp_raw = map.next_value::<ByteBuf>()?;
+ rp_id_hash =
+ Some(RpIdHash::from(rp_raw.as_slice()).map_err(|_| {
+ SerdeError::invalid_length(rp_raw.len(), &"32")
+ })?);
+ }
+ 0x05 => {
+ if total_rps.is_some() {
+ return Err(SerdeError::duplicate_field("total_rps"));
+ }
+ total_rps = Some(map.next_value()?);
+ }
+ 0x06 => {
+ if user.is_some() {
+ return Err(SerdeError::duplicate_field("user"));
+ }
+ user = Some(map.next_value()?);
+ }
+ 0x07 => {
+ if credential_id.is_some() {
+ return Err(SerdeError::duplicate_field("credential_id"));
+ }
+ credential_id = Some(map.next_value()?);
+ }
+ 0x08 => {
+ if public_key.is_some() {
+ return Err(SerdeError::duplicate_field("public_key"));
+ }
+ public_key = Some(map.next_value()?);
+ }
+ 0x09 => {
+ if total_credentials.is_some() {
+ return Err(SerdeError::duplicate_field("total_credentials"));
+ }
+ total_credentials = Some(map.next_value()?);
+ }
+ 0x0A => {
+ if cred_protect.is_some() {
+ return Err(SerdeError::duplicate_field("cred_protect"));
+ }
+ cred_protect = Some(map.next_value()?);
+ }
+ 0x0B => {
+ if large_blob_key.is_some() {
+ return Err(SerdeError::duplicate_field("large_blob_key"));
+ }
+ // Using into_vec, to avoid any copy of large_blob_key
+ large_blob_key = Some(map.next_value::<ByteBuf>()?.into_vec());
+ }
+
+ k => {
+ warn!("ClientPinResponse: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ continue;
+ }
+ }
+ }
+
+ Ok(CredentialManagementResponse {
+ existing_resident_credentials_count,
+ max_possible_remaining_resident_credentials_count,
+ rp,
+ rp_id_hash,
+ total_rps,
+ user,
+ credential_id,
+ public_key,
+ total_credentials,
+ cred_protect,
+ large_blob_key,
+ })
+ }
+ }
+ deserializer.deserialize_bytes(CredentialManagementResponseVisitor)
+ }
+}
+
+impl RequestCtap2 for CredentialManagement {
+ type Output = CredentialManagementResponse;
+
+ fn command(&self) -> Command {
+ if self.use_legacy_preview {
+ Command::CredentialManagementPreview
+ } else {
+ Command::CredentialManagement
+ }
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ let output = to_vec(&self).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: FidoDevice,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if status.is_ok() {
+ if input.len() > 1 {
+ trace!("parsing credential management data: {:#04X?}", &input);
+ let credential_management =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Ok(credential_management)
+ } else {
+ // Some subcommands return only an OK-status without any data
+ Ok(CredentialManagementResponse::default())
+ }
+ } else {
+ let data: Option<Value> = if input.len() > 1 {
+ Some(from_slice(&input[1..]).map_err(CommandError::Deserializing)?)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, data).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: crate::VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ unimplemented!()
+ }
+}
+
+impl PinUvAuthCommand for CredentialManagement {
+ fn get_rp_id(&self) -> Option<&String> {
+ None
+ }
+
+ fn set_pin_uv_auth_param(
+ &mut self,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+ ) -> Result<(), AuthenticatorError> {
+ let mut param = None;
+ if let Some(token) = pin_uv_auth_token {
+ // pinUvAuthParam (0x04): the result of calling
+ // authenticate(pinUvAuthToken, uint8(subCommand) || subCommandParams).
+ let (id, params) = self.subcommand.to_id_and_param();
+ let mut data = vec![id];
+ if params.has_some() {
+ data.extend(to_vec(&params).map_err(CommandError::Serializing)?);
+ }
+ param = Some(token.derive(&data).map_err(CommandError::Crypto)?);
+ }
+ self.pin_uv_auth_param = param;
+ Ok(())
+ }
+
+ fn can_skip_user_verification(
+ &mut self,
+ _info: &crate::AuthenticatorInfo,
+ _uv: UserVerificationRequirement,
+ ) -> bool {
+ // "discouraged" does not exist for AuthenticatorConfig
+ false
+ }
+
+ fn set_uv_option(&mut self, _uv: Option<bool>) {
+ /* No-op */
+ }
+
+ fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> {
+ self.pin_uv_auth_param.as_ref()
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs
new file mode 100644
index 0000000000..54d7d4919f
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs
@@ -0,0 +1,1494 @@
+use super::get_info::AuthenticatorInfo;
+use super::{
+ Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable,
+ StatusCode,
+};
+use crate::consts::{
+ PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN,
+ U2F_REQUEST_USER_PRESENCE,
+};
+use crate::crypto::{COSEKey, CryptoError, PinUvAuthParam, PinUvAuthToken, SharedSecret};
+use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags};
+use crate::ctap2::client_data::ClientDataHash;
+use crate::ctap2::commands::get_next_assertion::GetNextAssertion;
+use crate::ctap2::commands::make_credentials::UserVerification;
+use crate::ctap2::server::{
+ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs,
+ AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity,
+ RelyingParty, RpIdHash, UserVerificationRequirement,
+};
+use crate::ctap2::utils::{read_be_u32, read_byte};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use crate::u2ftypes::CTAP1RequestAPDU;
+use serde::{
+ de::{Error as DesError, MapAccess, Visitor},
+ ser::{Error as SerError, SerializeMap},
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor::{de::from_slice, ser, Value};
+use std::fmt;
+use std::io::Cursor;
+
+#[derive(Clone, Copy, Debug, Serialize)]
+#[cfg_attr(test, derive(Deserialize))]
+pub struct GetAssertionOptions {
+ #[serde(rename = "uv", skip_serializing_if = "Option::is_none")]
+ pub user_verification: Option<bool>,
+ #[serde(rename = "up", skip_serializing_if = "Option::is_none")]
+ pub user_presence: Option<bool>,
+}
+
+impl Default for GetAssertionOptions {
+ fn default() -> Self {
+ Self {
+ user_presence: Some(true),
+ user_verification: None,
+ }
+ }
+}
+
+impl GetAssertionOptions {
+ pub(crate) fn has_some(&self) -> bool {
+ self.user_presence.is_some() || self.user_verification.is_some()
+ }
+}
+
+impl UserVerification for GetAssertionOptions {
+ fn ask_user_verification(&self) -> bool {
+ if let Some(e) = self.user_verification {
+ e
+ } else {
+ false
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct CalculatedHmacSecretExtension {
+ pub public_key: COSEKey,
+ pub salt_enc: Vec<u8>,
+ pub salt_auth: Vec<u8>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct HmacSecretExtension {
+ pub salt1: Vec<u8>,
+ pub salt2: Option<Vec<u8>>,
+ calculated_hmac: Option<CalculatedHmacSecretExtension>,
+}
+
+impl HmacSecretExtension {
+ pub fn new(salt1: Vec<u8>, salt2: Option<Vec<u8>>) -> Self {
+ HmacSecretExtension {
+ salt1,
+ salt2,
+ calculated_hmac: None,
+ }
+ }
+
+ pub fn calculate(&mut self, secret: &SharedSecret) -> Result<(), AuthenticatorError> {
+ if self.salt1.len() < 32 {
+ return Err(CryptoError::WrongSaltLength.into());
+ }
+ let salt_enc = match &self.salt2 {
+ Some(salt2) => {
+ if salt2.len() < 32 {
+ return Err(CryptoError::WrongSaltLength.into());
+ }
+ let salts = [&self.salt1[..32], &salt2[..32]].concat(); // salt1 || salt2
+ secret.encrypt(&salts)
+ }
+ None => secret.encrypt(&self.salt1[..32]),
+ }?;
+ let salt_auth = secret.authenticate(&salt_enc)?;
+ let public_key = secret.client_input().clone();
+ self.calculated_hmac = Some(CalculatedHmacSecretExtension {
+ public_key,
+ salt_enc,
+ salt_auth,
+ });
+
+ Ok(())
+ }
+}
+
+impl Serialize for HmacSecretExtension {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ if let Some(calc) = &self.calculated_hmac {
+ let mut map = serializer.serialize_map(Some(3))?;
+ map.serialize_entry(&1, &calc.public_key)?;
+ map.serialize_entry(&2, serde_bytes::Bytes::new(&calc.salt_enc))?;
+ map.serialize_entry(&3, serde_bytes::Bytes::new(&calc.salt_auth))?;
+ map.end()
+ } else {
+ Err(SerError::custom(
+ "hmac secret has not been calculated before being serialized",
+ ))
+ }
+ }
+}
+
+#[derive(Debug, Default, Clone, Serialize)]
+pub struct GetAssertionExtensions {
+ #[serde(skip_serializing)]
+ pub app_id: Option<String>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<HmacSecretExtension>,
+}
+
+impl From<AuthenticationExtensionsClientInputs> for GetAssertionExtensions {
+ fn from(input: AuthenticationExtensionsClientInputs) -> Self {
+ Self {
+ app_id: input.app_id,
+ ..Default::default()
+ }
+ }
+}
+
+impl GetAssertionExtensions {
+ fn has_content(&self) -> bool {
+ self.hmac_secret.is_some()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct GetAssertion {
+ pub client_data_hash: ClientDataHash,
+ pub rp: RelyingParty,
+ pub allow_list: Vec<PublicKeyCredentialDescriptor>,
+
+ // https://www.w3.org/TR/webauthn/#client-extension-input
+ // The client extension input, which is a value that can be encoded in JSON,
+ // is passed from the WebAuthn Relying Party to the client in the get() or
+ // create() call, while the CBOR authenticator extension input is passed
+ // from the client to the authenticator for authenticator extensions during
+ // the processing of these calls.
+ pub extensions: GetAssertionExtensions,
+ pub options: GetAssertionOptions,
+ pub pin_uv_auth_param: Option<PinUvAuthParam>,
+}
+
+impl GetAssertion {
+ pub fn new(
+ client_data_hash: ClientDataHash,
+ rp: RelyingParty,
+ allow_list: Vec<PublicKeyCredentialDescriptor>,
+ options: GetAssertionOptions,
+ extensions: GetAssertionExtensions,
+ ) -> Self {
+ Self {
+ client_data_hash,
+ rp,
+ allow_list,
+ extensions,
+ options,
+ pin_uv_auth_param: None,
+ }
+ }
+
+ pub fn finalize_result<Dev: FidoDevice>(&self, dev: &Dev, result: &mut GetAssertionResult) {
+ result.attachment = match dev.get_authenticator_info() {
+ Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform,
+ Some(_) => AuthenticatorAttachment::CrossPlatform,
+ None => AuthenticatorAttachment::Unknown,
+ };
+
+ // Handle extensions whose outputs are not encoded in the authenticator data.
+ // 1. appId
+ if let Some(app_id) = &self.extensions.app_id {
+ result.extensions.app_id =
+ Some(result.assertion.auth_data.rp_id_hash == RelyingParty::from(app_id).hash());
+ }
+ }
+}
+
+impl PinUvAuthCommand for GetAssertion {
+ fn set_pin_uv_auth_param(
+ &mut self,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+ ) -> Result<(), AuthenticatorError> {
+ let mut param = None;
+ if let Some(token) = pin_uv_auth_token {
+ param = Some(
+ token
+ .derive(self.client_data_hash.as_ref())
+ .map_err(CommandError::Crypto)?,
+ );
+ }
+ self.pin_uv_auth_param = param;
+ Ok(())
+ }
+
+ fn set_uv_option(&mut self, uv: Option<bool>) {
+ self.options.user_verification = uv;
+ }
+
+ fn get_rp_id(&self) -> Option<&String> {
+ Some(&self.rp.id)
+ }
+
+ fn can_skip_user_verification(
+ &mut self,
+ info: &AuthenticatorInfo,
+ uv_req: UserVerificationRequirement,
+ ) -> bool {
+ let supports_uv = info.options.user_verification == Some(true);
+ let pin_configured = info.options.client_pin == Some(true);
+ let device_protected = supports_uv || pin_configured;
+ let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged;
+ let always_uv = info.options.always_uv == Some(true);
+
+ !always_uv && (!device_protected || uv_discouraged)
+ }
+
+ fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> {
+ self.pin_uv_auth_param.as_ref()
+ }
+}
+
+impl Serialize for GetAssertion {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 2;
+ if !self.allow_list.is_empty() {
+ map_len += 1;
+ }
+ if self.extensions.has_content() {
+ map_len += 1;
+ }
+ if self.options.has_some() {
+ map_len += 1;
+ }
+ if self.pin_uv_auth_param.is_some() {
+ map_len += 2;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ map.serialize_entry(&1, &self.rp.id)?;
+ map.serialize_entry(&2, &self.client_data_hash)?;
+ if !self.allow_list.is_empty() {
+ map.serialize_entry(&3, &self.allow_list)?;
+ }
+ if self.extensions.has_content() {
+ map.serialize_entry(&4, &self.extensions)?;
+ }
+ if self.options.has_some() {
+ map.serialize_entry(&5, &self.options)?;
+ }
+ if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param {
+ map.serialize_entry(&6, &pin_uv_auth_param)?;
+ map.serialize_entry(&7, &pin_uv_auth_param.pin_protocol.id())?;
+ }
+ map.end()
+ }
+}
+
+type GetAssertionOutput = Vec<GetAssertionResult>;
+impl CtapResponse for GetAssertionOutput {}
+
+impl RequestCtap1 for GetAssertion {
+ type Output = Vec<GetAssertionResult>;
+ type AdditionalInfo = PublicKeyCredentialDescriptor;
+
+ fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError> {
+ // Pre-flighting should reduce the list to exactly one entry
+ let key_handle = match &self.allow_list[..] {
+ [key_handle] => key_handle,
+ [] => {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NoCredentials,
+ None,
+ )));
+ }
+ _ => {
+ return Err(HIDError::UnsupportedCommand);
+ }
+ };
+
+ debug!("sending key_handle = {:?}", key_handle);
+
+ let flags = if self.options.user_presence.unwrap_or(true) {
+ U2F_REQUEST_USER_PRESENCE
+ } else {
+ U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN
+ };
+ let mut auth_data =
+ Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.id.len());
+
+ auth_data.extend_from_slice(self.client_data_hash.as_ref());
+ auth_data.extend_from_slice(self.rp.hash().as_ref());
+ auth_data.extend_from_slice(&[key_handle.id.len() as u8]);
+ auth_data.extend_from_slice(key_handle.id.as_ref());
+
+ let cmd = U2F_AUTHENTICATE;
+ let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?;
+ Ok((apdu, key_handle.clone()))
+ }
+
+ fn handle_response_ctap1<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ add_info: &PublicKeyCredentialDescriptor,
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ if Err(ApduErrorStatus::ConditionsNotSatisfied) == status {
+ return Err(Retryable::Retry);
+ }
+ if let Err(err) = status {
+ return Err(Retryable::Error(HIDError::ApduStatus(err)));
+ }
+
+ let mut result = GetAssertionResult::from_ctap1(input, &self.rp.hash(), add_info)
+ .map_err(|e| Retryable::Error(HIDError::Command(e)))?;
+ self.finalize_result(dev, &mut result);
+ // Although there's only one result, we return a vector for consistency with CTAP2.
+ Ok(vec![result])
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ let mut results = dev.get_assertion(self)?;
+ for result in results.iter_mut() {
+ self.finalize_result(dev, result);
+ }
+ Ok(results)
+ }
+}
+
+impl RequestCtap2 for GetAssertion {
+ type Output = Vec<GetAssertionResult>;
+
+ fn command(&self) -> Command {
+ Command::GetAssertion
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?)
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!(
+ "response status code: {:?}, rest: {:?}",
+ status,
+ &input[1..]
+ );
+ if input.len() == 1 {
+ if status.is_ok() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+ return Err(CommandError::StatusCode(status, None).into());
+ }
+
+ if status.is_ok() {
+ let assertion: GetAssertionResponse =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ let number_of_credentials = assertion.number_of_credentials.unwrap_or(1);
+
+ let mut results = Vec::with_capacity(number_of_credentials);
+ results.push(GetAssertionResult {
+ assertion: assertion.into(),
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ });
+
+ let msg = GetNextAssertion;
+ // We already have one, so skipping 0
+ for _ in 1..number_of_credentials {
+ let assertion = dev.send_cbor(&msg)?;
+ results.push(GetAssertionResult {
+ assertion: assertion.into(),
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ });
+ }
+
+ for result in results.iter_mut() {
+ self.finalize_result(dev, result);
+ }
+ Ok(results)
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(CommandError::StatusCode(status, Some(data)).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ let mut results = dev.get_assertion(self)?;
+ for result in results.iter_mut() {
+ self.finalize_result(dev, result);
+ }
+ Ok(results)
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Assertion {
+ pub credentials: Option<PublicKeyCredentialDescriptor>, /* Was optional in CTAP2.0, is
+ * mandatory in CTAP2.1 */
+ pub auth_data: AuthenticatorData,
+ pub signature: Vec<u8>,
+ pub user: Option<PublicKeyCredentialUserEntity>,
+}
+
+impl From<GetAssertionResponse> for Assertion {
+ fn from(r: GetAssertionResponse) -> Self {
+ Assertion {
+ credentials: r.credentials,
+ auth_data: r.auth_data,
+ signature: r.signature,
+ user: r.user,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct GetAssertionResult {
+ pub assertion: Assertion,
+ pub attachment: AuthenticatorAttachment,
+ pub extensions: AuthenticationExtensionsClientOutputs,
+}
+
+impl GetAssertionResult {
+ pub fn from_ctap1(
+ input: &[u8],
+ rp_id_hash: &RpIdHash,
+ key_handle: &PublicKeyCredentialDescriptor,
+ ) -> Result<GetAssertionResult, CommandError> {
+ let mut data = Cursor::new(input);
+ let user_presence = read_byte(&mut data).map_err(CommandError::Deserializing)?;
+ let counter = read_be_u32(&mut data).map_err(CommandError::Deserializing)?;
+ // Remaining data is signature (Note: `data.remaining_slice()` is not yet stabilized)
+ let signature = Vec::from(&data.get_ref()[data.position() as usize..]);
+
+ // Step 5 of Section 10.3 of CTAP2.1: "Copy bits 0 (the UP bit) and bit 1 from the
+ // CTAP2/U2F response user presence byte to bits 0 and 1 of the CTAP2 flags, respectively.
+ // Set all other bits of flags to zero."
+ let flag_mask = AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::RESERVED_1;
+ let flags = flag_mask & AuthenticatorDataFlags::from_bits_truncate(user_presence);
+ let auth_data = AuthenticatorData {
+ rp_id_hash: rp_id_hash.clone(),
+ flags,
+ counter,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+ let assertion = Assertion {
+ credentials: Some(key_handle.clone()),
+ signature,
+ user: None,
+ auth_data,
+ };
+
+ Ok(GetAssertionResult {
+ assertion,
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ })
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct GetAssertionResponse {
+ pub credentials: Option<PublicKeyCredentialDescriptor>,
+ pub auth_data: AuthenticatorData,
+ pub signature: Vec<u8>,
+ pub user: Option<PublicKeyCredentialUserEntity>,
+ pub number_of_credentials: Option<usize>,
+}
+
+impl CtapResponse for GetAssertionResponse {}
+
+impl<'de> Deserialize<'de> for GetAssertionResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct GetAssertionResponseVisitor;
+
+ impl<'de> Visitor<'de> for GetAssertionResponseVisitor {
+ type Value = GetAssertionResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut credentials = None;
+ let mut auth_data = None;
+ let mut signature = None;
+ let mut user = None;
+ let mut number_of_credentials = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ 1 => {
+ if credentials.is_some() {
+ return Err(M::Error::duplicate_field("credentials"));
+ }
+ credentials = Some(map.next_value()?);
+ }
+ 2 => {
+ if auth_data.is_some() {
+ return Err(M::Error::duplicate_field("auth_data"));
+ }
+ auth_data = Some(map.next_value()?);
+ }
+ 3 => {
+ if signature.is_some() {
+ return Err(M::Error::duplicate_field("signature"));
+ }
+ let signature_bytes: ByteBuf = map.next_value()?;
+ let signature_bytes: Vec<u8> = signature_bytes.into_vec();
+ signature = Some(signature_bytes);
+ }
+ 4 => {
+ if user.is_some() {
+ return Err(M::Error::duplicate_field("user"));
+ }
+ user = map.next_value()?;
+ }
+ 5 => {
+ if number_of_credentials.is_some() {
+ return Err(M::Error::duplicate_field("number_of_credentials"));
+ }
+ number_of_credentials = Some(map.next_value()?);
+ }
+ k => return Err(M::Error::custom(format!("unexpected key: {k:?}"))),
+ }
+ }
+
+ let auth_data = auth_data.ok_or_else(|| M::Error::missing_field("auth_data"))?;
+ let signature = signature.ok_or_else(|| M::Error::missing_field("signature"))?;
+
+ Ok(GetAssertionResponse {
+ credentials,
+ auth_data,
+ signature,
+ user,
+ number_of_credentials,
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(GetAssertionResponseVisitor)
+ }
+}
+
+#[cfg(test)]
+pub mod test {
+ use super::{
+ Assertion, CommandError, GetAssertion, GetAssertionOptions, GetAssertionResult, HIDError,
+ StatusCode,
+ };
+ use crate::consts::{
+ Capability, HIDCmd, SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, U2F_CHECK_IS_REGISTERED,
+ U2F_REQUEST_USER_PRESENCE,
+ };
+ use crate::ctap2::attestation::{AAGuid, AuthenticatorData, AuthenticatorDataFlags};
+ use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType};
+ use crate::ctap2::commands::get_info::tests::AAGUID_RAW;
+ use crate::ctap2::commands::get_info::{
+ AuthenticatorInfo, AuthenticatorOptions, AuthenticatorVersion,
+ };
+ use crate::ctap2::commands::RequestCtap1;
+ use crate::ctap2::preflight::{
+ do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2,
+ };
+ use crate::ctap2::server::{
+ AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity,
+ RelyingParty, RpIdHash, Transport,
+ };
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol};
+ use crate::u2ftypes::U2FDeviceInfo;
+ use rand::{thread_rng, RngCore};
+
+ #[test]
+ fn test_get_assertion_ctap2() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ };
+ let assertion = GetAssertion::new(
+ client_data.hash().expect("failed to serialize client data"),
+ RelyingParty::from("example.com"),
+ vec![PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35,
+ 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D,
+ 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5,
+ 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5,
+ 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38,
+ ],
+ transports: vec![Transport::USB],
+ }],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ );
+ let mut device = Device::new("commands/get_assertion").unwrap();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]);
+ msg.extend(vec![0x2]); // u2f command
+ msg.extend(vec![
+ 0xa4, // map(4)
+ 0x1, // rpid
+ 0x6b, // text(11)
+ 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com
+ 0x2, // clientDataHash
+ 0x58, 0x20, //bytes(32)
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32,
+ 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87,
+ 0x54, 0xc3, 0x2d, 0x80, // hash
+ 0x3, //allowList
+ 0x81, // array(1)
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // id
+ 0x58, // bytes(
+ ]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend([0x0]); //SEQ
+ msg.extend([0x40]); // 64)
+ msg.extend(&assertion.allow_list[0].id[..58]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend([0x1]); //SEQ
+ msg.extend(&assertion.allow_list[0].id[58..64]);
+ msg.extend(vec![
+ 0x64, // text(4),
+ 0x74, 0x79, 0x70, 0x65, // type
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key
+ 0x5, // options
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x75, 0x70, // up
+ 0xf5, // true
+ ]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let mut msg = cid.to_vec();
+ msg.extend([HIDCmd::Cbor.into(), 0x1, 0x2a]); // cmd + bcnt
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[..57]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x0]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[57..116]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x1]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[116..175]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x2]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[175..234]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x3]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[234..293]);
+ device.add_read(&msg, 0);
+ let mut msg = cid.to_vec();
+ msg.extend([0x4]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[293..]);
+ device.add_read(&msg, 0);
+
+ // Check if response is correct
+ let expected_auth_data = AuthenticatorData {
+ rp_id_hash: RpIdHash([
+ 0x62, 0x5d, 0xda, 0xdf, 0x74, 0x3f, 0x57, 0x27, 0xe6, 0x6b, 0xba, 0x8c, 0x2e, 0x38,
+ 0x79, 0x22, 0xd1, 0xaf, 0x43, 0xc5, 0x03, 0xd9, 0x11, 0x4a, 0x8f, 0xba, 0x10, 0x4d,
+ 0x84, 0xd0, 0x2b, 0xfa,
+ ]),
+ flags: AuthenticatorDataFlags::USER_PRESENT,
+ counter: 0x11,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+
+ let expected_assertion = Assertion {
+ credentials: Some(PublicKeyCredentialDescriptor {
+ id: vec![
+ 242, 32, 6, 222, 79, 144, 90, 246, 138, 67, 148, 47, 2, 79, 42, 94, 206, 96,
+ 61, 156, 109, 75, 61, 248, 190, 8, 237, 1, 252, 68, 38, 70, 208, 52, 133, 138,
+ 199, 91, 237, 63, 213, 128, 191, 152, 8, 217, 79, 203, 238, 130, 185, 178, 239,
+ 102, 119, 175, 10, 220, 195, 88, 82, 234, 107, 158,
+ ],
+ transports: vec![],
+ }),
+ signature: vec![
+ 0x30, 0x45, 0x02, 0x20, 0x4a, 0x5a, 0x9d, 0xd3, 0x92, 0x98, 0x14, 0x9d, 0x90, 0x47,
+ 0x69, 0xb5, 0x1a, 0x45, 0x14, 0x33, 0x00, 0x6f, 0x18, 0x2a, 0x34, 0xfb, 0xdf, 0x66,
+ 0xde, 0x5f, 0xc7, 0x17, 0xd7, 0x5f, 0xb3, 0x50, 0x02, 0x21, 0x00, 0xa4, 0x6b, 0x8e,
+ 0xa3, 0xc3, 0xb9, 0x33, 0x82, 0x1c, 0x6e, 0x7f, 0x5e, 0xf9, 0xda, 0xae, 0x94, 0xab,
+ 0x47, 0xf1, 0x8d, 0xb4, 0x74, 0xc7, 0x47, 0x90, 0xea, 0xab, 0xb1, 0x44, 0x11, 0xe7,
+ 0xa0,
+ ],
+ user: Some(PublicKeyCredentialUserEntity {
+ id: vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02,
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02,
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82,
+ ],
+ name: Some("johnpsmith@example.com".to_string()),
+ display_name: Some("John P. Smith".to_string()),
+ }),
+ auth_data: expected_auth_data,
+ };
+
+ let expected = vec![GetAssertionResult {
+ assertion: expected_assertion,
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ }];
+ let response = device.send_cbor(&assertion).unwrap();
+ assert_eq!(response, expected);
+ }
+
+ fn fill_device_ctap1(device: &mut Device, cid: [u8; 4], flags: u8, answer_status: [u8; 2]) {
+ // ctap2 request
+ let mut msg = cid.to_vec();
+ msg.extend([HIDCmd::Msg.into(), 0x00, 0x8A]); // cmd + bcnt
+ msg.extend([0x00, 0x2]); // U2F_AUTHENTICATE
+ msg.extend([flags]);
+ msg.extend([0x00, 0x00, 0x00]);
+ msg.extend([0x81]); // Data len - 7
+ msg.extend(CLIENT_DATA_HASH);
+ msg.extend(&RELYING_PARTY_HASH[..18]);
+ device.add_write(&msg, 0);
+
+ // Continuation package
+ let mut msg = cid.to_vec();
+ msg.extend(vec![0x00]); // SEQ
+ msg.extend(&RELYING_PARTY_HASH[18..]);
+ msg.extend([KEY_HANDLE.len() as u8]);
+ msg.extend(&KEY_HANDLE[..44]);
+ device.add_write(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![0x01]); // SEQ
+ msg.extend(&KEY_HANDLE[44..]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let mut msg = cid.to_vec();
+ msg.extend([HIDCmd::Msg.into(), 0x0, 0x4D]); // cmd + bcnt
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[0..57]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x0]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[57..]);
+ msg.extend(answer_status);
+ device.add_read(&msg, 0);
+ }
+
+ #[test]
+ fn test_get_assertion_ctap1() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ };
+ let allowed_key = PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF,
+ 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85,
+ 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78,
+ 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC,
+ 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38,
+ ],
+ transports: vec![Transport::USB],
+ };
+ let mut assertion = GetAssertion::new(
+ client_data.hash().expect("failed to serialize client data"),
+ RelyingParty::from("example.com"),
+ vec![allowed_key.clone()],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ );
+ let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
+ // channel id
+ device.downgrade_to_ctap1();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP1);
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ device.set_cid(cid);
+
+ // ctap1 request
+ fill_device_ctap1(
+ &mut device,
+ cid,
+ U2F_CHECK_IS_REGISTERED,
+ SW_CONDITIONS_NOT_SATISFIED,
+ );
+ let key_handle = do_credential_list_filtering_ctap1(
+ &mut device,
+ &assertion.allow_list,
+ &assertion.rp,
+ &assertion.client_data_hash,
+ )
+ .expect("Did not find a key_handle, even though it should have");
+ assertion.allow_list = vec![key_handle];
+ let (ctap1_request, key_handle) = assertion.ctap1_format().unwrap();
+ assert_eq!(key_handle, allowed_key);
+ // Check if the request is going to be correct
+ assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1);
+
+ // Now do it again, but parse the actual response
+ // Pre-flighting is not done automatically
+ fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR);
+
+ let response = device.send_ctap1(&assertion).unwrap();
+
+ // Check if response is correct
+ let expected_auth_data = AuthenticatorData {
+ rp_id_hash: RpIdHash(RELYING_PARTY_HASH),
+ flags: AuthenticatorDataFlags::USER_PRESENT,
+ counter: 0x3B,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+
+ let expected_assertion = Assertion {
+ credentials: Some(allowed_key),
+ signature: vec![
+ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0,
+ 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83,
+ 0x5F, 0x45, 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47,
+ 0x87, 0x7F, 0x85, 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E,
+ 0x36, 0x39, 0xE7, 0x71, 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E,
+ ],
+ user: None,
+ auth_data: expected_auth_data,
+ };
+
+ let expected = vec![GetAssertionResult {
+ assertion: expected_assertion,
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ }];
+ assert_eq!(response, expected);
+ }
+
+ #[test]
+ fn test_get_assertion_ctap1_long_keys() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ };
+
+ let too_long_key_handle = PublicKeyCredentialDescriptor {
+ id: vec![0; 1000],
+ transports: vec![Transport::USB],
+ };
+ let mut assertion = GetAssertion::new(
+ client_data.hash().expect("failed to serialize client data"),
+ RelyingParty::from("example.com"),
+ vec![too_long_key_handle.clone()],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ );
+
+ let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
+ // channel id
+ device.downgrade_to_ctap1();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP1);
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ device.set_cid(cid);
+
+ assert_matches!(
+ do_credential_list_filtering_ctap1(
+ &mut device,
+ &assertion.allow_list,
+ &assertion.rp,
+ &assertion.client_data_hash,
+ ),
+ None
+ );
+ assertion.allow_list = vec![];
+ // It should also fail when trying to format
+ assert_matches!(
+ assertion.ctap1_format(),
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NoCredentials,
+ ..
+ )))
+ );
+
+ // Test also multiple too long keys and an empty allow list
+ for allow_list in [vec![], vec![too_long_key_handle.clone(); 5]] {
+ assertion.allow_list = allow_list;
+
+ assert_matches!(
+ do_credential_list_filtering_ctap1(
+ &mut device,
+ &assertion.allow_list,
+ &assertion.rp,
+ &assertion.client_data_hash,
+ ),
+ None
+ );
+ }
+
+ let ok_key_handle = PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF,
+ 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85,
+ 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78,
+ 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC,
+ 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38,
+ ],
+ transports: vec![Transport::USB],
+ };
+ assertion.allow_list = vec![
+ too_long_key_handle.clone(),
+ too_long_key_handle.clone(),
+ too_long_key_handle.clone(),
+ ok_key_handle.clone(),
+ too_long_key_handle,
+ ];
+
+ // ctap1 request
+ fill_device_ctap1(
+ &mut device,
+ cid,
+ U2F_CHECK_IS_REGISTERED,
+ SW_CONDITIONS_NOT_SATISFIED,
+ );
+ let key_handle = do_credential_list_filtering_ctap1(
+ &mut device,
+ &assertion.allow_list,
+ &assertion.rp,
+ &assertion.client_data_hash,
+ )
+ .expect("Did not find a key_handle, even though it should have");
+ assertion.allow_list = vec![key_handle];
+ let (ctap1_request, key_handle) = assertion.ctap1_format().unwrap();
+ assert_eq!(key_handle, ok_key_handle);
+ // Check if the request is going to be correct
+ assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1);
+
+ // Now do it again, but parse the actual response
+ // Pre-flighting is not done automatically
+ fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR);
+
+ let response = device.send_ctap1(&assertion).unwrap();
+
+ // Check if response is correct
+ let expected_auth_data = AuthenticatorData {
+ rp_id_hash: RpIdHash(RELYING_PARTY_HASH),
+ flags: AuthenticatorDataFlags::USER_PRESENT,
+ counter: 0x3B,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+
+ let expected_assertion = Assertion {
+ credentials: Some(ok_key_handle),
+ signature: vec![
+ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0,
+ 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83,
+ 0x5F, 0x45, 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47,
+ 0x87, 0x7F, 0x85, 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E,
+ 0x36, 0x39, 0xE7, 0x71, 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E,
+ ],
+ user: None,
+ auth_data: expected_auth_data,
+ };
+
+ let expected = vec![GetAssertionResult {
+ assertion: expected_assertion,
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ }];
+ assert_eq!(response, expected);
+ }
+
+ #[test]
+ fn test_get_assertion_ctap2_pre_flight() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ };
+ let assertion = GetAssertion::new(
+ client_data.hash().expect("failed to serialize client data"),
+ RelyingParty::from("example.com"),
+ vec![
+ // This should never be tested, because it gets pre-filtered, since it is too long
+ // (see max_credential_id_length)
+ PublicKeyCredentialDescriptor {
+ id: vec![0x10; 100],
+ transports: vec![Transport::USB],
+ },
+ // One we test and skip
+ PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11,
+ ],
+ transports: vec![Transport::USB],
+ },
+ // This one is the 'right' one
+ PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26,
+ 0x35, 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3,
+ 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94,
+ 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64,
+ 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38,
+ ],
+ transports: vec![Transport::USB],
+ },
+ // We should never test this one
+ PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22,
+ ],
+ transports: vec![Transport::USB],
+ },
+ ],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ );
+ let mut device = Device::new("commands/get_assertion").unwrap();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+ device.set_cid(cid);
+ device.set_device_info(U2FDeviceInfo {
+ vendor_name: Vec::new(),
+ device_name: Vec::new(),
+ version_interface: 0x02,
+ version_major: 0x04,
+ version_minor: 0x01,
+ version_build: 0x08,
+ cap_flags: Capability::WINK | Capability::CBOR,
+ });
+ device.set_authenticator_info(AuthenticatorInfo {
+ versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
+ extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
+ aaguid: AAGuid(AAGUID_RAW),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(false),
+ user_presence: true,
+ user_verification: None,
+ ..Default::default()
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: Some(vec![1]),
+ max_credential_count_in_list: None,
+ max_credential_id_length: Some(80),
+ transports: None,
+ algorithms: None,
+ max_ser_large_blob_array: None,
+ force_pin_change: None,
+ min_pin_length: None,
+ firmware_version: None,
+ max_cred_blob_length: None,
+ max_rpids_for_set_min_pin_length: None,
+ preferred_platform_uv_attempts: None,
+ uv_modality: None,
+ certifications: None,
+ remaining_discoverable_credentials: None,
+ vendor_prototype_config_commands: None,
+ });
+
+ // Sending first GetAssertion with first allow_list-entry, that will return an error
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]);
+ msg.extend(vec![0x2]); // u2f command
+ msg.extend(vec![
+ 0xa4, // map(4)
+ 0x1, // rpid
+ 0x6b, // text(11)
+ 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com
+ 0x2, // clientDataHash
+ 0x58, 0x20, //bytes(32)
+ 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
+ 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
+ 0x78, 0x52, 0xb8, 0x55, // empty hash
+ 0x3, //allowList
+ 0x81, // array(1)
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // id
+ 0x58, // bytes(
+ ]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend([0x0]); //SEQ
+ msg.extend([0x40]); // 64)
+ msg.extend(&assertion.allow_list[1].id[..58]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend([0x1]); //SEQ
+ msg.extend(&assertion.allow_list[1].id[58..64]);
+ msg.extend(vec![
+ 0x64, // text(4),
+ 0x74, 0x79, 0x70, 0x65, // type
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key
+ 0x5, // options
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x75, 0x70, // up
+ 0xf4, // false
+ ]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let len = 0x1;
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt
+ msg.push(0x2e); // Status code: NoCredentials
+ device.add_read(&msg, 0);
+
+ // Sending second GetAssertion with first allow_list-entry, that will return a success
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]);
+ msg.extend(vec![0x2]); // u2f command
+ msg.extend(vec![
+ 0xa4, // map(4)
+ 0x1, // rpid
+ 0x6b, // text(11)
+ 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com
+ 0x2, // clientDataHash
+ 0x58, 0x20, //bytes(32)
+ 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
+ 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
+ 0x78, 0x52, 0xb8, 0x55, // empty hash
+ 0x3, //allowList
+ 0x81, // array(1)
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // id
+ 0x58, // bytes(
+ ]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend([0x0]); //SEQ
+ msg.extend([0x40]); // 64)
+ msg.extend(&assertion.allow_list[2].id[..58]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend([0x1]); //SEQ
+ msg.extend(&assertion.allow_list[2].id[58..64]);
+ msg.extend(vec![
+ 0x64, // text(4),
+ 0x74, 0x79, 0x70, 0x65, // type
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key
+ 0x5, // options
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x75, 0x70, // up
+ 0xf4, // false
+ ]);
+ device.add_write(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([HIDCmd::Cbor.into(), 0x1, 0x2a]); // cmd + bcnt
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[..57]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x0]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[57..116]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x1]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[116..175]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x2]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[175..234]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend([0x3]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[234..293]);
+ device.add_read(&msg, 0);
+ let mut msg = cid.to_vec();
+ msg.extend([0x4]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[293..]);
+ device.add_read(&msg, 0);
+
+ assert_matches!(
+ do_credential_list_filtering_ctap2(
+ &mut device,
+ &assertion.allow_list,
+ &assertion.rp,
+ None,
+ ),
+ Ok(..)
+ );
+ }
+
+ #[test]
+ fn test_get_assertion_ctap1_flags() {
+ // Ensure that only the two low bits of flags are preserved when repackaging a
+ // CTAP1 response.
+ let mut sample = GET_ASSERTION_SAMPLE_RESPONSE_CTAP1.to_vec();
+ sample[0] = 0xff; // Set all 8 flag bits before repackaging
+ let add_info = PublicKeyCredentialDescriptor {
+ id: vec![],
+ transports: vec![],
+ };
+ let rp_hash = RpIdHash([0u8; 32]);
+ let resp = GetAssertionResult::from_ctap1(&sample, &rp_hash, &add_info)
+ .expect("could not handle response");
+ assert_eq!(
+ resp.assertion.auth_data.flags,
+ AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::RESERVED_1
+ );
+ }
+
+ // Manually assembled according to https://www.w3.org/TR/webauthn-2/#clientdatajson-serialization
+ const CLIENT_DATA_VEC: [u8; 140] = [
+ 0x7b, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, // {"type":
+ 0x22, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74,
+ 0x65, 0x22, // "webauthn.create"
+ 0x2c, 0x22, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x22,
+ 0x3a, // (,"challenge":
+ 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // challenge in base64
+ 0x2c, 0x22, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0x3a, // ,"origin":
+ 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0x22, // "example.com"
+ 0x2c, 0x22, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22,
+ 0x3a, // ,"crossOrigin":
+ 0x66, 0x61, 0x6c, 0x73, 0x65, // false
+ 0x2c, 0x22, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22,
+ 0x3a, // ,"tokenBinding":
+ 0x7b, 0x22, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3a, // {"status":
+ 0x22, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x22, // "present"
+ 0x2c, 0x22, 0x69, 0x64, 0x22, 0x3a, // ,"id":
+ 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // "AAECAw"
+ 0x7d, // }
+ 0x7d, // }
+ ];
+
+ const CLIENT_DATA_HASH: [u8; 32] = [
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ ];
+
+ const RELYING_PARTY_HASH: [u8; 32] = [
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47,
+ ];
+ const KEY_HANDLE: [u8; 64] = [
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38,
+ ];
+
+ const GET_ASSERTION_SAMPLE_REQUEST_CTAP1: [u8; 138] = [
+ // CBOR Header
+ 0x0, // CLA
+ 0x2, // INS U2F_Authenticate
+ 0x3, // P1 Flags (user presence)
+ 0x0, // P2
+ 0x0, 0x0, 0x81, // Lc
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash)
+ // clientDataHash:
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ // rpIdHash:
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47, // ..
+ // Key Handle Length (1 Byte):
+ 0x40, // ..
+ // Key Handle (Key Handle Length Bytes):
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38, // ..
+ // Le (Ne=65536):
+ 0x0, 0x0,
+ ];
+
+ const GET_ASSERTION_SAMPLE_REQUEST_CTAP2: [u8; 138] = [
+ // CBOR Header
+ 0x0, // leading zero
+ 0x2, // CMD U2F_Authenticate
+ 0x3, // Flags (user presence)
+ 0x0, 0x0, // zero bits
+ 0x0, 0x81, // size
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash)
+ // clientDataHash:
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32, 0x64,
+ 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87, 0x54, 0xc3,
+ 0x2d, 0x80, // hash
+ // rpIdHash:
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47, // ..
+ // Key Handle Length (1 Byte):
+ 0x40, // ..
+ // Key Handle (Key Handle Length Bytes):
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38, 0x0, 0x0, // 2 trailing zeros from protocol
+ ];
+
+ const GET_ASSERTION_SAMPLE_RESPONSE_CTAP1: [u8; 75] = [
+ 0x01, // User Presence (1 Byte)
+ 0x00, 0x00, 0x00, 0x3B, // Sign Count (4 Bytes)
+ // Signature (variable Length)
+ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, 0x03,
+ 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, 0x5F, 0x45,
+ 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47, 0x87, 0x7F, 0x85,
+ 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E, 0x36, 0x39, 0xE7, 0x71,
+ 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E,
+ ];
+
+ const GET_ASSERTION_SAMPLE_RESPONSE_CTAP2: [u8; 298] = [
+ 0x00, // status == success
+ 0xA5, // map(5)
+ 0x01, // unsigned(1)
+ 0xA2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x40, // bytes(0x64, ) credential_id
+ 0xF2, 0x20, 0x06, 0xDE, 0x4F, 0x90, 0x5A, 0xF6, 0x8A, 0x43, 0x94, 0x2F, 0x02, 0x4F, 0x2A,
+ 0x5E, 0xCE, 0x60, 0x3D, 0x9C, 0x6D, 0x4B, 0x3D, 0xF8, 0xBE, 0x08, 0xED, 0x01, 0xFC, 0x44,
+ 0x26, 0x46, 0xD0, 0x34, 0x85, 0x8A, 0xC7, 0x5B, 0xED, 0x3F, 0xD5, 0x80, 0xBF, 0x98, 0x08,
+ 0xD9, 0x4F, 0xCB, 0xEE, 0x82, 0xB9, 0xB2, 0xEF, 0x66, 0x77, 0xAF, 0x0A, 0xDC, 0xC3, 0x58,
+ 0x52, 0xEA, 0x6B, 0x9E, // end: credential_id
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6A, // text(0x10, )
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0x02, // unsigned(2)
+ 0x58, 0x25, // bytes(0x37, ) auth_data
+ 0x62, 0x5D, 0xDA, 0xDF, 0x74, 0x3F, 0x57, 0x27, 0xE6, 0x6B, 0xBA, 0x8C, 0x2E, 0x38, 0x79,
+ 0x22, 0xD1, 0xAF, 0x43, 0xC5, 0x03, 0xD9, 0x11, 0x4A, 0x8F, 0xBA, 0x10, 0x4D, 0x84, 0xD0,
+ 0x2B, 0xFA, 0x01, 0x00, 0x00, 0x00, 0x11, // end: auth_data
+ 0x03, // unsigned(3)
+ 0x58, 0x47, // bytes(0x71, ) signature
+ 0x30, 0x45, 0x02, 0x20, 0x4A, 0x5A, 0x9D, 0xD3, 0x92, 0x98, 0x14, 0x9D, 0x90, 0x47, 0x69,
+ 0xB5, 0x1A, 0x45, 0x14, 0x33, 0x00, 0x6F, 0x18, 0x2A, 0x34, 0xFB, 0xDF, 0x66, 0xDE, 0x5F,
+ 0xC7, 0x17, 0xD7, 0x5F, 0xB3, 0x50, 0x02, 0x21, 0x00, 0xA4, 0x6B, 0x8E, 0xA3, 0xC3, 0xB9,
+ 0x33, 0x82, 0x1C, 0x6E, 0x7F, 0x5E, 0xF9, 0xDA, 0xAE, 0x94, 0xAB, 0x47, 0xF1, 0x8D, 0xB4,
+ 0x74, 0xC7, 0x47, 0x90, 0xEA, 0xAB, 0xB1, 0x44, 0x11, 0xE7, 0xA0, // end: signature
+ 0x04, // unsigned(4)
+ 0xA3, // map(3)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(0x32, ) user_id
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
+ 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93,
+ 0x30, 0x82, // end: user_id
+ 0x64, // text(4)
+ 0x6E, 0x61, 0x6D, 0x65, // "name"
+ 0x76, // text(0x22, )
+ 0x6A, 0x6F, 0x68, 0x6E, 0x70, 0x73, 0x6D, 0x69, 0x74, 0x68, 0x40, 0x65, 0x78, 0x61, 0x6D,
+ 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // "johnpsmith@example.com"
+ 0x6B, // text(0x11, )
+ 0x64, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x4E, 0x61, 0x6D, 0x65, // "displayName"
+ 0x6D, // text(0x13, )
+ 0x4A, 0x6F, 0x68, 0x6E, 0x20, 0x50, 0x2E, 0x20, 0x53, 0x6D, 0x69, 0x74,
+ 0x68, // "John P. Smith"
+ 0x05, // unsigned(5)
+ 0x01, // unsigned(1)
+ ];
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_info.rs b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs
new file mode 100644
index 0000000000..f676605a0b
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs
@@ -0,0 +1,1054 @@
+use super::{Command, CommandError, CtapResponse, RequestCtap2, StatusCode};
+use crate::ctap2::attestation::AAGuid;
+use crate::ctap2::server::PublicKeyCredentialParameters;
+use crate::transport::errors::HIDError;
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use serde::{
+ de::{Error as SError, IgnoredAny, MapAccess, Visitor},
+ Deserialize, Deserializer, Serialize,
+};
+use serde_cbor::{de::from_slice, Value};
+use std::collections::BTreeMap;
+use std::fmt;
+
+#[derive(Debug, Default)]
+pub struct GetInfo {}
+
+impl RequestCtap2 for GetInfo {
+ type Output = AuthenticatorInfo;
+
+ fn command(&self) -> Command {
+ Command::GetInfo
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if input.len() > 1 {
+ if status.is_ok() {
+ trace!("parsing authenticator info data: {:#04X?}", &input);
+ let authenticator_info =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Ok(authenticator_info)
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(CommandError::StatusCode(status, Some(data)).into())
+ }
+ } else {
+ Err(CommandError::InputTooSmall.into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ dev.get_info()
+ }
+}
+
+fn true_val() -> bool {
+ true
+}
+
+#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)]
+pub struct AuthenticatorOptions {
+ /// Indicates that the device is attached to the client and therefore can’t
+ /// be removed and used on another client.
+ #[serde(rename = "plat", default)]
+ pub platform_device: bool,
+ /// Indicates that the device is capable of storing keys on the device
+ /// itself and therefore can satisfy the authenticatorGetAssertion request
+ /// with allowList parameter not specified or empty.
+ #[serde(rename = "rk", default)]
+ pub resident_key: bool,
+
+ /// Client PIN:
+ /// If present and set to true, it indicates that the device is capable of
+ /// accepting a PIN from the client and PIN has been set.
+ /// If present and set to false, it indicates that the device is capable of
+ /// accepting a PIN from the client and PIN has not been set yet.
+ /// If absent, it indicates that the device is not capable of accepting a
+ /// PIN from the client.
+ /// Client PIN is one of the ways to do user verification.
+ #[serde(rename = "clientPin")]
+ pub client_pin: Option<bool>,
+
+ /// Indicates that the device is capable of testing user presence.
+ #[serde(rename = "up", default = "true_val")]
+ pub user_presence: bool,
+
+ /// Indicates that the device is capable of verifying the user within
+ /// itself. For example, devices with UI, biometrics fall into this
+ /// category.
+ /// If present and set to true, it indicates that the device is capable of
+ /// user verification within itself and has been configured.
+ /// If present and set to false, it indicates that the device is capable of
+ /// user verification within itself and has not been yet configured. For
+ /// example, a biometric device that has not yet been configured will
+ /// return this parameter set to false.
+ /// If absent, it indicates that the device is not capable of user
+ /// verification within itself.
+ /// A device that can only do Client PIN will not return the "uv" parameter.
+ /// If a device is capable of verifying the user within itself as well as
+ /// able to do Client PIN, it will return both "uv" and the Client PIN
+ /// option.
+ // TODO(MS): My Token (key-ID FIDO2) does return Some(false) here, even though
+ // it has no built-in verification method. Not to be trusted...
+ #[serde(rename = "uv")]
+ pub user_verification: Option<bool>,
+
+ // ----------------------------------------------------
+ // CTAP 2.1 options
+ // ----------------------------------------------------
+ /// If pinUvAuthToken is:
+ /// present and set to true
+ /// if the clientPin option id is present and set to true, then the
+ /// authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingPinWithPermissions
+ /// subcommand. If the uv option id is present and set to true, then
+ /// the authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingUvWithPermissions
+ /// subcommand.
+ /// present and set to false, or absent.
+ /// the authenticator does not support authenticatorClientPIN's
+ /// getPinUvAuthTokenUsingPinWithPermissions and getPinUvAuthTokenUsingUvWithPermissions
+ /// subcommands.
+ #[serde(rename = "pinUvAuthToken")]
+ pub pin_uv_auth_token: Option<bool>,
+
+ /// If this noMcGaPermissionsWithClientPin is:
+ /// present and set to true
+ /// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions
+ /// (or getPinToken) cannot be used for authenticatorMakeCredential or
+ /// authenticatorGetAssertion commands, because it will lack the necessary
+ /// mc and ga permissions. In this situation, platforms SHOULD NOT attempt
+ /// to use getPinUvAuthTokenUsingPinWithPermissions if using
+ /// getPinUvAuthTokenUsingUvWithPermissions fails.
+ /// present and set to false, or absent.
+ /// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions
+ /// (or getPinToken) can be used for authenticatorMakeCredential or
+ /// authenticatorGetAssertion commands.
+ /// Note: noMcGaPermissionsWithClientPin MUST only be present if the
+ /// clientPin option ID is present.
+ #[serde(rename = "noMcGaPermissionsWithClientPin")]
+ pub no_mc_ga_permissions_with_client_pin: Option<bool>,
+
+ /// If largeBlobs is:
+ /// present and set to true
+ /// the authenticator supports the authenticatorLargeBlobs command.
+ /// present and set to false, or absent.
+ /// The authenticatorLargeBlobs command is NOT supported.
+ #[serde(rename = "largeBlobs")]
+ pub large_blobs: Option<bool>,
+
+ /// Enterprise Attestation feature support:
+ /// If ep is:
+ /// Present and set to true
+ /// The authenticator is enterprise attestation capable, and enterprise
+ /// attestation is enabled.
+ /// Present and set to false
+ /// The authenticator is enterprise attestation capable, and enterprise
+ /// attestation is disabled.
+ /// Absent
+ /// The Enterprise Attestation feature is NOT supported.
+ #[serde(rename = "ep")]
+ pub ep: Option<bool>,
+
+ /// If bioEnroll is:
+ /// present and set to true
+ /// the authenticator supports the authenticatorBioEnrollment commands,
+ /// and has at least one bio enrollment presently provisioned.
+ /// present and set to false
+ /// the authenticator supports the authenticatorBioEnrollment commands,
+ /// and does not yet have any bio enrollments provisioned.
+ /// absent
+ /// the authenticatorBioEnrollment commands are NOT supported.
+ #[serde(rename = "bioEnroll")]
+ pub bio_enroll: Option<bool>,
+
+ /// "FIDO_2_1_PRE" Prototype Credential management support:
+ /// If userVerificationMgmtPreview is:
+ /// present and set to true
+ /// the authenticator supports the Prototype authenticatorBioEnrollment (0x41)
+ /// commands, and has at least one bio enrollment presently provisioned.
+ /// present and set to false
+ /// the authenticator supports the Prototype authenticatorBioEnrollment (0x41)
+ /// commands, and does not yet have any bio enrollments provisioned.
+ /// absent
+ /// the Prototype authenticatorBioEnrollment (0x41) commands are not supported.
+ #[serde(rename = "userVerificationMgmtPreview")]
+ pub user_verification_mgmt_preview: Option<bool>,
+
+ /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the be permission:
+ /// This option ID MUST only be present if bioEnroll is also present.
+ /// If uvBioEnroll is:
+ /// present and set to true
+ /// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions
+ /// is supported.
+ /// present and set to false, or absent.
+ /// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions
+ /// is NOT supported.
+ #[serde(rename = "uvBioEnroll")]
+ pub uv_bio_enroll: Option<bool>,
+
+ /// authenticatorConfig command support:
+ /// If authnrCfg is:
+ /// present and set to true
+ /// the authenticatorConfig command is supported.
+ /// present and set to false, or absent.
+ /// the authenticatorConfig command is NOT supported.
+ #[serde(rename = "authnrCfg")]
+ pub authnr_cfg: Option<bool>,
+
+ /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the acfg permission:
+ /// This option ID MUST only be present if authnrCfg is also present.
+ /// If uvAcfg is:
+ /// present and set to true
+ /// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions
+ /// is supported.
+ /// present and set to false, or absent.
+ /// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions
+ /// is NOT supported.
+ #[serde(rename = "uvAcfg")]
+ pub uv_acfg: Option<bool>,
+
+ /// Credential management support:
+ /// If credMgmt is:
+ /// present and set to true
+ /// the authenticatorCredentialManagement command is supported.
+ /// present and set to false, or absent.
+ /// the authenticatorCredentialManagement command is NOT supported.
+ #[serde(rename = "credMgmt")]
+ pub cred_mgmt: Option<bool>,
+
+ /// "FIDO_2_1_PRE" Prototype Credential management support:
+ /// If credentialMgmtPreview is:
+ /// present and set to true
+ /// the Prototype authenticatorCredentialManagement (0x41) command is supported.
+ /// present and set to false, or absent.
+ /// the Prototype authenticatorCredentialManagement (0x41) command is NOT supported.
+ #[serde(rename = "credentialMgmtPreview")]
+ pub credential_mgmt_preview: Option<bool>,
+
+ /// Support for the Set Minimum PIN Length feature.
+ /// If setMinPINLength is:
+ /// present and set to true
+ /// the setMinPINLength subcommand is supported.
+ /// present and set to false, or absent.
+ /// the setMinPINLength subcommand is NOT supported.
+ /// Note: setMinPINLength MUST only be present if the clientPin option ID is present.
+ #[serde(rename = "setMinPINLength")]
+ pub set_min_pin_length: Option<bool>,
+
+ /// Support for making non-discoverable credentials without requiring User Verification.
+ /// If makeCredUvNotRqd is:
+ /// present and set to true
+ /// the authenticator allows creation of non-discoverable credentials without
+ /// requiring any form of user verification, if the platform requests this behaviour.
+ /// present and set to false, or absent.
+ /// the authenticator requires some form of user verification for creating
+ /// non-discoverable credentials, regardless of the parameters the platform supplies
+ /// for the authenticatorMakeCredential command.
+ /// Authenticators SHOULD include this option with the value true.
+ #[serde(rename = "makeCredUvNotRqd")]
+ pub make_cred_uv_not_rqd: Option<bool>,
+
+ /// Support for the Always Require User Verification feature:
+ /// If alwaysUv is
+ /// present and set to true
+ /// the authenticator supports the Always Require User Verification feature and it is enabled.
+ /// present and set to false
+ /// the authenticator supports the Always Require User Verification feature but it is disabled.
+ /// absent
+ /// the authenticator does not support the Always Require User Verification feature.
+ /// Note: If the alwaysUv option ID is present and true the authenticator MUST set the value
+ /// of makeCredUvNotRqd to false.
+ #[serde(rename = "alwaysUv")]
+ pub always_uv: Option<bool>,
+}
+
+impl Default for AuthenticatorOptions {
+ fn default() -> Self {
+ AuthenticatorOptions {
+ platform_device: false,
+ resident_key: false,
+ client_pin: None,
+ user_presence: true,
+ user_verification: None,
+ pin_uv_auth_token: None,
+ no_mc_ga_permissions_with_client_pin: None,
+ large_blobs: None,
+ ep: None,
+ bio_enroll: None,
+ user_verification_mgmt_preview: None,
+ uv_bio_enroll: None,
+ authnr_cfg: None,
+ uv_acfg: None,
+ cred_mgmt: None,
+ credential_mgmt_preview: None,
+ set_min_pin_length: None,
+ make_cred_uv_not_rqd: None,
+ always_uv: None,
+ }
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+pub enum AuthenticatorVersion {
+ U2F_V2,
+ FIDO_2_0,
+ FIDO_2_1_PRE,
+ FIDO_2_1,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
+pub struct AuthenticatorInfo {
+ pub versions: Vec<AuthenticatorVersion>,
+ pub extensions: Vec<String>,
+ pub aaguid: AAGuid,
+ pub options: AuthenticatorOptions,
+ pub max_msg_size: Option<usize>,
+ pub pin_protocols: Option<Vec<u64>>,
+ // CTAP 2.1
+ pub max_credential_count_in_list: Option<usize>,
+ pub max_credential_id_length: Option<usize>,
+ pub transports: Option<Vec<String>>,
+ pub algorithms: Option<Vec<PublicKeyCredentialParameters>>,
+ pub max_ser_large_blob_array: Option<u64>,
+ pub force_pin_change: Option<bool>,
+ pub min_pin_length: Option<u64>,
+ pub firmware_version: Option<u64>,
+ pub max_cred_blob_length: Option<u64>,
+ pub max_rpids_for_set_min_pin_length: Option<u64>,
+ pub preferred_platform_uv_attempts: Option<u64>,
+ pub uv_modality: Option<u64>,
+ pub certifications: Option<BTreeMap<String, u64>>,
+ pub remaining_discoverable_credentials: Option<u64>,
+ pub vendor_prototype_config_commands: Option<Vec<u64>>,
+}
+
+impl AuthenticatorInfo {
+ pub fn supports_cred_protect(&self) -> bool {
+ self.extensions.contains(&"credProtect".to_string())
+ }
+
+ pub fn supports_hmac_secret(&self) -> bool {
+ self.extensions.contains(&"hmac-secret".to_string())
+ }
+
+ pub fn max_supported_version(&self) -> AuthenticatorVersion {
+ let versions = vec![
+ AuthenticatorVersion::FIDO_2_1,
+ AuthenticatorVersion::FIDO_2_1_PRE,
+ AuthenticatorVersion::FIDO_2_0,
+ AuthenticatorVersion::U2F_V2,
+ ];
+ for ver in versions {
+ if self.versions.contains(&ver) {
+ return ver;
+ }
+ }
+ AuthenticatorVersion::U2F_V2
+ }
+
+ pub fn device_is_protected(&self) -> bool {
+ self.options.client_pin == Some(true) || self.options.user_verification == Some(true)
+ }
+}
+
+impl CtapResponse for AuthenticatorInfo {}
+
+macro_rules! parse_next_optional_value {
+ ($name:expr, $map:expr) => {
+ if $name.is_some() {
+ return Err(serde::de::Error::duplicate_field("$name"));
+ }
+ $name = Some($map.next_value()?);
+ };
+}
+
+impl<'de> Deserialize<'de> for AuthenticatorInfo {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AuthenticatorInfoVisitor;
+
+ impl<'de> Visitor<'de> for AuthenticatorInfoVisitor {
+ type Value = AuthenticatorInfo;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut versions = Vec::new();
+ let mut extensions = Vec::new();
+ let mut aaguid = None;
+ let mut options = AuthenticatorOptions::default();
+ let mut max_msg_size = None;
+ let mut pin_protocols: Option<Vec<_>> = None;
+ let mut max_credential_count_in_list = None;
+ let mut max_credential_id_length = None;
+ let mut transports = None;
+ let mut algorithms = None;
+ let mut max_ser_large_blob_array = None;
+ let mut force_pin_change = None;
+ let mut min_pin_length = None;
+ let mut firmware_version = None;
+ let mut max_cred_blob_length = None;
+ let mut max_rpids_for_set_min_pin_length = None;
+ let mut preferred_platform_uv_attempts = None;
+ let mut uv_modality = None;
+ let mut certifications = None;
+ let mut remaining_discoverable_credentials = None;
+ let mut vendor_prototype_config_commands = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ 0x01 => {
+ if !versions.is_empty() {
+ return Err(serde::de::Error::duplicate_field("versions"));
+ }
+ versions = map.next_value()?;
+ }
+ 0x02 => {
+ if !extensions.is_empty() {
+ return Err(serde::de::Error::duplicate_field("extensions"));
+ }
+ extensions = map.next_value()?;
+ }
+ 0x03 => {
+ parse_next_optional_value!(aaguid, map);
+ }
+ 0x04 => {
+ options = map.next_value()?;
+ }
+ 0x05 => {
+ parse_next_optional_value!(max_msg_size, map);
+ }
+ 0x06 => {
+ parse_next_optional_value!(pin_protocols, map);
+ }
+ 0x07 => {
+ parse_next_optional_value!(max_credential_count_in_list, map);
+ }
+ 0x08 => {
+ parse_next_optional_value!(max_credential_id_length, map);
+ }
+ 0x09 => {
+ parse_next_optional_value!(transports, map);
+ }
+ 0x0a => {
+ parse_next_optional_value!(algorithms, map);
+ }
+ 0x0b => {
+ parse_next_optional_value!(max_ser_large_blob_array, map);
+ }
+ 0x0c => {
+ parse_next_optional_value!(force_pin_change, map);
+ }
+ 0x0d => {
+ parse_next_optional_value!(min_pin_length, map);
+ }
+ 0x0e => {
+ parse_next_optional_value!(firmware_version, map);
+ }
+ 0x0f => {
+ parse_next_optional_value!(max_cred_blob_length, map);
+ }
+ 0x10 => {
+ parse_next_optional_value!(max_rpids_for_set_min_pin_length, map);
+ }
+ 0x11 => {
+ parse_next_optional_value!(preferred_platform_uv_attempts, map);
+ }
+ 0x12 => {
+ parse_next_optional_value!(uv_modality, map);
+ }
+ 0x13 => {
+ parse_next_optional_value!(certifications, map);
+ }
+ 0x14 => {
+ parse_next_optional_value!(remaining_discoverable_credentials, map);
+ }
+ 0x15 => {
+ parse_next_optional_value!(vendor_prototype_config_commands, map);
+ }
+ k => {
+ warn!("GetInfo: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ continue;
+ }
+ }
+ }
+
+ if versions.is_empty() {
+ return Err(M::Error::custom(
+ "expected at least one version, got none".to_string(),
+ ));
+ }
+
+ if let Some(protocols) = &pin_protocols {
+ if protocols.is_empty() {
+ return Err(M::Error::custom(
+ "Token returned empty PIN protocol list, which is not allowed"
+ .to_string(),
+ ));
+ }
+ }
+
+ if let Some(aaguid) = aaguid {
+ Ok(AuthenticatorInfo {
+ versions,
+ extensions,
+ aaguid,
+ options,
+ max_msg_size,
+ pin_protocols,
+ max_credential_count_in_list,
+ max_credential_id_length,
+ transports,
+ algorithms,
+ max_ser_large_blob_array,
+ force_pin_change,
+ min_pin_length,
+ firmware_version,
+ max_cred_blob_length,
+ max_rpids_for_set_min_pin_length,
+ preferred_platform_uv_attempts,
+ uv_modality,
+ certifications,
+ remaining_discoverable_credentials,
+ vendor_prototype_config_commands,
+ })
+ } else {
+ Err(M::Error::custom("No AAGuid specified".to_string()))
+ }
+ }
+ }
+
+ deserializer.deserialize_bytes(AuthenticatorInfoVisitor)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::{Capability, HIDCmd, CID_BROADCAST};
+ use crate::crypto::COSEAlgorithm;
+ use crate::transport::device_selector::Device;
+ use crate::transport::platform::device::IN_HID_RPT_SIZE;
+ use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol};
+ use rand::{thread_rng, RngCore};
+ use serde_cbor::de::from_slice;
+
+ // Raw data take from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py
+ pub const AAGUID_RAW: [u8; 16] = [
+ 0xF8, 0xA0, 0x11, 0xF3, 0x8C, 0x0A, 0x4D, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1F, 0x9E, 0xDC,
+ 0x7D,
+ ];
+
+ pub const AUTHENTICATOR_INFO_PAYLOAD: [u8; 89] = [
+ 0xa6, // map(6)
+ 0x01, // unsigned(1)
+ 0x82, // array(2)
+ 0x66, // text(6)
+ 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, // "U2F_V2"
+ 0x68, // text(8)
+ 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, // "FIDO_2_0"
+ 0x02, // unsigned(2)
+ 0x82, // array(2)
+ 0x63, // text(3)
+ 0x75, 0x76, 0x6d, // "uvm"
+ 0x6b, // text(11)
+ 0x68, 0x6d, 0x61, 0x63, 0x2d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0x03, // unsigned(3)
+ 0x50, // bytes(16)
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d, // "\xF8\xA0\u0011\xF3\x8C\nM\u0015\x80\u0006\u0017\u0011\u001F\x9E\xDC}"
+ 0x04, // unsigned(4)
+ 0xa4, // map(4)
+ 0x62, // text(2)
+ 0x72, 0x6b, // "rk"
+ 0xf5, // primitive(21)
+ 0x62, // text(2)
+ 0x75, 0x70, // "up"
+ 0xf5, // primitive(21)
+ 0x64, // text(4)
+ 0x70, 0x6c, 0x61, 0x74, // "plat"
+ 0xf4, // primitive(20)
+ 0x69, // text(9)
+ 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x69, 0x6e, // "clientPin"
+ 0xf4, // primitive(20)
+ 0x05, // unsigned(5)
+ 0x19, 0x04, 0xb0, // unsigned(1200)
+ 0x06, // unsigned(6)
+ 0x81, // array(1)
+ 0x01, // unsigned(1)
+ ];
+
+ // Real world example from Yubikey Bio
+ pub const AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C: [u8; 409] = [
+ 0xB3, // map(19)
+ 0x01, // unsigned(1)
+ 0x84, // array(4)
+ 0x66, // text(6)
+ 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, // "U2F_V2"
+ 0x68, // text(8)
+ 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, // "FIDO_2_0"
+ 0x6C, // text(12)
+ 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52,
+ 0x45, // "FIDO_2_1_PRE"
+ 0x68, // text(8)
+ 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, // "FIDO_2_1"
+ 0x02, // unsigned(2)
+ 0x85, // array(5)
+ 0x6B, // text(11)
+ 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, // "credProtect"
+ 0x6B, // text(11)
+ 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0x6C, // text(12)
+ 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x4B, 0x65,
+ 0x79, // "largeBlobKey"
+ 0x68, // text(8)
+ 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, // "credBlob"
+ 0x6C, // text(12)
+ 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74,
+ 0x68, // "minPinLength"
+ 0x03, // unsigned(3)
+ 0x50, // bytes(16)
+ 0xD8, 0x52, 0x2D, 0x9F, 0x57, 0x5B, 0x48, 0x66, 0x88, 0xA9, 0xBA, 0x99, 0xFA, 0x02, 0xF3,
+ 0x5B, // "\xD8R-\x9FW[Hf\x88\xA9\xBA\x99\xFA\u0002\xF3["
+ 0x04, // unsigned(4)
+ 0xB0, // map(16)
+ 0x62, // text(2)
+ 0x72, 0x6B, // "rk"
+ 0xF5, // primitive(21)
+ 0x62, // text(2)
+ 0x75, 0x70, // "up"
+ 0xF5, // primitive(21)
+ 0x62, // text(2)
+ 0x75, 0x76, // "uv"
+ 0xF5, // primitive(21)
+ 0x64, // text(4)
+ 0x70, 0x6C, 0x61, 0x74, // "plat"
+ 0xF4, // primitive(20)
+ 0x67, // text(7)
+ 0x75, 0x76, 0x54, 0x6F, 0x6B, 0x65, 0x6E, // "uvToken"
+ 0xF5, // primitive(21)
+ 0x68, // text(8)
+ 0x61, 0x6C, 0x77, 0x61, 0x79, 0x73, 0x55, 0x76, // "alwaysUv"
+ 0xF5, // primitive(21)
+ 0x68, // text(8)
+ 0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, // "credMgmt"
+ 0xF5, // primitive(21)
+ 0x69, // text(9)
+ 0x61, 0x75, 0x74, 0x68, 0x6E, 0x72, 0x43, 0x66, 0x67, // "authnrCfg"
+ 0xF5, // primitive(21)
+ 0x69, // text(9)
+ 0x62, 0x69, 0x6F, 0x45, 0x6E, 0x72, 0x6F, 0x6C, 0x6C, // "bioEnroll"
+ 0xF5, // primitive(21)
+ 0x69, // text(9)
+ 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, // "clientPin"
+ 0xF5, // primitive(21)
+ 0x6A, // text(10)
+ 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x73, // "largeBlobs"
+ 0xF5, // primitive(21)
+ 0x6E, // text(14)
+ 0x70, 0x69, 0x6E, 0x55, 0x76, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6F, 0x6B, 0x65,
+ 0x6E, // "pinUvAuthToken"
+ 0xF5, // primitive(21)
+ 0x6F, // text(15)
+ 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74,
+ 0x68, // "setMinPINLength"
+ 0xF5, // primitive(21)
+ 0x70, // text(16)
+ 0x6D, 0x61, 0x6B, 0x65, 0x43, 0x72, 0x65, 0x64, 0x55, 0x76, 0x4E, 0x6F, 0x74, 0x52, 0x71,
+ 0x64, // "makeCredUvNotRqd"
+ 0xF4, // primitive(20)
+ 0x75, // text(21)
+ 0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x4D, 0x67, 0x6D, 0x74, 0x50,
+ 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, // "credentialMgmtPreview"
+ 0xF5, // primitive(21)
+ 0x78, 0x1B, // text(27)
+ 0x75, 0x73, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F,
+ 0x6E, 0x4D, 0x67, 0x6D, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65,
+ 0x77, // "userVerificationMgmtPreview"
+ 0xF5, // primitive(21)
+ 0x05, // unsigned(5)
+ 0x19, 0x04, 0xB0, // unsigned(1200)
+ 0x06, // unsigned(6)
+ 0x82, // array(2)
+ 0x02, // unsigned(2)
+ 0x01, // unsigned(1)
+ 0x07, // unsigned(7)
+ 0x08, // unsigned(8)
+ 0x08, // unsigned(8)
+ 0x18, 0x80, // unsigned(128)
+ 0x09, // unsigned(9)
+ 0x81, // array(1)
+ 0x63, // text(3)
+ 0x75, 0x73, 0x62, // "usb"
+ 0x0A, // unsigned(10)
+ 0x82, // array(2)
+ 0xA2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6C, 0x67, // "alg"
+ 0x26, // negative(6)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6A, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0xA2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6C, 0x67, // "alg"
+ 0x27, // negative(7)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6A, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0x0B, // unsigned(11)
+ 0x19, 0x04, 0x00, // unsigned(1024)
+ 0x0C, // unsigned(12)
+ 0xF4, // primitive(20)
+ 0x0D, // unsigned(13)
+ 0x04, // unsigned(4)
+ 0x0E, // unsigned(14)
+ 0x1A, 0x00, 0x05, 0x05, 0x06, // unsigned(328966)
+ 0x0F, // unsigned(15)
+ 0x18, 0x20, // unsigned(32)
+ 0x10, // unsigned(16)
+ 0x01, // unsigned(1)
+ 0x11, // unsigned(17)
+ 0x03, // unsigned(3)
+ 0x12, // unsigned(18)
+ 0x02, // unsigned(2)
+ 0x14, // unsigned(20)
+ 0x18, 0x18, // unsigned(24)
+ ];
+
+ #[test]
+ fn parse_authenticator_info() {
+ let authenticator_info: AuthenticatorInfo =
+ from_slice(&AUTHENTICATOR_INFO_PAYLOAD).unwrap();
+
+ let expected = AuthenticatorInfo {
+ versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
+ extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
+ aaguid: AAGuid(AAGUID_RAW),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(false),
+ user_presence: true,
+ user_verification: None,
+ ..Default::default()
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: Some(vec![1]),
+ max_credential_count_in_list: None,
+ max_credential_id_length: None,
+ transports: None,
+ algorithms: None,
+ max_ser_large_blob_array: None,
+ force_pin_change: None,
+ min_pin_length: None,
+ firmware_version: None,
+ max_cred_blob_length: None,
+ max_rpids_for_set_min_pin_length: None,
+ preferred_platform_uv_attempts: None,
+ uv_modality: None,
+ certifications: None,
+ remaining_discoverable_credentials: None,
+ vendor_prototype_config_commands: None,
+ };
+
+ assert_eq!(authenticator_info, expected);
+
+ // Test broken auth info
+ let mut broken_payload = AUTHENTICATOR_INFO_PAYLOAD.to_vec();
+ // Have one more entry in the map
+ broken_payload[0] += 1;
+ // Add the additional entry at the back with an invalid key
+ broken_payload.extend_from_slice(&[
+ 0x17, // unsigned(23) -> invalid key-number. CTAP2.1 goes only to 0x15
+ 0x6B, // text(11)
+ 0x69, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x5F, 0x6B, 0x65, 0x79, // "invalid_key"
+ ]);
+
+ let authenticator_info: AuthenticatorInfo = from_slice(&broken_payload).unwrap();
+ assert_eq!(authenticator_info, expected);
+ }
+
+ #[test]
+ fn parse_authenticator_info_yk_bio_5c() {
+ let authenticator_info: AuthenticatorInfo =
+ from_slice(&AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C).unwrap();
+
+ let expected = AuthenticatorInfo {
+ versions: vec![
+ AuthenticatorVersion::U2F_V2,
+ AuthenticatorVersion::FIDO_2_0,
+ AuthenticatorVersion::FIDO_2_1_PRE,
+ AuthenticatorVersion::FIDO_2_1,
+ ],
+ extensions: vec![
+ "credProtect".to_string(),
+ "hmac-secret".to_string(),
+ "largeBlobKey".to_string(),
+ "credBlob".to_string(),
+ "minPinLength".to_string(),
+ ],
+ aaguid: AAGuid([
+ 0xd8, 0x52, 0x2d, 0x9f, 0x57, 0x5b, 0x48, 0x66, 0x88, 0xa9, 0xba, 0x99, 0xfa, 0x02,
+ 0xf3, 0x5b,
+ ]),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(true),
+ user_presence: true,
+ user_verification: Some(true),
+ pin_uv_auth_token: Some(true),
+ no_mc_ga_permissions_with_client_pin: None,
+ large_blobs: Some(true),
+ ep: None,
+ bio_enroll: Some(true),
+ user_verification_mgmt_preview: Some(true),
+ uv_bio_enroll: None,
+ authnr_cfg: Some(true),
+ uv_acfg: None,
+ cred_mgmt: Some(true),
+ credential_mgmt_preview: Some(true),
+ set_min_pin_length: Some(true),
+ make_cred_uv_not_rqd: Some(false),
+ always_uv: Some(true),
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: Some(vec![2, 1]),
+ max_credential_count_in_list: Some(8),
+ max_credential_id_length: Some(128),
+ transports: Some(vec!["usb".to_string()]),
+ algorithms: Some(vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::EDDSA,
+ },
+ ]),
+ max_ser_large_blob_array: Some(1024),
+ force_pin_change: Some(false),
+ min_pin_length: Some(4),
+ firmware_version: Some(328966),
+ max_cred_blob_length: Some(32),
+ max_rpids_for_set_min_pin_length: Some(1),
+ preferred_platform_uv_attempts: Some(3),
+ uv_modality: Some(2),
+ certifications: None,
+ remaining_discoverable_credentials: Some(24),
+ vendor_prototype_config_commands: None,
+ };
+
+ assert_eq!(authenticator_info, expected);
+ }
+
+ #[test]
+ fn test_get_info_ctap2_only() {
+ let mut device = Device::new("commands/get_info").unwrap();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
+ let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
+
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ // init packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ device.add_write(&msg, 0);
+
+ // init_resp packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![
+ 0x06, /* HIDCmd::Init without TYPE_INIT */
+ 0x00, 0x11,
+ ]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ msg.extend_from_slice(&cid); // new channel id
+
+ // We are setting NMSG, to signal that the device does not support CTAP1
+ msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01 | 0x04 | 0x08]); // versions + flags (wink+cbor+nmsg)
+ device.add_read(&msg, 0);
+
+ // ctap2 request
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
+ msg.extend(vec![0x04]); // authenticatorGetInfo
+ device.add_write(&msg, 0);
+
+ // ctap2 response
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x5A]); // cmd + bcnt
+ msg.extend(vec![0]); // Status code: Success
+ msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[0..(IN_HID_RPT_SIZE - 8)]);
+ device.add_read(&msg, 0);
+ // Continuation package
+ let mut msg = cid.to_vec();
+ msg.extend(vec![0x00]); // SEQ
+ msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[(IN_HID_RPT_SIZE - 8)..]);
+ device.add_read(&msg, 0);
+ device.init().expect("Failed to init device");
+
+ assert_eq!(device.get_cid(), &cid);
+
+ let dev_info = device.get_device_info();
+ assert_eq!(
+ dev_info.cap_flags,
+ Capability::WINK | Capability::CBOR | Capability::NMSG
+ );
+
+ let result = device
+ .get_authenticator_info()
+ .expect("Didn't get any authenticator_info");
+ let expected = AuthenticatorInfo {
+ versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
+ extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
+ aaguid: AAGuid(AAGUID_RAW),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(false),
+ user_presence: true,
+ user_verification: None,
+ ..Default::default()
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: Some(vec![1]),
+ max_credential_count_in_list: None,
+ max_credential_id_length: None,
+ transports: None,
+ algorithms: None,
+ max_ser_large_blob_array: None,
+ force_pin_change: None,
+ min_pin_length: None,
+ firmware_version: None,
+ max_cred_blob_length: None,
+ max_rpids_for_set_min_pin_length: None,
+ preferred_platform_uv_attempts: None,
+ uv_modality: None,
+ certifications: None,
+ remaining_discoverable_credentials: None,
+ vendor_prototype_config_commands: None,
+ };
+
+ assert_eq!(result, &expected);
+ }
+
+ #[test]
+ fn test_authenticator_info_max_version() {
+ let fido2_0 = AuthenticatorInfo {
+ versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
+ ..Default::default()
+ };
+ assert_eq!(
+ fido2_0.max_supported_version(),
+ AuthenticatorVersion::FIDO_2_0
+ );
+
+ let fido2_1_pre = AuthenticatorInfo {
+ versions: vec![
+ AuthenticatorVersion::FIDO_2_1_PRE,
+ AuthenticatorVersion::U2F_V2,
+ ],
+ ..Default::default()
+ };
+ assert_eq!(
+ fido2_1_pre.max_supported_version(),
+ AuthenticatorVersion::FIDO_2_1_PRE
+ );
+
+ let fido2_1 = AuthenticatorInfo {
+ versions: vec![
+ AuthenticatorVersion::FIDO_2_1_PRE,
+ AuthenticatorVersion::FIDO_2_1,
+ AuthenticatorVersion::U2F_V2,
+ AuthenticatorVersion::FIDO_2_0,
+ ],
+ ..Default::default()
+ };
+ assert_eq!(
+ fido2_1.max_supported_version(),
+ AuthenticatorVersion::FIDO_2_1
+ );
+ }
+
+ #[test]
+ fn parse_authenticator_info_protocol_versions() {
+ let mut expected = AuthenticatorInfo {
+ versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
+ extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
+ aaguid: AAGuid(AAGUID_RAW),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(false),
+ user_presence: true,
+ ..Default::default()
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: None,
+ ..Default::default()
+ };
+
+ let raw_data = AUTHENTICATOR_INFO_PAYLOAD.to_vec();
+ // pin protocol entry (last 3 bytes in payload):
+ // 0x06, // unsigned(6)
+ // 0x81, // array(1)
+ // 0x01, // unsigned(1)
+
+ let mut raw_empty_list = raw_data.clone();
+ raw_empty_list.pop();
+ let raw_list_len = raw_empty_list.len();
+ raw_empty_list[raw_list_len - 1] = 0x80; // array(0) instead of array(1)
+
+ // Empty protocols-array, that should produce an error
+ from_slice::<AuthenticatorInfo>(&raw_empty_list).unwrap_err();
+
+ // No protocols specified
+ let mut raw_no_list = raw_data.clone();
+ raw_no_list.pop();
+ raw_no_list.pop();
+ raw_no_list.pop();
+ raw_no_list[0] = 0xa5; // map(5) instead of map(6)
+
+ let authenticator_info: AuthenticatorInfo = from_slice(&raw_no_list).unwrap();
+ assert_eq!(authenticator_info, expected);
+
+ // Both 1 and 2
+ let mut raw_list = raw_data;
+ let raw_list_len = raw_list.len();
+ raw_list[raw_list_len - 2] = 0x82; // array(2) instead of array(1)
+ raw_list.push(0x02);
+
+ expected.pin_protocols = Some(vec![1, 2]);
+ let authenticator_info: AuthenticatorInfo = from_slice(&raw_list).unwrap();
+ assert_eq!(authenticator_info, expected);
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs
new file mode 100644
index 0000000000..b75bb51b08
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs
@@ -0,0 +1,54 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::ctap2::commands::get_assertion::GetAssertionResponse;
+use crate::transport::errors::HIDError;
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug)]
+pub(crate) struct GetNextAssertion;
+
+impl RequestCtap2 for GetNextAssertion {
+ type Output = GetAssertionResponse;
+
+ fn command(&self) -> Command {
+ Command::GetNextAssertion
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!("response status code: {:?}", status);
+ if input.len() > 1 {
+ if status.is_ok() {
+ let assertion = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ // TODO(baloo): check assertion response does not have numberOfCredentials
+ Ok(assertion)
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(CommandError::StatusCode(status, Some(data)).into())
+ }
+ } else if status.is_ok() {
+ Err(CommandError::InputTooSmall.into())
+ } else {
+ Err(CommandError::StatusCode(status, None).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ unimplemented!()
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_version.rs b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs
new file mode 100644
index 0000000000..40019c8f1a
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs
@@ -0,0 +1,121 @@
+use super::{CommandError, CtapResponse, RequestCtap1, Retryable};
+use crate::consts::U2F_VERSION;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use crate::u2ftypes::CTAP1RequestAPDU;
+
+#[allow(non_camel_case_types)]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum U2FInfo {
+ U2F_V2,
+}
+
+impl CtapResponse for U2FInfo {}
+
+#[derive(Debug, Default)]
+// TODO(baloo): if one does not issue U2F_VERSION before makecredentials or getassertion, token
+// will return error (ConditionsNotSatified), test this in unit tests
+pub struct GetVersion {}
+
+impl RequestCtap1 for GetVersion {
+ type Output = U2FInfo;
+ type AdditionalInfo = ();
+
+ fn handle_response_ctap1<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ _status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ _add_info: &(),
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ if input.is_empty() {
+ return Err(Retryable::Error(HIDError::Command(
+ CommandError::InputTooSmall,
+ )));
+ }
+
+ let expected = String::from("U2F_V2");
+ let result = String::from_utf8_lossy(input);
+ match result {
+ ref data if data == &expected => Ok(U2FInfo::U2F_V2),
+ _ => Err(Retryable::Error(HIDError::UnexpectedVersion)),
+ }
+ }
+
+ fn ctap1_format(&self) -> Result<(Vec<u8>, ()), HIDError> {
+ let flags = 0;
+
+ let cmd = U2F_VERSION;
+ let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?;
+ Ok((data, ()))
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ dev.get_version(self)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR};
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol};
+ use rand::{thread_rng, RngCore};
+
+ #[test]
+ fn test_get_version_ctap1_only() {
+ let mut device = Device::new("commands/get_version").unwrap();
+ device.downgrade_to_ctap1();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP1);
+ let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
+
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ // init packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend([HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ device.add_write(&msg, 0);
+
+ // init_resp packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![
+ 0x06, /* HIDCmd::Init without !TYPE_INIT */
+ 0x00, 0x11,
+ ]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ msg.extend_from_slice(&cid); // new channel id
+
+ // We are not setting CBOR, to signal that the device does not support CTAP1
+ msg.extend([0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags (wink)
+ device.add_read(&msg, 0);
+
+ // ctap1 U2F_VERSION request
+ let mut msg = cid.to_vec();
+ msg.extend([HIDCmd::Msg.into(), 0x0, 0x7]); // cmd + bcnt
+ msg.extend([0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let mut msg = cid.to_vec();
+ msg.extend([HIDCmd::Msg.into(), 0x0, 0x08]); // cmd + bcnt
+ msg.extend([0x55, 0x32, 0x46, 0x5f, 0x56, 0x32]); // 'U2F_V2'
+ msg.extend(SW_NO_ERROR);
+ device.add_read(&msg, 0);
+
+ device.init().expect("Failed to init device");
+
+ assert_eq!(device.get_cid(), &cid);
+
+ let dev_info = device.get_device_info();
+ assert_eq!(dev_info.cap_flags, Capability::WINK);
+
+ let result = device.get_authenticator_info();
+ assert!(result.is_none());
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs
new file mode 100644
index 0000000000..1de1048404
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs
@@ -0,0 +1,1061 @@
+use super::get_info::{AuthenticatorInfo, AuthenticatorVersion};
+use super::{
+ Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable,
+ StatusCode,
+};
+use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE};
+use crate::crypto::{
+ parse_u2f_der_certificate, COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve,
+ PinUvAuthParam, PinUvAuthToken,
+};
+use crate::ctap2::attestation::{
+ AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F,
+ AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, HmacSecretResponse,
+};
+use crate::ctap2::client_data::ClientDataHash;
+use crate::ctap2::server::{
+ AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs,
+ AuthenticatorAttachment, CredentialProtectionPolicy, PublicKeyCredentialDescriptor,
+ PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash,
+ UserVerificationRequirement,
+};
+use crate::ctap2::utils::{read_byte, serde_parse_err};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use crate::u2ftypes::CTAP1RequestAPDU;
+use serde::{
+ de::{Error as DesError, MapAccess, Unexpected, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_cbor::{self, de::from_slice, ser, Value};
+use std::fmt;
+use std::io::{Cursor, Read};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct MakeCredentialsResult {
+ pub att_obj: AttestationObject,
+ pub attachment: AuthenticatorAttachment,
+ pub extensions: AuthenticationExtensionsClientOutputs,
+}
+
+impl MakeCredentialsResult {
+ pub fn from_ctap1(input: &[u8], rp_id_hash: &RpIdHash) -> Result<Self, CommandError> {
+ let mut data = Cursor::new(input);
+ let magic_num = read_byte(&mut data).map_err(CommandError::Deserializing)?;
+ if magic_num != 0x05 {
+ error!("error while parsing registration: magic header not 0x05, but {magic_num}");
+ return Err(CommandError::Deserializing(DesError::invalid_value(
+ serde::de::Unexpected::Unsigned(magic_num as u64),
+ &"0x05",
+ )));
+ }
+ let mut public_key = [0u8; 65];
+ data.read_exact(&mut public_key)
+ .map_err(|_| CommandError::Deserializing(serde_parse_err("PublicKey")))?;
+
+ let credential_id_len = read_byte(&mut data).map_err(CommandError::Deserializing)?;
+ let mut credential_id = vec![0u8; credential_id_len as usize];
+ data.read_exact(&mut credential_id)
+ .map_err(|_| CommandError::Deserializing(serde_parse_err("CredentialId")))?;
+
+ let cert_and_sig = parse_u2f_der_certificate(&data.get_ref()[data.position() as usize..])
+ .map_err(|err| {
+ CommandError::Deserializing(serde_parse_err(&format!(
+ "Certificate and Signature: {err:?}",
+ )))
+ })?;
+
+ let credential_ec2_key = COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &public_key)
+ .map_err(|err| {
+ CommandError::Deserializing(serde_parse_err(&format!("EC2 Key: {err:?}",)))
+ })?;
+
+ let credential_public_key = COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(credential_ec2_key),
+ };
+
+ let auth_data = AuthenticatorData {
+ rp_id_hash: rp_id_hash.clone(),
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-authenticatorMakeCredential-interoperability
+ // "Let flags be a byte whose zeroth bit (bit 0, UP) is set, and whose sixth bit
+ // (bit 6, AT) is set, and all other bits are zero (bit zero is the least
+ // significant bit)"
+ flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED,
+ counter: 0,
+ credential_data: Some(AttestedCredentialData {
+ aaguid: AAGuid::default(),
+ credential_id,
+ credential_public_key,
+ }),
+ extensions: Default::default(),
+ };
+
+ let att_stmt = AttestationStatement::FidoU2F(AttestationStatementFidoU2F::new(
+ cert_and_sig.certificate,
+ cert_and_sig.signature,
+ ));
+
+ let att_obj = AttestationObject {
+ auth_data,
+ att_stmt,
+ };
+
+ Ok(Self {
+ att_obj,
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ })
+ }
+}
+
+impl<'de> Deserialize<'de> for MakeCredentialsResult {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct MakeCredentialsResultVisitor;
+
+ impl<'de> Visitor<'de> for MakeCredentialsResultVisitor {
+ type Value = MakeCredentialsResult;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a cbor map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut format: Option<&str> = None;
+ let mut auth_data: Option<AuthenticatorData> = None;
+ let mut att_stmt: Option<AttestationStatement> = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ 1 => {
+ if format.is_some() {
+ return Err(DesError::duplicate_field("fmt (0x01)"));
+ }
+ format = Some(map.next_value()?);
+ }
+ 2 => {
+ if auth_data.is_some() {
+ return Err(DesError::duplicate_field("authData (0x02)"));
+ }
+ auth_data = Some(map.next_value()?);
+ }
+ 3 => {
+ let format =
+ format.ok_or_else(|| DesError::missing_field("fmt (0x01)"))?;
+ if att_stmt.is_some() {
+ return Err(DesError::duplicate_field("attStmt (0x03)"));
+ }
+ att_stmt = match format {
+ "none" => {
+ let map: std::collections::BTreeMap<(), ()> =
+ map.next_value()?;
+ if !map.is_empty() {
+ return Err(DesError::invalid_value(
+ Unexpected::Map,
+ &"the empty map",
+ ));
+ }
+ Some(AttestationStatement::None)
+ }
+ "packed" => Some(AttestationStatement::Packed(map.next_value()?)),
+ "fido-u2f" => {
+ Some(AttestationStatement::FidoU2F(map.next_value()?))
+ }
+ _ => {
+ return Err(DesError::custom(
+ "unknown attestation statement format",
+ ))
+ }
+ }
+ }
+ _ => continue,
+ }
+ }
+
+ let auth_data = auth_data
+ .ok_or_else(|| M::Error::custom("found no authData (0x02)".to_string()))?;
+ let att_stmt = att_stmt
+ .ok_or_else(|| M::Error::custom("found no attStmt (0x03)".to_string()))?;
+
+ Ok(MakeCredentialsResult {
+ att_obj: AttestationObject {
+ auth_data,
+ att_stmt,
+ },
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(MakeCredentialsResultVisitor)
+ }
+}
+
+impl CtapResponse for MakeCredentialsResult {}
+
+#[derive(Copy, Clone, Debug, Default, Serialize)]
+#[cfg_attr(test, derive(Deserialize))]
+pub struct MakeCredentialsOptions {
+ #[serde(rename = "rk", skip_serializing_if = "Option::is_none")]
+ pub resident_key: Option<bool>,
+ #[serde(rename = "uv", skip_serializing_if = "Option::is_none")]
+ pub user_verification: Option<bool>,
+ // TODO(MS): ctap2.1 supports user_presence, but ctap2.0 does not and tokens will error out
+ // Commands need a version-flag to know what to de/serialize and what to ignore.
+}
+
+impl MakeCredentialsOptions {
+ pub(crate) fn has_some(&self) -> bool {
+ self.resident_key.is_some() || self.user_verification.is_some()
+ }
+}
+
+pub(crate) trait UserVerification {
+ fn ask_user_verification(&self) -> bool;
+}
+
+impl UserVerification for MakeCredentialsOptions {
+ fn ask_user_verification(&self) -> bool {
+ if let Some(e) = self.user_verification {
+ e
+ } else {
+ false
+ }
+ }
+}
+
+#[derive(Debug, Default, Clone, Serialize)]
+pub struct MakeCredentialsExtensions {
+ #[serde(skip_serializing)]
+ pub cred_props: Option<bool>,
+ #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")]
+ pub cred_protect: Option<CredentialProtectionPolicy>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<bool>,
+ #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")]
+ pub min_pin_length: Option<bool>,
+}
+
+impl MakeCredentialsExtensions {
+ fn has_content(&self) -> bool {
+ self.cred_protect.is_some() || self.hmac_secret.is_some() || self.min_pin_length.is_some()
+ }
+}
+
+impl From<AuthenticationExtensionsClientInputs> for MakeCredentialsExtensions {
+ fn from(input: AuthenticationExtensionsClientInputs) -> Self {
+ Self {
+ cred_props: input.cred_props,
+ cred_protect: input.credential_protection_policy,
+ hmac_secret: input.hmac_create_secret,
+ min_pin_length: input.min_pin_length,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct MakeCredentials {
+ pub client_data_hash: ClientDataHash,
+ pub rp: RelyingParty,
+ // Note(baloo): If none -> ctap1
+ pub user: Option<PublicKeyCredentialUserEntity>,
+ pub pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ pub exclude_list: Vec<PublicKeyCredentialDescriptor>,
+
+ // https://www.w3.org/TR/webauthn/#client-extension-input
+ // The client extension input, which is a value that can be encoded in JSON,
+ // is passed from the WebAuthn Relying Party to the client in the get() or
+ // create() call, while the CBOR authenticator extension input is passed
+ // from the client to the authenticator for authenticator extensions during
+ // the processing of these calls.
+ pub extensions: MakeCredentialsExtensions,
+ pub options: MakeCredentialsOptions,
+ pub pin_uv_auth_param: Option<PinUvAuthParam>,
+ pub enterprise_attestation: Option<u64>,
+}
+
+impl MakeCredentials {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ client_data_hash: ClientDataHash,
+ rp: RelyingParty,
+ user: Option<PublicKeyCredentialUserEntity>,
+ pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ exclude_list: Vec<PublicKeyCredentialDescriptor>,
+ options: MakeCredentialsOptions,
+ extensions: MakeCredentialsExtensions,
+ ) -> Self {
+ Self {
+ client_data_hash,
+ rp,
+ user,
+ pub_cred_params,
+ exclude_list,
+ extensions,
+ options,
+ pin_uv_auth_param: None,
+ enterprise_attestation: None,
+ }
+ }
+
+ pub fn finalize_result<Dev: FidoDevice>(&self, dev: &Dev, result: &mut MakeCredentialsResult) {
+ let maybe_info = dev.get_authenticator_info();
+
+ result.attachment = match maybe_info {
+ Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform,
+ Some(_) => AuthenticatorAttachment::CrossPlatform,
+ None => AuthenticatorAttachment::Unknown,
+ };
+
+ // Handle extensions whose outputs are not encoded in the authenticator data.
+ // 1. credProps
+ // "set clientExtensionResults["credProps"]["rk"] to the value of the
+ // requireResidentKey parameter that was used in the invocation of the
+ // authenticatorMakeCredential operation."
+ // Note: a CTAP 2.0 authenticator is allowed to create a discoverable credential even
+ // if one was not requested, so there is a case in which we cannot confidently
+ // return `rk=false` here. We omit the response entirely in this case.
+ let dev_supports_rk = maybe_info.map_or(false, |info| info.options.resident_key);
+ let requested_rk = self.options.resident_key.unwrap_or(false);
+ let max_supported_version = maybe_info.map_or(AuthenticatorVersion::U2F_V2, |info| {
+ info.max_supported_version()
+ });
+ let rk_uncertain = max_supported_version == AuthenticatorVersion::FIDO_2_0
+ && dev_supports_rk
+ && !requested_rk;
+ if self.extensions.cred_props == Some(true) && !rk_uncertain {
+ result
+ .extensions
+ .cred_props
+ .get_or_insert(Default::default())
+ .rk = requested_rk;
+ }
+
+ // 2. hmac-secret
+ // The extension returns a flag in the authenticator data which we need to mirror as a
+ // client output.
+ if self.extensions.hmac_secret == Some(true) {
+ if let Some(HmacSecretResponse::Confirmed(flag)) =
+ result.att_obj.auth_data.extensions.hmac_secret
+ {
+ result.extensions.hmac_create_secret = Some(flag);
+ }
+ }
+ }
+}
+
+impl PinUvAuthCommand for MakeCredentials {
+ fn set_pin_uv_auth_param(
+ &mut self,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+ ) -> Result<(), AuthenticatorError> {
+ let mut param = None;
+ if let Some(token) = pin_uv_auth_token {
+ param = Some(
+ token
+ .derive(self.client_data_hash.as_ref())
+ .map_err(CommandError::Crypto)?,
+ );
+ }
+ self.pin_uv_auth_param = param;
+ Ok(())
+ }
+
+ fn set_uv_option(&mut self, uv: Option<bool>) {
+ self.options.user_verification = uv;
+ }
+
+ fn get_rp_id(&self) -> Option<&String> {
+ Some(&self.rp.id)
+ }
+
+ fn can_skip_user_verification(
+ &mut self,
+ info: &AuthenticatorInfo,
+ uv_req: UserVerificationRequirement,
+ ) -> bool {
+ // TODO(MS): Handle here the case where we NEED a UV, the device supports PINs, but hasn't set a PIN.
+ // For this, the user has to be prompted to set a PIN first (see https://github.com/mozilla/authenticator-rs/issues/223)
+
+ let supports_uv = info.options.user_verification == Some(true);
+ let pin_configured = info.options.client_pin == Some(true);
+
+ // CTAP 2.0 authenticators require user verification if the device is protected
+ let device_protected = supports_uv || pin_configured;
+
+ // CTAP 2.1 authenticators may allow the creation of non-discoverable credentials without
+ // user verification. This is only relevant if the relying party has not requested user
+ // verification.
+ let make_cred_uv_not_required = info.options.make_cred_uv_not_rqd == Some(true)
+ && self.options.resident_key != Some(true)
+ && uv_req == UserVerificationRequirement::Discouraged;
+
+ // Alternatively, CTAP 2.1 authenticators may require user verification regardless of the
+ // RP's requirement.
+ let always_uv = info.options.always_uv == Some(true);
+
+ !always_uv && (!device_protected || make_cred_uv_not_required)
+ }
+
+ fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> {
+ self.pin_uv_auth_param.as_ref()
+ }
+}
+
+impl Serialize for MakeCredentials {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ debug!("Serialize MakeCredentials");
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 4;
+ if !self.exclude_list.is_empty() {
+ map_len += 1;
+ }
+ if self.extensions.has_content() {
+ map_len += 1;
+ }
+ if self.options.has_some() {
+ map_len += 1;
+ }
+ if self.pin_uv_auth_param.is_some() {
+ map_len += 2;
+ }
+ if self.enterprise_attestation.is_some() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ map.serialize_entry(&0x01, &self.client_data_hash)?;
+ map.serialize_entry(&0x02, &self.rp)?;
+ map.serialize_entry(&0x03, &self.user)?;
+ map.serialize_entry(&0x04, &self.pub_cred_params)?;
+ if !self.exclude_list.is_empty() {
+ map.serialize_entry(&0x05, &self.exclude_list)?;
+ }
+ if self.extensions.has_content() {
+ map.serialize_entry(&0x06, &self.extensions)?;
+ }
+ if self.options.has_some() {
+ map.serialize_entry(&0x07, &self.options)?;
+ }
+ if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param {
+ map.serialize_entry(&0x08, &pin_uv_auth_param)?;
+ map.serialize_entry(&0x09, &pin_uv_auth_param.pin_protocol.id())?;
+ }
+ if let Some(enterprise_attestation) = self.enterprise_attestation {
+ map.serialize_entry(&0x0a, &enterprise_attestation)?;
+ }
+ map.end()
+ }
+}
+
+impl RequestCtap1 for MakeCredentials {
+ type Output = MakeCredentialsResult;
+ type AdditionalInfo = ();
+
+ fn ctap1_format(&self) -> Result<(Vec<u8>, ()), HIDError> {
+ let flags = U2F_REQUEST_USER_PRESENCE;
+
+ let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE);
+ register_data.extend_from_slice(self.client_data_hash.as_ref());
+ register_data.extend_from_slice(self.rp.hash().as_ref());
+ let cmd = U2F_REGISTER;
+ let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &register_data)?;
+
+ Ok((apdu, ()))
+ }
+
+ fn handle_response_ctap1<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ _add_info: &(),
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ if Err(ApduErrorStatus::ConditionsNotSatisfied) == status {
+ return Err(Retryable::Retry);
+ }
+ if let Err(err) = status {
+ return Err(Retryable::Error(HIDError::ApduStatus(err)));
+ }
+
+ let mut output = MakeCredentialsResult::from_ctap1(input, &self.rp.hash())
+ .map_err(|e| Retryable::Error(HIDError::Command(e)))?;
+ self.finalize_result(dev, &mut output);
+ Ok(output)
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ let mut output = dev.make_credentials(self)?;
+ self.finalize_result(dev, &mut output);
+ Ok(output)
+ }
+}
+
+impl RequestCtap2 for MakeCredentials {
+ type Output = MakeCredentialsResult;
+
+ fn command(&self) -> Command {
+ Command::MakeCredentials
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?)
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ if input.is_empty() {
+ return Err(HIDError::Command(CommandError::InputTooSmall));
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!("response status code: {:?}", status);
+ if input.len() == 1 {
+ if status.is_ok() {
+ return Err(HIDError::Command(CommandError::InputTooSmall));
+ }
+ return Err(HIDError::Command(CommandError::StatusCode(status, None)));
+ }
+
+ if status.is_ok() {
+ let mut output: MakeCredentialsResult =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ self.finalize_result(dev, &mut output);
+ Ok(output)
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(HIDError::Command(CommandError::StatusCode(
+ status,
+ Some(data),
+ )))
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ let mut output = dev.make_credentials(self)?;
+ self.finalize_result(dev, &mut output);
+ Ok(output)
+ }
+}
+
+pub(crate) fn dummy_make_credentials_cmd() -> MakeCredentials {
+ let mut req = MakeCredentials::new(
+ // Hardcoded hash of:
+ // CollectedClientData {
+ // webauthn_type: WebauthnType::Create,
+ // challenge: Challenge::new(vec![0, 1, 2, 3, 4]),
+ // origin: String::new(),
+ // cross_origin: false,
+ // token_binding: None,
+ // }
+ ClientDataHash([
+ 208, 206, 230, 252, 125, 191, 89, 154, 145, 157, 184, 251, 149, 19, 17, 38, 159, 14,
+ 183, 129, 247, 132, 28, 108, 192, 84, 74, 217, 218, 52, 21, 75,
+ ]),
+ RelyingParty::from("make.me.blink"),
+ Some(PublicKeyCredentialUserEntity {
+ id: vec![0],
+ name: Some(String::from("make.me.blink")),
+ ..Default::default()
+ }),
+ vec![PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ }],
+ vec![],
+ MakeCredentialsOptions::default(),
+ MakeCredentialsExtensions::default(),
+ );
+ // Using a zero-length pinAuth will trigger the device to blink.
+ // For CTAP1, this gets ignored anyways and we do a 'normal' register
+ // command, which also just blinks.
+ req.pin_uv_auth_param = Some(PinUvAuthParam::create_empty());
+ req
+}
+
+#[cfg(test)]
+pub mod test {
+ use super::{MakeCredentials, MakeCredentialsOptions, MakeCredentialsResult};
+ use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve};
+ use crate::ctap2::attestation::test::create_attestation_obj;
+ use crate::ctap2::attestation::{
+ AAGuid, AttestationCertificate, AttestationObject, AttestationStatement,
+ AttestationStatementFidoU2F, AttestedCredentialData, AuthenticatorData,
+ AuthenticatorDataFlags, Signature,
+ };
+ use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType};
+ use crate::ctap2::commands::{RequestCtap1, RequestCtap2};
+ use crate::ctap2::server::RpIdHash;
+ use crate::ctap2::server::{
+ AuthenticatorAttachment, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity,
+ RelyingParty,
+ };
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use crate::transport::{FidoDevice, FidoProtocol};
+ use base64::Engine;
+
+ #[test]
+ fn test_make_credentials_ctap2() {
+ let req = MakeCredentials::new(
+ CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ }
+ .hash()
+ .expect("failed to serialize client data"),
+ RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ },
+ Some(PublicKeyCredentialUserEntity {
+ id: base64::engine::general_purpose::URL_SAFE
+ .decode("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=")
+ .unwrap(),
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: Some(String::from("John P. Smith")),
+ }),
+ vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ],
+ Vec::new(),
+ MakeCredentialsOptions {
+ resident_key: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ );
+
+ let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it)
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
+ let req_serialized = req
+ .wire_format()
+ .expect("Failed to serialize MakeCredentials request");
+ assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2);
+ let make_cred_result = req
+ .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2)
+ .expect("Failed to handle CTAP2 response");
+
+ let expected = MakeCredentialsResult {
+ att_obj: create_attestation_obj(),
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ };
+
+ assert_eq!(make_cred_result, expected);
+ }
+
+ #[test]
+ fn test_make_credentials_ctap1() {
+ let req = MakeCredentials::new(
+ CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ }
+ .hash()
+ .expect("failed to serialize client data"),
+ RelyingParty::from("example.com"),
+ Some(PublicKeyCredentialUserEntity {
+ id: base64::engine::general_purpose::URL_SAFE
+ .decode("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=")
+ .unwrap(),
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: Some(String::from("John P. Smith")),
+ }),
+ vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ],
+ Vec::new(),
+ MakeCredentialsOptions {
+ resident_key: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ );
+
+ let (req_serialized, _) = req
+ .ctap1_format()
+ .expect("Failed to serialize MakeCredentials request");
+ assert_eq!(
+ req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1,
+ "\nGot: {req_serialized:X?}\nExpected: {MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1:X?}"
+ );
+ let mut device = Device::new("commands/make_credentials").unwrap(); // not really used
+ let make_cred_result = req
+ .handle_response_ctap1(
+ &mut device,
+ Ok(()),
+ &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1,
+ &(),
+ )
+ .expect("Failed to handle CTAP1 response");
+
+ let att_obj = AttestationObject {
+ auth_data: AuthenticatorData {
+ rp_id_hash: RpIdHash::from(&[
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
+ 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
+ 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47,
+ ])
+ .unwrap(),
+ flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED,
+ counter: 0,
+ credential_data: Some(AttestedCredentialData {
+ aaguid: AAGuid::default(),
+ credential_id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26,
+ 0x35, 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3,
+ 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94,
+ 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64,
+ 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38,
+ ],
+ credential_public_key: COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: vec![
+ 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76,
+ 0x6E, 0x80, 0x87, 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56,
+ 0x7F, 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90, 0xA6, 0x0E, 0x14,
+ ],
+ y: vec![
+ 0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58,
+ 0x1E, 0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22,
+ 0xF8, 0xC9, 0x70, 0x45, 0xF4, 0x61, 0x2F, 0xB2, 0x0C, 0x91,
+ ],
+ }),
+ },
+ }),
+ extensions: Default::default(),
+ },
+ att_stmt: AttestationStatement::FidoU2F(AttestationStatementFidoU2F {
+ sig: Signature(vec![
+ 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A,
+ 0x11, 0x97, 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6,
+ 0x6A, 0xE1, 0x2A, 0x99, 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00,
+ 0xE3, 0x95, 0x16, 0xAC, 0x4D, 0x61, 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15,
+ 0xA6, 0xA4, 0xD4, 0xD8, 0x4B, 0xA6, 0xD8, 0x95, 0xCB, 0x5A, 0xB7, 0xA1, 0xAA,
+ 0x7D, 0x08, 0x1D, 0xE3, 0x41, 0xFA,
+ ]),
+ attestation_cert: vec![AttestationCertificate(vec![
+ 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02,
+ 0x02, 0x04, 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48,
+ 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30,
+ 0x2A, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41,
+ 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30,
+ 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38, 0x30,
+ 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35,
+ 0x30, 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x30,
+ 0x2C, 0x31, 0x2A, 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x21, 0x59,
+ 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20,
+ 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x32, 0x34, 0x39, 0x31, 0x38, 0x32,
+ 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A,
+ 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D,
+ 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97,
+ 0x28, 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65,
+ 0xB2, 0xD5, 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4,
+ 0x76, 0xD8, 0xD1, 0xE9, 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56,
+ 0xE6, 0x21, 0x0F, 0xD4, 0x27, 0x85, 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE,
+ 0x31, 0x0F, 0x6C, 0xDB, 0x9F, 0xF4, 0xA3, 0x3B, 0x30, 0x39, 0x30, 0x22, 0x06,
+ 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xC4, 0x0A, 0x02, 0x04, 0x15, 0x31,
+ 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, 0x34, 0x2E, 0x31, 0x2E, 0x34, 0x31,
+ 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, 0x13, 0x06, 0x0B, 0x2B, 0x06,
+ 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02,
+ 0x04, 0x30, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01,
+ 0x01, 0x0B, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, 0x05, 0x22,
+ 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99, 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, 0x65,
+ 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C, 0x1C, 0x1F, 0xFB, 0x36,
+ 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92, 0xC7, 0xE6,
+ 0xE7, 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4,
+ 0x5E, 0x4A, 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A,
+ 0x29, 0x54, 0x0C, 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B,
+ 0x89, 0x62, 0xC0, 0xF4, 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16,
+ 0xB4, 0x4A, 0x96, 0xF5, 0xD3, 0x5A, 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F,
+ 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, 0xB6, 0x5C, 0x99, 0xE7, 0xEB, 0x69, 0x19,
+ 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, 0xE8, 0xF7, 0x5C, 0xCA, 0x44, 0xAA,
+ 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, 0xA8, 0x69, 0x6A, 0x6F, 0x1B,
+ 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, 0x8F, 0xDA, 0x53, 0xFA,
+ 0x4A, 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, 0xA1, 0x33, 0x9D,
+ 0x00, 0x2D, 0x15, 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, 0xF5, 0xDE,
+ 0xF9, 0x96, 0x5A, 0x37, 0x1D, 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, 0x7C,
+ 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF, 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5,
+ 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E, 0xBC, 0xAD, 0xC7, 0x4E,
+ 0x07, 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90, 0xDF, 0xEA,
+ 0xCA, 0xE6, 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9,
+ 0x5F, 0x06, 0x07, 0x33, 0xF5,
+ ])],
+ }),
+ };
+
+ let expected = MakeCredentialsResult {
+ att_obj,
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ };
+
+ assert_eq!(make_cred_result, expected);
+ }
+
+ // This includes a CTAP2 encoded attestation object that is identical to
+ // the WebAuthn encoded attestation object in `ctap2::attestation::test::SAMPLE_ATTESTATION`.
+ // Both values decode to `ctap2::attestation::test::create_attestation_obj`.
+ #[rustfmt::skip]
+ pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2: [u8; 660] = [
+ 0x00, // status = success
+ 0xa3, // map(3)
+ 0x01, // unsigned(1)
+ 0x66, // text(6)
+ 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed"
+ 0x02, // unsigned(2)
+ 0x58, 0x94, // bytes(148)
+ // authData
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, 0x27, // rp_id_hash
+ 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, 0x87, // rp_id_hash
+ 0x05, 0x1d, // rp_id_hash
+ 0x41, // authData Flags
+ 0x00, 0x00, 0x00, 0x0b, // authData counter
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, 0x7d, // AAGUID
+ 0x00, 0x10, // credential id length
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, 0x6f, // credential id
+ // credential public key
+ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, 0xc4,
+ 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, 0xaf, 0xde,
+ 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, 0xfa, 0x3a, 0x32,
+ 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, 0x59, 0x50, 0x1e, 0x4b,
+ 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, 0xa6, 0x1c,
+ 0x03, // unsigned(3)
+ 0xa3, // map(3)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x26, // -7 (ES256)
+ 0x63, // text(3)
+ 0x73, 0x69, 0x67, // "sig"
+ 0x58, 0x47, // bytes(71)
+ 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c, 0xc9, // signature
+ 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, 0xf0, 0x56, 0x12, 0x35, // ..
+ 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, // ..
+ 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, // ..
+ 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, // ..
+ 0x63, // text(3)
+ 0x78, 0x35, 0x63, // "x5c"
+ 0x81, // array(1)
+ 0x59, 0x01, 0x97, // bytes(407)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, //certificate...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b,
+ 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30,
+ 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b,
+ 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72,
+ 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17,
+ 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17,
+ 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30,
+ 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b,
+ 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72,
+ 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30,
+ 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48,
+ 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52,
+ 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf,
+ 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff,
+ 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79,
+ 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d,
+ 0x30, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a,
+ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46,
+ 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, 0x10,
+ 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, 0xda, 0x1f, 0xd2,
+ 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, 0x34, 0x45, 0xa8, 0x20,
+ 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5,
+ 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, 0xf3,
+ ];
+
+ #[rustfmt::skip]
+ pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 210] = [
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash (see client_data.rs))
+ 0xa5, // map(5)
+ 0x01, // unsigned(1) - clientDataHash
+ 0x58, 0x20, // bytes(32)
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ 0x02, // unsigned(2) - rp
+ 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name"
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x6b, // text(11)
+ 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // "example.com"
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x64, // text(4)
+ 0x41, 0x63, 0x6d, 0x65, // "Acme"
+ 0x03, // unsigned(3) - user
+ 0xa3, // map(3)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // userid
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // ...
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, // ...
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x76, // text(22)
+ 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, // "johnpsmith@example.com"
+ 0x68, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // ...
+ 0x6b, // text(11)
+ 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, // "displayName"
+ 0x6d, // text(13)
+ 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, 0x69, 0x74, 0x68, // "John P. Smith"
+ 0x04, // unsigned(4) - pubKeyCredParams
+ 0x82, // array(2)
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x26, // -7 (ES256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x39, 0x01, 0x00, // -257 (RS256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ // TODO(MS): Options seem to be parsed differently than in the example here.
+ 0x07, // unsigned(7) - options
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x72, 0x6b, // "rk"
+ 0xf5, // primitive(21)
+ ];
+
+ pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1: [u8; 73] = [
+ // CBOR Header
+ 0x0, // CLA
+ 0x1, // INS U2F_Register
+ 0x3, // P1 Flags
+ 0x0, // P2
+ 0x0, 0x0, 0x40, // Lc
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash)
+ // clientDataHash:
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ // rpIdHash:
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47, // ..
+ // Le (Ne=65536):
+ 0x0, 0x0,
+ ];
+
+ pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1: [u8; 792] = [
+ 0x05, // Reserved Byte (1 Byte)
+ // User Public Key (65 Bytes)
+ 0x04, 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76, 0x6E, 0x80, 0x87,
+ 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56, 0x7F, 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90,
+ 0xA6, 0x0E, 0x14, 0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58, 0x1E,
+ 0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22, 0xF8, 0xC9, 0x70, 0x45, 0xF4,
+ 0x61, 0x2F, 0xB2, 0x0C, 0x91, // ...
+ 0x40, // Key Handle Length (1 Byte)
+ // Key Handle (Key Handle Length Bytes)
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38, // ...
+ // X.509 Cert (Variable length Cert)
+ 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x04,
+ 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01,
+ 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30, 0x2A, 0x06, 0x03, 0x55, 0x04, 0x03,
+ 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6F,
+ 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x34, 0x35,
+ 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38,
+ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35, 0x30,
+ 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x30, 0x2C, 0x31, 0x2A,
+ 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x21, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F,
+ 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20,
+ 0x32, 0x34, 0x39, 0x31, 0x38, 0x32, 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30,
+ 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48,
+ 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97,
+ 0x28, 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65, 0xB2, 0xD5,
+ 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4, 0x76, 0xD8, 0xD1, 0xE9,
+ 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56, 0xE6, 0x21, 0x0F, 0xD4, 0x27, 0x85,
+ 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE, 0x31, 0x0F, 0x6C, 0xDB, 0x9F, 0xF4, 0xA3, 0x3B,
+ 0x30, 0x39, 0x30, 0x22, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xC4, 0x0A, 0x02,
+ 0x04, 0x15, 0x31, 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, 0x34, 0x2E, 0x31, 0x2E, 0x34,
+ 0x31, 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, 0x13, 0x06, 0x0B, 0x2B, 0x06, 0x01,
+ 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02, 0x04, 0x30, 0x30,
+ 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x03,
+ 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, 0x05, 0x22, 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99,
+ 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, 0x65, 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C,
+ 0x1C, 0x1F, 0xFB, 0x36, 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92,
+ 0xC7, 0xE6, 0xE7, 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4,
+ 0x5E, 0x4A, 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A, 0x29, 0x54,
+ 0x0C, 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B, 0x89, 0x62, 0xC0, 0xF4,
+ 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16, 0xB4, 0x4A, 0x96, 0xF5, 0xD3, 0x5A,
+ 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F, 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, 0xB6, 0x5C,
+ 0x99, 0xE7, 0xEB, 0x69, 0x19, 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, 0xE8, 0xF7, 0x5C,
+ 0xCA, 0x44, 0xAA, 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, 0xA8, 0x69, 0x6A, 0x6F,
+ 0x1B, 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, 0x8F, 0xDA, 0x53, 0xFA, 0x4A,
+ 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, 0xA1, 0x33, 0x9D, 0x00, 0x2D, 0x15,
+ 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, 0xF5, 0xDE, 0xF9, 0x96, 0x5A, 0x37, 0x1D,
+ 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, 0x7C, 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF,
+ 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5, 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E,
+ 0xBC, 0xAD, 0xC7, 0x4E, 0x07, 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90,
+ 0xDF, 0xEA, 0xCA, 0xE6, 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9,
+ 0x5F, 0x06, 0x07, 0x33, 0xF5, // ...
+ // Signature (variable Length)
+ 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A, 0x11, 0x97,
+ 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6, 0x6A, 0xE1, 0x2A, 0x99,
+ 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00, 0xE3, 0x95, 0x16, 0xAC, 0x4D, 0x61,
+ 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15, 0xA6, 0xA4, 0xD4, 0xD8, 0x4B, 0xA6, 0xD8, 0x95,
+ 0xCB, 0x5A, 0xB7, 0xA1, 0xAA, 0x7D, 0x08, 0x1D, 0xE3, 0x41, 0xFA, // ...
+ ];
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/mod.rs b/third_party/rust/authenticator/src/ctap2/commands/mod.rs
new file mode 100644
index 0000000000..12990122d9
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/mod.rs
@@ -0,0 +1,477 @@
+use crate::crypto::{CryptoError, PinUvAuthParam, PinUvAuthToken};
+use crate::ctap2::commands::client_pin::{GetPinRetries, GetUvRetries, PinError};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::ctap2::server::UserVerificationRequirement;
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use serde_cbor::{error::Error as CborError, Value};
+use serde_json as json;
+use std::error::Error as StdErrorT;
+use std::fmt;
+
+pub mod authenticator_config;
+pub mod bio_enrollment;
+pub mod client_pin;
+pub mod credential_management;
+pub mod get_assertion;
+pub mod get_info;
+pub mod get_next_assertion;
+pub mod get_version;
+pub mod make_credentials;
+pub mod reset;
+pub mod selection;
+
+/// Retryable wraps an error type and may ask manager to retry sending a
+/// command, this is useful for ctap1 where token will reply with "condition not
+/// sufficient" because user needs to press the button.
+#[derive(Debug)]
+pub enum Retryable<T> {
+ Retry,
+ Error(T),
+}
+
+impl<T> Retryable<T> {
+ pub fn is_retry(&self) -> bool {
+ matches!(*self, Retryable::Retry)
+ }
+
+ pub fn is_error(&self) -> bool {
+ !self.is_retry()
+ }
+}
+
+impl<T> From<T> for Retryable<T> {
+ fn from(e: T) -> Self {
+ Retryable::Error(e)
+ }
+}
+
+pub trait RequestCtap1: fmt::Debug {
+ type Output: CtapResponse;
+ // E.g.: For GetAssertion, which key-handle is currently being tested
+ type AdditionalInfo;
+
+ /// Serializes a request into FIDO v1.x / CTAP1 / U2F format.
+ ///
+ /// See [`crate::u2ftypes::CTAP1RequestAPDU::serialize()`]
+ fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError>;
+
+ /// Deserializes a response from FIDO v1.x / CTAP1 / U2Fv2 format.
+ fn handle_response_ctap1<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ add_info: &Self::AdditionalInfo,
+ ) -> Result<Self::Output, Retryable<HIDError>>;
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError>;
+}
+
+pub trait RequestCtap2: fmt::Debug {
+ type Output: CtapResponse;
+
+ fn command(&self) -> Command;
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError>;
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>;
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError>;
+}
+
+// Sadly, needs to be 'static to enable us in tests to collect them in a Vec
+// but all of them are 'static, so this is currently no problem.
+pub trait CtapResponse: std::fmt::Debug + 'static {}
+
+#[derive(Debug, Clone)]
+pub enum PinUvAuthResult {
+ /// Request is CTAP1 and does not need PinUvAuth
+ RequestIsCtap1,
+ /// Device is not capable of CTAP2
+ DeviceIsCtap1,
+ /// Device does not support UV or PINs
+ NoAuthTypeSupported,
+ /// Request doesn't want user verification (uv = "discouraged")
+ NoAuthRequired,
+ /// Device is CTAP2.0 and has internal UV capability
+ UsingInternalUv,
+ /// Successfully established PinUvAuthToken via GetPinToken (CTAP2.0)
+ SuccessGetPinToken(PinUvAuthToken),
+ /// Successfully established PinUvAuthToken via UV (CTAP2.1)
+ SuccessGetPinUvAuthTokenUsingUvWithPermissions(PinUvAuthToken),
+ /// Successfully established PinUvAuthToken via Pin (CTAP2.1)
+ SuccessGetPinUvAuthTokenUsingPinWithPermissions(PinUvAuthToken),
+}
+
+impl PinUvAuthResult {
+ pub(crate) fn get_pin_uv_auth_token(&self) -> Option<PinUvAuthToken> {
+ match self {
+ PinUvAuthResult::RequestIsCtap1
+ | PinUvAuthResult::DeviceIsCtap1
+ | PinUvAuthResult::NoAuthTypeSupported
+ | PinUvAuthResult::NoAuthRequired
+ | PinUvAuthResult::UsingInternalUv => None,
+ PinUvAuthResult::SuccessGetPinToken(token) => Some(token.clone()),
+ PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(token) => {
+ Some(token.clone())
+ }
+ PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(token) => {
+ Some(token.clone())
+ }
+ }
+ }
+}
+
+/// Helper-trait to determine pin_uv_auth_param from PIN or UV.
+pub(crate) trait PinUvAuthCommand: RequestCtap2 {
+ fn set_pin_uv_auth_param(
+ &mut self,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+ ) -> Result<(), AuthenticatorError>;
+ fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam>;
+ fn set_uv_option(&mut self, uv: Option<bool>);
+ fn get_rp_id(&self) -> Option<&String>;
+ fn can_skip_user_verification(
+ &mut self,
+ info: &AuthenticatorInfo,
+ uv_req: UserVerificationRequirement,
+ ) -> bool;
+}
+
+pub(crate) fn repackage_pin_errors<D: FidoDevice>(
+ dev: &mut D,
+ error: HIDError,
+) -> AuthenticatorError {
+ match error {
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)) => {
+ // If the given PIN was wrong, determine no. of left retries
+ let cmd = GetPinRetries::new();
+ // Treat any error as if the device returned a valid response without a pinRetries
+ // field.
+ let resp = dev.send_cbor(&cmd).unwrap_or_default();
+ AuthenticatorError::PinError(PinError::InvalidPin(resp.pin_retries))
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthBlocked, _)) => {
+ AuthenticatorError::PinError(PinError::PinAuthBlocked)
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinBlocked, _)) => {
+ AuthenticatorError::PinError(PinError::PinBlocked)
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _)) => {
+ AuthenticatorError::PinError(PinError::PinRequired)
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _)) => {
+ AuthenticatorError::PinError(PinError::PinNotSet)
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) => {
+ AuthenticatorError::PinError(PinError::PinAuthInvalid)
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::UvInvalid, _)) => {
+ // If the internal UV failed, determine no. of left retries
+ let cmd = GetUvRetries::new();
+ // Treat any error as if the device returned a valid response without a uvRetries
+ // field.
+ let resp = dev.send_cbor(&cmd).unwrap_or_default();
+ AuthenticatorError::PinError(PinError::InvalidUv(resp.uv_retries))
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) => {
+ AuthenticatorError::PinError(PinError::UvBlocked)
+ }
+ // TODO(MS): Add "PinPolicyViolated"
+ err => AuthenticatorError::HIDError(err),
+ }
+}
+
+// Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api
+// and: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticator-api
+#[repr(u8)]
+#[derive(Debug, PartialEq, Clone)]
+pub enum Command {
+ MakeCredentials = 0x01,
+ GetAssertion = 0x02,
+ GetInfo = 0x04,
+ ClientPin = 0x06,
+ Reset = 0x07,
+ GetNextAssertion = 0x08,
+ BioEnrollment = 0x09,
+ CredentialManagement = 0x0A,
+ Selection = 0x0B,
+ AuthenticatorConfig = 0x0D,
+ BioEnrollmentPreview = 0x40,
+ CredentialManagementPreview = 0x41,
+}
+
+#[derive(Debug)]
+pub enum StatusCode {
+ /// Indicates successful response.
+ OK,
+ /// The command is not a valid CTAP command.
+ InvalidCommand,
+ /// The command included an invalid parameter.
+ InvalidParameter,
+ /// Invalid message or item length.
+ InvalidLength,
+ /// Invalid message sequencing.
+ InvalidSeq,
+ /// Message timed out.
+ Timeout,
+ /// Channel busy.
+ ChannelBusy,
+ /// Command requires channel lock.
+ LockRequired,
+ /// Command not allowed on this cid.
+ InvalidChannel,
+ /// Invalid/unexpected CBOR error.
+ CBORUnexpectedType,
+ /// Error when parsing CBOR.
+ InvalidCBOR,
+ /// Missing non-optional parameter.
+ MissingParameter,
+ /// Limit for number of items exceeded.
+ LimitExceeded,
+ /// Unsupported extension.
+ UnsupportedExtension,
+ /// Valid credential found in the exclude list.
+ CredentialExcluded,
+ /// Processing (Lengthy operation is in progress).
+ Processing,
+ /// Credential not valid for the authenticator.
+ InvalidCredential,
+ /// Authentication is waiting for user interaction.
+ UserActionPending,
+ /// Processing, lengthy operation is in progress.
+ OperationPending,
+ /// No request is pending.
+ NoOperations,
+ /// Authenticator does not support requested algorithm.
+ UnsupportedAlgorithm,
+ /// Not authorized for requested operation.
+ OperationDenied,
+ /// Internal key storage is full.
+ KeyStoreFull,
+ /// No outstanding operations.
+ NoOperationPending,
+ /// Unsupported option.
+ UnsupportedOption,
+ /// Not a valid option for current operation.
+ InvalidOption,
+ /// Pending keep alive was cancelled.
+ KeepaliveCancel,
+ /// No valid credentials provided.
+ NoCredentials,
+ /// Timeout waiting for user interaction.
+ UserActionTimeout,
+ /// Continuation command, such as, authenticatorGetNextAssertion not
+ /// allowed.
+ NotAllowed,
+ /// PIN Invalid.
+ PinInvalid,
+ /// PIN Blocked.
+ PinBlocked,
+ /// PIN authentication,pinAuth, verification failed.
+ PinAuthInvalid,
+ /// PIN authentication,pinAuth, blocked. Requires power recycle to reset.
+ PinAuthBlocked,
+ /// No PIN has been set.
+ PinNotSet,
+ /// PIN is required for the selected operation.
+ PinRequired,
+ /// PIN policy violation. Currently only enforces minimum length.
+ PinPolicyViolation,
+ /// pinToken expired on authenticator.
+ PinTokenExpired,
+ /// Authenticator cannot handle this request due to memory constraints.
+ RequestTooLarge,
+ /// The current operation has timed out.
+ ActionTimeout,
+ /// User presence is required for the requested operation.
+ UpRequired,
+ /// built-in user verification is disabled.
+ UvBlocked,
+ /// A checksum did not match.
+ IntegrityFailure,
+ /// The requested subcommand is either invalid or not implemented.
+ InvalidSubcommand,
+ /// built-in user verification unsuccessful. The platform SHOULD retry.
+ UvInvalid,
+ /// The permissions parameter contains an unauthorized permission.
+ UnauthorizedPermission,
+ /// Other unspecified error.
+ Other,
+
+ /// Unknown status.
+ Unknown(u8),
+}
+
+impl StatusCode {
+ fn is_ok(&self) -> bool {
+ matches!(*self, StatusCode::OK)
+ }
+
+ fn device_busy(&self) -> bool {
+ matches!(*self, StatusCode::ChannelBusy)
+ }
+}
+
+impl From<u8> for StatusCode {
+ fn from(value: u8) -> StatusCode {
+ match value {
+ 0x00 => StatusCode::OK,
+ 0x01 => StatusCode::InvalidCommand,
+ 0x02 => StatusCode::InvalidParameter,
+ 0x03 => StatusCode::InvalidLength,
+ 0x04 => StatusCode::InvalidSeq,
+ 0x05 => StatusCode::Timeout,
+ 0x06 => StatusCode::ChannelBusy,
+ 0x0A => StatusCode::LockRequired,
+ 0x0B => StatusCode::InvalidChannel,
+ 0x11 => StatusCode::CBORUnexpectedType,
+ 0x12 => StatusCode::InvalidCBOR,
+ 0x14 => StatusCode::MissingParameter,
+ 0x15 => StatusCode::LimitExceeded,
+ 0x16 => StatusCode::UnsupportedExtension,
+ 0x19 => StatusCode::CredentialExcluded,
+ 0x21 => StatusCode::Processing,
+ 0x22 => StatusCode::InvalidCredential,
+ 0x23 => StatusCode::UserActionPending,
+ 0x24 => StatusCode::OperationPending,
+ 0x25 => StatusCode::NoOperations,
+ 0x26 => StatusCode::UnsupportedAlgorithm,
+ 0x27 => StatusCode::OperationDenied,
+ 0x28 => StatusCode::KeyStoreFull,
+ 0x2A => StatusCode::NoOperationPending,
+ 0x2B => StatusCode::UnsupportedOption,
+ 0x2C => StatusCode::InvalidOption,
+ 0x2D => StatusCode::KeepaliveCancel,
+ 0x2E => StatusCode::NoCredentials,
+ 0x2f => StatusCode::UserActionTimeout,
+ 0x30 => StatusCode::NotAllowed,
+ 0x31 => StatusCode::PinInvalid,
+ 0x32 => StatusCode::PinBlocked,
+ 0x33 => StatusCode::PinAuthInvalid,
+ 0x34 => StatusCode::PinAuthBlocked,
+ 0x35 => StatusCode::PinNotSet,
+ 0x36 => StatusCode::PinRequired,
+ 0x37 => StatusCode::PinPolicyViolation,
+ 0x38 => StatusCode::PinTokenExpired,
+ 0x39 => StatusCode::RequestTooLarge,
+ 0x3A => StatusCode::ActionTimeout,
+ 0x3B => StatusCode::UpRequired,
+ 0x3C => StatusCode::UvBlocked,
+ 0x3D => StatusCode::IntegrityFailure,
+ 0x3E => StatusCode::InvalidSubcommand,
+ 0x3F => StatusCode::UvInvalid,
+ 0x40 => StatusCode::UnauthorizedPermission,
+ 0x7F => StatusCode::Other,
+ othr => StatusCode::Unknown(othr),
+ }
+ }
+}
+
+#[cfg(test)]
+impl From<StatusCode> for u8 {
+ fn from(v: StatusCode) -> u8 {
+ match v {
+ StatusCode::OK => 0x00,
+ StatusCode::InvalidCommand => 0x01,
+ StatusCode::InvalidParameter => 0x02,
+ StatusCode::InvalidLength => 0x03,
+ StatusCode::InvalidSeq => 0x04,
+ StatusCode::Timeout => 0x05,
+ StatusCode::ChannelBusy => 0x06,
+ StatusCode::LockRequired => 0x0A,
+ StatusCode::InvalidChannel => 0x0B,
+ StatusCode::CBORUnexpectedType => 0x11,
+ StatusCode::InvalidCBOR => 0x12,
+ StatusCode::MissingParameter => 0x14,
+ StatusCode::LimitExceeded => 0x15,
+ StatusCode::UnsupportedExtension => 0x16,
+ StatusCode::CredentialExcluded => 0x19,
+ StatusCode::Processing => 0x21,
+ StatusCode::InvalidCredential => 0x22,
+ StatusCode::UserActionPending => 0x23,
+ StatusCode::OperationPending => 0x24,
+ StatusCode::NoOperations => 0x25,
+ StatusCode::UnsupportedAlgorithm => 0x26,
+ StatusCode::OperationDenied => 0x27,
+ StatusCode::KeyStoreFull => 0x28,
+ StatusCode::NoOperationPending => 0x2A,
+ StatusCode::UnsupportedOption => 0x2B,
+ StatusCode::InvalidOption => 0x2C,
+ StatusCode::KeepaliveCancel => 0x2D,
+ StatusCode::NoCredentials => 0x2E,
+ StatusCode::UserActionTimeout => 0x2f,
+ StatusCode::NotAllowed => 0x30,
+ StatusCode::PinInvalid => 0x31,
+ StatusCode::PinBlocked => 0x32,
+ StatusCode::PinAuthInvalid => 0x33,
+ StatusCode::PinAuthBlocked => 0x34,
+ StatusCode::PinNotSet => 0x35,
+ StatusCode::PinRequired => 0x36,
+ StatusCode::PinPolicyViolation => 0x37,
+ StatusCode::PinTokenExpired => 0x38,
+ StatusCode::RequestTooLarge => 0x39,
+ StatusCode::ActionTimeout => 0x3A,
+ StatusCode::UpRequired => 0x3B,
+ StatusCode::UvBlocked => 0x3C,
+ StatusCode::IntegrityFailure => 0x3D,
+ StatusCode::InvalidSubcommand => 0x3E,
+ StatusCode::UvInvalid => 0x3F,
+ StatusCode::UnauthorizedPermission => 0x40,
+ StatusCode::Other => 0x7F,
+
+ StatusCode::Unknown(othr) => othr,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum CommandError {
+ InputTooSmall,
+ MissingRequiredField(&'static str),
+ Deserializing(CborError),
+ Serializing(CborError),
+ StatusCode(StatusCode, Option<Value>),
+ Json(json::Error),
+ Crypto(CryptoError),
+ UnsupportedPinProtocol,
+}
+
+impl fmt::Display for CommandError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"),
+ CommandError::MissingRequiredField(field) => {
+ write!(f, "CommandError: Missing required field {field}")
+ }
+ CommandError::Deserializing(ref e) => {
+ write!(f, "CommandError: Error while parsing: {e}")
+ }
+ CommandError::Serializing(ref e) => {
+ write!(f, "CommandError: Error while serializing: {e}")
+ }
+ CommandError::StatusCode(ref code, ref value) => {
+ write!(f, "CommandError: Unexpected code: {code:?} ({value:?})")
+ }
+ CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {e}"),
+ CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {e:?}"),
+ CommandError::UnsupportedPinProtocol => {
+ write!(f, "CommandError: Pin protocol is not supported")
+ }
+ }
+ }
+}
+
+impl StdErrorT for CommandError {}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/reset.rs b/third_party/rust/authenticator/src/ctap2/commands/reset.rs
new file mode 100644
index 0000000000..a1006800b5
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/reset.rs
@@ -0,0 +1,123 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::transport::errors::HIDError;
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug, Default)]
+pub struct Reset {}
+
+impl RequestCtap2 for Reset {
+ type Output = ();
+
+ fn command(&self) -> Command {
+ Command::Reset
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if status.is_ok() {
+ Ok(())
+ } else {
+ let msg = if input.len() > 1 {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Some(data)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, msg).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ dev.reset(self)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::HIDCmd;
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol};
+ use rand::{thread_rng, RngCore};
+ use serde_cbor::{de::from_slice, Value};
+
+ fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> {
+ let mut device = Device::new("commands/Reset").unwrap();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
+ // ctap2 request
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
+ msg.extend(vec![0x07]); // authenticatorReset
+ device.add_write(&msg, 0);
+
+ // ctap2 response
+ let len = 0x1 + add.len() as u8;
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt
+ msg.push(cmd); // Status code
+ msg.extend(add); // + maybe additional data
+ device.add_read(&msg, 0);
+
+ device.send_cbor(&Reset {})
+ }
+
+ #[test]
+ fn test_select_ctap2_only() {
+ // Test, if we can parse the status codes specified by the spec
+
+ // Ok()
+ issue_command_and_get_response(0, &[]).expect("Unexpected error");
+
+ // Denied by the user
+ let response = issue_command_and_get_response(0x27, &[]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None))
+ ));
+
+ // Timeout
+ let response = issue_command_and_get_response(0x2F, &[]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(
+ StatusCode::UserActionTimeout,
+ None
+ ))
+ ));
+
+ // Unexpected error with more random CBOR-data
+ let add_data = vec![
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ ];
+ let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!");
+ match response {
+ HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => {
+ let expected: Value = from_slice(&add_data).unwrap();
+ assert_eq!(d, expected)
+ }
+ e => panic!("Not the expected response: {:?}", e),
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/selection.rs b/third_party/rust/authenticator/src/ctap2/commands/selection.rs
new file mode 100644
index 0000000000..4e9fc5213e
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/selection.rs
@@ -0,0 +1,123 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::transport::errors::HIDError;
+use crate::transport::{FidoDevice, VirtualFidoDevice};
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug, Default)]
+pub struct Selection {}
+
+impl RequestCtap2 for Selection {
+ type Output = ();
+
+ fn command(&self) -> Command {
+ Command::Selection
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if status.is_ok() {
+ Ok(())
+ } else {
+ let msg = if input.len() > 1 {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Some(data)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, msg).into())
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ dev.selection(self)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::HIDCmd;
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol};
+ use rand::{thread_rng, RngCore};
+ use serde_cbor::{de::from_slice, Value};
+
+ fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> {
+ let mut device = Device::new("commands/selection").unwrap();
+ assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
+ // ctap2 request
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
+ msg.extend(vec![0x0B]); // authenticatorSelection
+ device.add_write(&msg, 0);
+
+ // ctap2 response
+ let len = 0x1 + add.len() as u8;
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt
+ msg.push(cmd); // Status code
+ msg.extend(add); // + maybe additional data
+ device.add_read(&msg, 0);
+
+ device.send_cbor(&Selection {})
+ }
+
+ #[test]
+ fn test_select_ctap2_only() {
+ // Test, if we can parse the status codes specified by the spec
+
+ // Ok()
+ issue_command_and_get_response(0, &[]).expect("Unexpected error");
+
+ // Denied by the user
+ let response = issue_command_and_get_response(0x27, &[]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None))
+ ));
+
+ // Timeout
+ let response = issue_command_and_get_response(0x2F, &[]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(
+ StatusCode::UserActionTimeout,
+ None
+ ))
+ ));
+
+ // Unexpected error with more random CBOR-data
+ let add_data = vec![
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ ];
+ let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!");
+ match response {
+ HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => {
+ let expected: Value = from_slice(&add_data).unwrap();
+ assert_eq!(d, expected)
+ }
+ e => panic!("Not the expected response: {:?}", e),
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/mod.rs b/third_party/rust/authenticator/src/ctap2/mod.rs
new file mode 100644
index 0000000000..bc45ceb9eb
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/mod.rs
@@ -0,0 +1,1518 @@
+pub mod attestation;
+pub mod client_data;
+#[allow(dead_code)] // TODO(MS): Remove me asap
+pub mod commands;
+pub mod preflight;
+pub mod server;
+pub(crate) mod utils;
+
+use crate::authenticatorservice::{RegisterArgs, SignArgs};
+use crate::crypto::COSEAlgorithm;
+use crate::ctap2::client_data::ClientDataHash;
+use crate::ctap2::commands::authenticator_config::{
+ AuthConfigCommand, AuthConfigResult, AuthenticatorConfig,
+};
+use crate::ctap2::commands::bio_enrollment::{
+ BioEnrollment, BioEnrollmentCommand, BioEnrollmentResult, FingerprintSensorInfo,
+};
+use crate::ctap2::commands::client_pin::{
+ ChangeExistingPin, Pin, PinError, PinUvAuthTokenPermission, SetNewPin,
+};
+use crate::ctap2::commands::credential_management::{
+ CredManagementCommand, CredentialList, CredentialListEntry, CredentialManagement,
+ CredentialManagementResult, CredentialRpListEntry,
+};
+use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions};
+use crate::ctap2::commands::make_credentials::{
+ dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsOptions,
+};
+use crate::ctap2::commands::reset::Reset;
+use crate::ctap2::commands::{
+ repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, RequestCtap2, StatusCode,
+};
+use crate::ctap2::preflight::{
+ do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2,
+ silently_discover_credentials,
+};
+use crate::ctap2::server::{
+ CredentialProtectionPolicy, RelyingParty, ResidentKeyRequirement, UserVerificationRequirement,
+};
+use crate::errors::{AuthenticatorError, UnsupportedOption};
+use crate::statecallback::StateCallback;
+use crate::status_update::{send_status, BioEnrollmentCmd, CredManagementCmd, InteractiveUpdate};
+use crate::transport::device_selector::{Device, DeviceSelectorEvent};
+use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol};
+use crate::{ManageResult, ResetResult, StatusPinUv, StatusUpdate};
+use std::sync::mpsc::{channel, RecvError, Sender};
+use std::thread;
+use std::time::Duration;
+
+use self::commands::get_info::AuthenticatorVersion;
+
+macro_rules! unwrap_option {
+ ($item: expr, $callback: expr) => {
+ match $item {
+ Some(r) => r,
+ None => {
+ $callback.call(Err(AuthenticatorError::Platform));
+ return false;
+ }
+ }
+ };
+}
+
+macro_rules! unwrap_result {
+ ($item: expr, $callback: expr) => {
+ match $item {
+ Ok(r) => r,
+ Err(e) => {
+ $callback.call(Err(e.into()));
+ return false;
+ }
+ }
+ };
+}
+
+macro_rules! handle_errors {
+ ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr) => {
+ let mut _dummy_skip_puap = false;
+ let mut _dummy_cached_puat = false;
+ handle_errors!(
+ $error,
+ $status,
+ $callback,
+ $pin_uv_auth_result,
+ $skip_uv,
+ _dummy_skip_puap,
+ _dummy_cached_puat
+ )
+ };
+ ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr) => {
+ let mut _dummy_cached_puat = false;
+ handle_errors!(
+ $error,
+ $status,
+ $callback,
+ $pin_uv_auth_result,
+ $skip_uv,
+ $skip_puap,
+ _dummy_cached_puat
+ )
+ };
+ ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr, $cached_puat: expr) => {
+ match $error {
+ HIDError::Command(CommandError::StatusCode(StatusCode::ChannelBusy, _)) => {
+ // Channel busy. Client SHOULD retry the request after a short delay.
+ thread::sleep(Duration::from_millis(100));
+ continue;
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _))
+ | HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _))
+ if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) =>
+ {
+ // This should only happen for CTAP2.0 tokens that use internal UV and failed
+ // (e.g. wrong fingerprint used), while doing GetAssertion or MakeCredentials.
+ send_status(
+ &$status,
+ StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)),
+ );
+ $skip_puap = false;
+ continue;
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _))
+ if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) =>
+ {
+ // This should only happen for CTAP2.0 tokens that use internal UV and failed
+ // repeatedly, so that we have to fall back to PINs
+ $skip_uv = true;
+ $skip_puap = false;
+ continue;
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _))
+ if matches!(
+ $pin_uv_auth_result,
+ PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(..)
+ ) =>
+ {
+ // This should only happen for CTAP2.1 tokens that use internal UV and failed
+ // repeatedly, so that we have to fall back to PINs
+ $skip_uv = true;
+ $skip_puap = false;
+ continue;
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::CredentialExcluded, _)) => {
+ $callback.call(Err(AuthenticatorError::CredentialExcluded));
+ break;
+ }
+ HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _))
+ if $cached_puat =>
+ {
+ // We used the cached PUAT, but it was invalid. So we just try again
+ // without the cached one and potentially trigger a new PIN/UV entry from
+ // the user. This happens e.g. if the PUAT expires, or we get an all-zeros
+ // PUAT from outside for whatever reason, etc.
+ $cached_puat = false;
+ $skip_puap = false;
+ continue;
+ }
+ e => {
+ warn!("error happened: {e}");
+ $callback.call(Err(AuthenticatorError::HIDError(e)));
+ break;
+ }
+ }
+ };
+}
+
+fn ask_user_for_pin(
+ was_invalid: bool,
+ retries: Option<u8>,
+ status: &Sender<StatusUpdate>,
+) -> Result<Pin, AuthenticatorError> {
+ info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply");
+ let (tx, rx) = channel();
+ if was_invalid {
+ send_status(
+ status,
+ crate::StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx, retries)),
+ );
+ } else {
+ send_status(
+ status,
+ crate::StatusUpdate::PinUvError(StatusPinUv::PinRequired(tx)),
+ );
+ }
+ match rx.recv() {
+ Ok(pin) => Ok(pin),
+ Err(RecvError) => {
+ // recv() can only fail, if the other side is dropping the Sender.
+ info!("Callback dropped the channel. Aborting.");
+ Err(AuthenticatorError::CancelledByUser)
+ }
+ }
+}
+
+/// Try to fetch PinUvAuthToken from the device and derive from it PinUvAuthParam.
+/// Prefer UV, fallback to PIN.
+/// Prefer newer pinUvAuth-methods, if supported by the device.
+fn get_pin_uv_auth_param<Dev: FidoDevice, T: PinUvAuthCommand + RequestCtap2>(
+ cmd: &mut T,
+ dev: &mut Dev,
+ permission: PinUvAuthTokenPermission,
+ skip_uv: bool,
+ uv_req: UserVerificationRequirement,
+ alive: &dyn Fn() -> bool,
+ pin: &Option<Pin>,
+) -> Result<PinUvAuthResult, AuthenticatorError> {
+ // CTAP 2.1 is very specific that the request should either include pinUvAuthParam
+ // OR uv=true, but not both at the same time. We now have to decide which (if either)
+ // to send. We may omit both values. Will never send an explicit uv=false, because
+ // a) this is the default, and
+ // b) some CTAP 2.0 authenticators return UnsupportedOption when uv=false.
+
+ // We ensure both pinUvAuthParam and uv are not set to start.
+ cmd.set_pin_uv_auth_param(None)?;
+ cmd.set_uv_option(None);
+
+ // Skip user verification if we're using CTAP1 or if the device does not support CTAP2.
+ let info = match (dev.get_protocol(), dev.get_authenticator_info()) {
+ (FidoProtocol::CTAP2, Some(info)) => info,
+ _ => return Ok(PinUvAuthResult::DeviceIsCtap1),
+ };
+
+ // Only use UV, if the device supports it and we don't skip it
+ // which happens as a fallback, if UV-usage failed too many times
+ // Note: In theory, we could also repeatedly query GetInfo here and check
+ // if uv is set to Some(true), as tokens should set it to Some(false)
+ // if UV is blocked (too many failed attempts). But the CTAP2.0-spec is
+ // vague and I don't trust all tokens to implement it that way. So we
+ // keep track of it ourselves, using `skip_uv`.
+ let supports_uv = info.options.user_verification == Some(true);
+ let supports_pin = info.options.client_pin.is_some();
+ let pin_configured = info.options.client_pin == Some(true);
+
+ // Check if the combination of device-protection and request-options
+ // are allowing for 'discouraged', meaning no auth required.
+ if cmd.can_skip_user_verification(info, uv_req) {
+ return Ok(PinUvAuthResult::NoAuthRequired);
+ }
+
+ // Device does not support any (remaining) auth-method
+ if (skip_uv || !supports_uv) && !supports_pin {
+ if supports_uv && uv_req == UserVerificationRequirement::Required {
+ // We should always set the uv option in the Required case, but the CTAP 2.1 spec
+ // says 'Platforms MUST NOT include the "uv" option key if the authenticator does
+ // not support built-in user verification.' This is to work around some CTAP 2.0
+ // authenticators which incorrectly error out with CTAP2_ERR_UNSUPPORTED_OPTION
+ // when the "uv" option is set. The RP that requested UV will (hopefully) reject our
+ // response in the !supports_uv case.
+ cmd.set_uv_option(Some(true));
+ }
+ return Ok(PinUvAuthResult::NoAuthTypeSupported);
+ }
+
+ // Device supports PINs, but a PIN is not configured. Signal that we
+ // can complete the operation if the user sets a PIN first.
+ if (skip_uv || !supports_uv) && !pin_configured {
+ return Err(AuthenticatorError::PinError(PinError::PinNotSet));
+ }
+
+ if info.options.pin_uv_auth_token == Some(true) {
+ if !skip_uv && supports_uv {
+ // CTAP 2.1 - UV
+ let pin_auth_token = dev
+ .get_pin_uv_auth_token_using_uv_with_permissions(permission, cmd.get_rp_id(), alive)
+ .map_err(|e| repackage_pin_errors(dev, e))?;
+ cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?;
+ Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(pin_auth_token))
+ } else {
+ // CTAP 2.1 - PIN
+ // We did not take the `!skip_uv && supports_uv` branch, so we have
+ // `(skip_uv || !supports_uv)`. Moreover we did not exit early in the
+ // `(skip_uv || !supports_uv) && !pin_configured` case. So we have
+ // `pin_configured`.
+ let pin_auth_token = dev
+ .get_pin_uv_auth_token_using_pin_with_permissions(
+ pin,
+ permission,
+ cmd.get_rp_id(),
+ alive,
+ )
+ .map_err(|e| repackage_pin_errors(dev, e))?;
+ cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?;
+ Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(pin_auth_token))
+ }
+ } else {
+ // CTAP 2.0 fallback
+ if !skip_uv && supports_uv && pin.is_none() {
+ // If the device supports internal user-verification (e.g. fingerprints),
+ // skip PIN-stuff
+
+ // We may need the shared secret for HMAC-extension, so we
+ // have to establish one
+ if info.supports_hmac_secret() {
+ let _shared_secret = dev.establish_shared_secret(alive)?;
+ }
+ // CTAP 2.1, Section 6.1.1, Step 1.1.2.1.2.
+ cmd.set_uv_option(Some(true));
+ return Ok(PinUvAuthResult::UsingInternalUv);
+ }
+
+ let pin_auth_token = dev
+ .get_pin_token(pin, alive)
+ .map_err(|e| repackage_pin_errors(dev, e))?;
+ cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?;
+ Ok(PinUvAuthResult::SuccessGetPinToken(pin_auth_token))
+ }
+}
+
+/// PUAP, as per spec: PinUvAuthParam
+/// Determines, if we need to establish a PinUvAuthParam, based on the
+/// capabilities of the device and the incoming request.
+/// If it is needed, tries to establish one and save it inside the Request.
+/// Returns Ok() if we can proceed with sending the actual Request to
+/// the device, Err() otherwise.
+/// Handles asking the user for a PIN, if needed and sending StatusUpdates
+/// regarding PIN and UV usage.
+#[allow(clippy::too_many_arguments)]
+fn determine_puap_if_needed<Dev: FidoDevice, T: PinUvAuthCommand + RequestCtap2>(
+ cmd: &mut T,
+ dev: &mut Dev,
+ mut skip_uv: bool,
+ permission: PinUvAuthTokenPermission,
+ uv_req: UserVerificationRequirement,
+ status: &Sender<StatusUpdate>,
+ alive: &dyn Fn() -> bool,
+ pin: &mut Option<Pin>,
+) -> Result<PinUvAuthResult, AuthenticatorError> {
+ while alive() {
+ debug!("-----------------------------------------------------------------");
+ debug!("Getting pinUvAuthParam");
+ match get_pin_uv_auth_param(cmd, dev, permission, skip_uv, uv_req, alive, pin) {
+ Ok(r) => {
+ return Ok(r);
+ }
+
+ Err(AuthenticatorError::PinError(PinError::PinRequired)) => {
+ let new_pin = ask_user_for_pin(false, None, status)?;
+ *pin = Some(new_pin);
+ skip_uv = true;
+ continue;
+ }
+ Err(AuthenticatorError::PinError(PinError::InvalidPin(retries))) => {
+ let new_pin = ask_user_for_pin(true, retries, status)?;
+ *pin = Some(new_pin);
+ continue;
+ }
+ Err(AuthenticatorError::PinError(PinError::InvalidUv(retries))) => {
+ if retries == Some(0) {
+ skip_uv = true;
+ }
+ send_status(
+ status,
+ StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries)),
+ )
+ }
+ Err(e @ AuthenticatorError::PinError(PinError::PinAuthBlocked)) => {
+ send_status(
+ status,
+ StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked),
+ );
+ error!("Error when determining pinAuth: {:?}", e);
+ return Err(e);
+ }
+ Err(e @ AuthenticatorError::PinError(PinError::PinBlocked)) => {
+ send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinBlocked));
+ error!("Error when determining pinAuth: {:?}", e);
+ return Err(e);
+ }
+ Err(e @ AuthenticatorError::PinError(PinError::PinNotSet)) => {
+ send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinNotSet));
+ error!("Error when determining pinAuth: {:?}", e);
+ return Err(e);
+ }
+ Err(AuthenticatorError::PinError(PinError::UvBlocked)) => {
+ skip_uv = true;
+ send_status(status, StatusUpdate::PinUvError(StatusPinUv::UvBlocked))
+ }
+ // Used for CTAP2.0 UV (fingerprints)
+ Err(AuthenticatorError::PinError(PinError::PinAuthInvalid)) => {
+ skip_uv = true;
+ send_status(
+ status,
+ StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)),
+ )
+ }
+ Err(e) => {
+ error!("Error when determining pinAuth: {:?}", e);
+ return Err(e);
+ }
+ }
+ }
+ Err(AuthenticatorError::CancelledByUser)
+}
+
+pub fn register<Dev: FidoDevice>(
+ dev: &mut Dev,
+ args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ alive: &dyn Fn() -> bool,
+) -> bool {
+ let mut options = MakeCredentialsOptions::default();
+
+ if dev.get_protocol() == FidoProtocol::CTAP2 {
+ let info = match dev.get_authenticator_info() {
+ Some(info) => info,
+ None => {
+ callback.call(Err(HIDError::DeviceNotInitialized.into()));
+ return false;
+ }
+ };
+
+ // Set options based on the arguments and the device info.
+ // The user verification option will be set in `determine_puap_if_needed`.
+ options.resident_key = match args.resident_key_req {
+ ResidentKeyRequirement::Required => Some(true),
+ ResidentKeyRequirement::Preferred => {
+ // Use a resident key if the authenticator supports it
+ Some(info.options.resident_key)
+ }
+ ResidentKeyRequirement::Discouraged => Some(false),
+ }
+ } else {
+ // Check that the request can be processed by a CTAP1 device.
+ // See CTAP 2.1 Section 10.2. Some additional checks are performed in
+ // MakeCredentials::RequestCtap1
+ if args.resident_key_req == ResidentKeyRequirement::Required {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::ResidentKey,
+ )));
+ return false;
+ }
+ if args.user_verification_req == UserVerificationRequirement::Required {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::UserVerification,
+ )));
+ return false;
+ }
+ if !args
+ .pub_cred_params
+ .iter()
+ .any(|x| x.alg == COSEAlgorithm::ES256)
+ {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::PubCredParams,
+ )));
+ return false;
+ }
+ }
+
+ // Client extension processing for credProtect:
+ // "When enforceCredentialProtectionPolicy is true, and credentialProtectionPolicy's value is
+ // [not "Optional"], the platform SHOULD NOT create the credential in a way that does not
+ // implement the requested protection policy. (For example, by creating it on an authenticator
+ // that does not support this extension.)"
+ let dev_supports_cred_protect = dev
+ .get_authenticator_info()
+ .map_or(false, |info| info.supports_cred_protect());
+ if args.extensions.enforce_credential_protection_policy == Some(true)
+ && args.extensions.credential_protection_policy
+ != Some(CredentialProtectionPolicy::UserVerificationOptional)
+ && !dev_supports_cred_protect
+ {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::CredProtect,
+ )));
+ return false;
+ }
+
+ let mut makecred = MakeCredentials::new(
+ ClientDataHash(args.client_data_hash),
+ args.relying_party,
+ Some(args.user),
+ args.pub_cred_params,
+ args.exclude_list,
+ options,
+ args.extensions.into(),
+ );
+
+ let mut skip_uv = false;
+ let mut pin = args.pin;
+ while alive() {
+ // Requesting both because pre-flighting (credential list filtering)
+ // can potentially send GetAssertion-commands
+ let permissions =
+ PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion;
+
+ let pin_uv_auth_result = unwrap_result!(
+ determine_puap_if_needed(
+ &mut makecred,
+ dev,
+ skip_uv,
+ permissions,
+ args.user_verification_req,
+ &status,
+ alive,
+ &mut pin,
+ ),
+ callback
+ );
+ // Do "pre-flight": Filter the exclude-list
+ if dev.get_protocol() == FidoProtocol::CTAP2 {
+ makecred.exclude_list = unwrap_result!(
+ do_credential_list_filtering_ctap2(
+ dev,
+ &makecred.exclude_list,
+ &makecred.rp,
+ pin_uv_auth_result.get_pin_uv_auth_token(),
+ ),
+ callback
+ );
+ } else {
+ let key_handle = do_credential_list_filtering_ctap1(
+ dev,
+ &makecred.exclude_list,
+ &makecred.rp,
+ &makecred.client_data_hash,
+ );
+ // That handle was already registered with the token
+ if key_handle.is_some() {
+ // Now we need to send a dummy registration request, to make the token blink
+ // Spec says "dummy appid and invalid challenge". We use the same, as we do for
+ // making the token blink upon device selection.
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+ let msg = dummy_make_credentials_cmd();
+ let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "CredentialExcluded"
+ callback.call(Err(AuthenticatorError::CredentialExcluded));
+ return false;
+ }
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{makecred:?} using {pin_uv_auth_result:?}");
+ debug!("------------------------------------------------------------------");
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+ let resp = dev.send_msg_cancellable(&makecred, alive);
+ match resp {
+ Ok(result) => {
+ callback.call(Ok(result));
+ return true;
+ }
+ Err(e) => {
+ handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv);
+ }
+ }
+ }
+ false
+}
+
+pub fn sign<Dev: FidoDevice>(
+ dev: &mut Dev,
+ args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ alive: &dyn Fn() -> bool,
+) -> bool {
+ if dev.get_protocol() == FidoProtocol::CTAP1 {
+ // Check that the request can be processed by a CTAP1 device.
+ // See CTAP 2.1 Section 10.3. Some additional checks are performed in
+ // GetAssertion::RequestCtap1
+ if args.user_verification_req == UserVerificationRequirement::Required {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::UserVerification,
+ )));
+ return false;
+ }
+ if args.allow_list.is_empty() {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::EmptyAllowList,
+ )));
+ return false;
+ }
+ }
+
+ let mut allow_list = args.allow_list;
+ let mut rp_id = RelyingParty::from(args.relying_party_id);
+ let client_data_hash = ClientDataHash(args.client_data_hash);
+ if let Some(ref app_id) = args.extensions.app_id {
+ if !allow_list.is_empty() {
+ // Try to silently discover U2F credentials that require the FIDO App ID extension. If
+ // any are found, we should use the alternate RP ID instead of the provided RP ID.
+ let alt_rp_id = RelyingParty::from(app_id);
+ let silent_creds =
+ silently_discover_credentials(dev, &allow_list, &alt_rp_id, &client_data_hash);
+ if !silent_creds.is_empty() {
+ allow_list = silent_creds;
+ rp_id = alt_rp_id;
+ }
+ }
+ }
+
+ let mut get_assertion = GetAssertion::new(
+ client_data_hash,
+ rp_id,
+ allow_list,
+ GetAssertionOptions {
+ user_presence: Some(args.user_presence_req),
+ user_verification: None,
+ },
+ args.extensions.into(),
+ );
+
+ let mut skip_uv = false;
+ let mut pin = args.pin;
+ while alive() {
+ let pin_uv_auth_result = unwrap_result!(
+ determine_puap_if_needed(
+ &mut get_assertion,
+ dev,
+ skip_uv,
+ PinUvAuthTokenPermission::GetAssertion,
+ args.user_verification_req,
+ &status,
+ alive,
+ &mut pin,
+ ),
+ callback
+ );
+ // Third, use the shared secret in the extensions, if requested
+ if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() {
+ if let Some(secret) = dev.get_shared_secret() {
+ match extension.calculate(secret) {
+ Ok(x) => x,
+ Err(e) => {
+ callback.call(Err(e));
+ return false;
+ }
+ }
+ }
+ }
+
+ // Do "pre-flight": Filter the allow-list
+ let original_allow_list_was_empty = get_assertion.allow_list.is_empty();
+ if dev.get_protocol() == FidoProtocol::CTAP2 {
+ get_assertion.allow_list = unwrap_result!(
+ do_credential_list_filtering_ctap2(
+ dev,
+ &get_assertion.allow_list,
+ &get_assertion.rp,
+ pin_uv_auth_result.get_pin_uv_auth_token(),
+ ),
+ callback
+ );
+ } else {
+ let key_handle = do_credential_list_filtering_ctap1(
+ dev,
+ &get_assertion.allow_list,
+ &get_assertion.rp,
+ &get_assertion.client_data_hash,
+ );
+ match key_handle {
+ Some(key_handle) => {
+ get_assertion.allow_list = vec![key_handle];
+ }
+ None => {
+ get_assertion.allow_list.clear();
+ }
+ }
+ }
+
+ // If the incoming list was not empty, but the filtered list is, we have to error out
+ if !original_allow_list_was_empty && get_assertion.allow_list.is_empty() {
+ // We have to collect a user interaction
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+ let msg = dummy_make_credentials_cmd();
+ let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "NoCredentials"
+ callback.call(Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NoCredentials,
+ None,
+ ))
+ .into()));
+ return false;
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{get_assertion:?} using {pin_uv_auth_result:?}");
+ debug!("------------------------------------------------------------------");
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+ let mut results = match dev.send_msg_cancellable(&get_assertion, alive) {
+ Ok(results) => results,
+ Err(e) => {
+ handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv);
+ }
+ };
+ if results.len() == 1 {
+ callback.call(Ok(results.swap_remove(0)));
+ return true;
+ }
+ let (tx, rx) = channel();
+ let user_entities = results
+ .iter()
+ .filter_map(|x| x.assertion.user.clone())
+ .collect();
+ send_status(
+ &status,
+ crate::StatusUpdate::SelectResultNotice(tx, user_entities),
+ );
+ match rx.recv() {
+ Ok(Some(index)) if index < results.len() => {
+ callback.call(Ok(results.swap_remove(index)));
+ return true;
+ }
+ _ => {
+ callback.call(Err(AuthenticatorError::CancelledByUser));
+ return true;
+ }
+ }
+ }
+ false
+}
+
+pub(crate) fn reset_helper<T: From<ResetResult>>(
+ dev: &mut Device,
+ selector: Sender<DeviceSelectorEvent>,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<T>>,
+ keep_alive: &dyn Fn() -> bool,
+) {
+ let reset = Reset {};
+ info!("Device {:?} continues with the reset process", dev.id());
+
+ debug!("------------------------------------------------------------------");
+ debug!("{:?}", reset);
+ debug!("------------------------------------------------------------------");
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+ let resp = dev.send_cbor_cancellable(&reset, keep_alive);
+ if resp.is_ok() {
+ // The DeviceSelector could already be dead, but it might also wait
+ // for us to respond, in order to cancel all other tokens in case
+ // we skipped the "blinking"-action and went straight for the actual
+ // request.
+ let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
+ }
+
+ match resp {
+ Ok(()) => callback.call(Ok(T::from(()))),
+ Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {}
+ Err(HIDError::Command(CommandError::StatusCode(StatusCode::ChannelBusy, _))) => {}
+ Err(e) => {
+ warn!("error happened: {}", e);
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ }
+ }
+}
+
+pub(crate) fn set_or_change_pin_helper<T: From<()>>(
+ dev: &mut Device,
+ mut current_pin: Option<Pin>,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<T>>,
+ alive: &dyn Fn() -> bool,
+) {
+ let mut shared_secret = match dev.establish_shared_secret(alive) {
+ Ok(s) => s,
+ Err(e) => {
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ return;
+ }
+ };
+
+ let authinfo = match dev.get_authenticator_info() {
+ Some(i) => i.clone(),
+ None => {
+ callback.call(Err(HIDError::DeviceNotInitialized.into()));
+ return;
+ }
+ };
+
+ // If the device has a min PIN use that, otherwise default to 4 according to Spec
+ if new_pin.as_bytes().len() < authinfo.min_pin_length.unwrap_or(4) as usize {
+ callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort)));
+ return;
+ }
+
+ // As per Spec: "Maximum PIN Length: UTF-8 representation MUST NOT exceed 63 bytes"
+ if new_pin.as_bytes().len() >= 64 {
+ callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooLong(
+ new_pin.as_bytes().len(),
+ ))));
+ return;
+ }
+
+ // Check if a client-pin is already set, or if a new one should be created
+ let res = if Some(true) == authinfo.options.client_pin {
+ let mut res;
+ let mut was_invalid = false;
+ let mut retries = None;
+ loop {
+ // current_pin will only be Some() in the interactive mode (running `manage()`)
+ // In case that PIN is wrong, we want to avoid an endless-loop here with re-trying
+ // that wrong PIN all the time. So we `take()` it, and only test it once.
+ // If that PIN is wrong, we fall back to the "ask_user_for_pin"-method.
+ let curr_pin = match current_pin.take() {
+ None => match ask_user_for_pin(was_invalid, retries, &status) {
+ Ok(pin) => pin,
+ Err(e) => {
+ callback.call(Err(e));
+ return;
+ }
+ },
+ Some(pin) => pin,
+ };
+
+ res = ChangeExistingPin::new(&authinfo, &shared_secret, &curr_pin, &new_pin)
+ .map_err(HIDError::Command)
+ .and_then(|msg| dev.send_cbor_cancellable(&msg, alive))
+ .map_err(|e| repackage_pin_errors(dev, e));
+
+ if let Err(AuthenticatorError::PinError(PinError::InvalidPin(r))) = res {
+ was_invalid = true;
+ retries = r;
+ // We need to re-establish the shared secret for the next round.
+ match dev.establish_shared_secret(alive) {
+ Ok(s) => {
+ shared_secret = s;
+ }
+ Err(e) => {
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ return;
+ }
+ };
+
+ continue;
+ } else {
+ break;
+ }
+ }
+ res
+ } else {
+ dev.send_cbor_cancellable(&SetNewPin::new(&shared_secret, &new_pin), alive)
+ .map_err(AuthenticatorError::HIDError)
+ };
+ // the callback is expecting `Result<(), AuthenticatorError>`, but `ChangeExistingPin`
+ // and `SetNewPin` return the default `ClientPinResponse` on success. Just discard it.
+ callback.call(res.map(|_| T::from(())));
+}
+
+pub(crate) fn bio_enrollment(
+ dev: &mut Device,
+ puat_result: Option<PinUvAuthResult>,
+ command: BioEnrollmentCmd,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ alive: &dyn Fn() -> bool,
+) -> bool {
+ let authinfo = match dev.get_authenticator_info() {
+ Some(i) => i,
+ None => {
+ callback.call(Err(HIDError::DeviceNotInitialized.into()));
+ return false;
+ }
+ };
+
+ if authinfo.options.bio_enroll.is_none()
+ && authinfo.options.user_verification_mgmt_preview.is_none()
+ {
+ callback.call(Err(AuthenticatorError::HIDError(
+ HIDError::UnsupportedCommand,
+ )));
+ return false;
+ }
+
+ let use_legacy_preview = authinfo.options.bio_enroll.is_none();
+
+ // We are not allowed to request the BE-permission using UV, so we have to skip UV
+ let mut skip_uv = authinfo.options.uv_bio_enroll != Some(true);
+ // Currently not used, but if we want, we can just set the value here.
+ let timeout = None;
+
+ let mut bio_cmd = match &command {
+ BioEnrollmentCmd::StartNewEnrollment(_name) => BioEnrollment::new(
+ BioEnrollmentCommand::EnrollBegin(timeout),
+ use_legacy_preview,
+ ),
+ BioEnrollmentCmd::DeleteEnrollment(id) => BioEnrollment::new(
+ BioEnrollmentCommand::RemoveEnrollment(id.clone()),
+ use_legacy_preview,
+ ),
+ BioEnrollmentCmd::ChangeName(id, name) => BioEnrollment::new(
+ BioEnrollmentCommand::SetFriendlyName((id.clone(), name.clone())),
+ use_legacy_preview,
+ ),
+ BioEnrollmentCmd::GetEnrollments => BioEnrollment::new(
+ BioEnrollmentCommand::EnumerateEnrollments,
+ use_legacy_preview,
+ ),
+ BioEnrollmentCmd::GetFingerprintSensorInfo => BioEnrollment::new(
+ BioEnrollmentCommand::GetFingerprintSensorInfo,
+ use_legacy_preview,
+ ),
+ };
+
+ let mut skip_puap = false;
+ let mut cached_puat = false; // If we were provided with a cached puat from the outside
+ let mut pin_uv_auth_result = puat_result
+ .clone()
+ .unwrap_or(PinUvAuthResult::NoAuthRequired);
+ // See, if we have a cached PUAT with matching permissions.
+ match puat_result {
+ Some(PinUvAuthResult::SuccessGetPinToken(t))
+ | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t))
+ | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t))
+ if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system
+ || use_legacy_preview // Preview doesn't use permissions
+ || t.permissions
+ .contains(PinUvAuthTokenPermission::BioEnrollment) =>
+ {
+ skip_puap = true;
+ cached_puat = true;
+ unwrap_result!(bio_cmd.set_pin_uv_auth_param(Some(t)), callback);
+ }
+ _ => {}
+ }
+ let mut pin = None;
+ while alive() {
+ if !skip_puap {
+ pin_uv_auth_result = unwrap_result!(
+ determine_puap_if_needed(
+ &mut bio_cmd,
+ dev,
+ skip_uv,
+ PinUvAuthTokenPermission::BioEnrollment,
+ UserVerificationRequirement::Preferred,
+ &status,
+ alive,
+ &mut pin,
+ ),
+ callback
+ );
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{bio_cmd:?} using {pin_uv_auth_result:?}");
+ debug!("------------------------------------------------------------------");
+
+ let resp = dev.send_cbor_cancellable(&bio_cmd, alive);
+ match resp {
+ Ok(result) => {
+ skip_puap = true;
+ match bio_cmd.subcommand {
+ BioEnrollmentCommand::EnrollBegin(..)
+ | BioEnrollmentCommand::EnrollCaptureNextSample(..) => {
+ let template_id =
+ if let BioEnrollmentCommand::EnrollCaptureNextSample((id, ..)) =
+ bio_cmd.subcommand
+ {
+ id
+ } else {
+ unwrap_option!(result.template_id, callback)
+ };
+ let last_enroll_sample_status =
+ unwrap_option!(result.last_enroll_sample_status, callback);
+ let remaining_samples = unwrap_option!(result.remaining_samples, callback);
+
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::BioEnrollmentUpdate((
+ BioEnrollmentResult::SampleStatus(
+ last_enroll_sample_status,
+ remaining_samples,
+ ),
+ Some(pin_uv_auth_result.clone()),
+ )),
+ ),
+ );
+
+ if remaining_samples == 0 {
+ if let BioEnrollmentCmd::StartNewEnrollment(Some(ref name)) = command {
+ bio_cmd.subcommand = BioEnrollmentCommand::SetFriendlyName((
+ template_id.to_vec(),
+ name.clone(),
+ ));
+ // We have to regenerate PUAP here. PUAT hasn't changed, but the content
+ // of the command has changed, and that is part of the PUAP-calculation
+ unwrap_result!(
+ bio_cmd.set_pin_uv_auth_param(
+ pin_uv_auth_result.get_pin_uv_auth_token()
+ ),
+ callback
+ );
+ continue;
+ } else {
+ let auth_info =
+ unwrap_option!(dev.refresh_authenticator_info(), callback);
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::BioEnrollmentUpdate((
+ BioEnrollmentResult::AddSuccess(auth_info.clone()),
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ } else {
+ bio_cmd.subcommand = BioEnrollmentCommand::EnrollCaptureNextSample((
+ template_id,
+ timeout,
+ ));
+ // We have to regenerate PUAP here. PUAT hasn't changed, but the content
+ // of the command has changed, and that is part of the PUAP-calculation
+ unwrap_result!(
+ bio_cmd.set_pin_uv_auth_param(
+ pin_uv_auth_result.get_pin_uv_auth_token()
+ ),
+ callback
+ );
+ continue;
+ }
+ }
+ BioEnrollmentCommand::EnumerateEnrollments => {
+ let list = result.template_infos.iter().map(|x| x.into()).collect();
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::BioEnrollmentUpdate((
+ BioEnrollmentResult::EnrollmentList(list),
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ BioEnrollmentCommand::SetFriendlyName(_) => {
+ let res = match command {
+ BioEnrollmentCmd::StartNewEnrollment(..) => {
+ let auth_info =
+ unwrap_option!(dev.refresh_authenticator_info(), callback);
+ BioEnrollmentResult::AddSuccess(auth_info.clone())
+ }
+ _ => BioEnrollmentResult::UpdateSuccess,
+ };
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::BioEnrollmentUpdate((
+ res,
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ BioEnrollmentCommand::RemoveEnrollment(_) => {
+ let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback);
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::BioEnrollmentUpdate((
+ BioEnrollmentResult::DeleteSuccess(auth_info.clone()),
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ BioEnrollmentCommand::CancelCurrentEnrollment => {
+ callback.call(Ok(ManageResult::Success));
+ return true;
+ }
+ BioEnrollmentCommand::GetFingerprintSensorInfo => {
+ let fingerprint_kind = unwrap_option!(result.fingerprint_kind, callback);
+ let max_capture_samples_required_for_enroll = unwrap_option!(
+ result.max_capture_samples_required_for_enroll,
+ callback
+ );
+ // FIDO_2_1_PRE-devices do not report this field. So we leave it optional.
+ let max_template_friendly_name = result.max_template_friendly_name;
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::BioEnrollmentUpdate((
+ BioEnrollmentResult::FingerprintSensorInfo(
+ FingerprintSensorInfo {
+ fingerprint_kind,
+ max_capture_samples_required_for_enroll,
+ max_template_friendly_name,
+ },
+ ),
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ };
+ }
+ Err(e) => {
+ handle_errors!(
+ e,
+ status,
+ callback,
+ pin_uv_auth_result,
+ skip_uv,
+ skip_puap,
+ cached_puat
+ );
+ }
+ }
+ }
+ false
+}
+
+pub(crate) fn credential_management(
+ dev: &mut Device,
+ puat_result: Option<PinUvAuthResult>,
+ command: CredManagementCmd,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ alive: &dyn Fn() -> bool,
+) -> bool {
+ let mut skip_uv = false;
+ let authinfo = match dev.get_authenticator_info() {
+ Some(i) => i.clone(),
+ None => {
+ callback.call(Err(HIDError::DeviceNotInitialized.into()));
+ return false;
+ }
+ };
+
+ if authinfo.options.cred_mgmt != Some(true)
+ && authinfo.options.credential_mgmt_preview != Some(true)
+ {
+ callback.call(Err(AuthenticatorError::HIDError(
+ HIDError::UnsupportedCommand,
+ )));
+ return false;
+ }
+
+ let use_legacy_preview = authinfo.options.cred_mgmt != Some(true);
+
+ // FIDO_2_1_PRE-devices do not support UpdateUserInformation.
+ if use_legacy_preview
+ && !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1)
+ && matches!(command, CredManagementCmd::UpdateUserInformation(..))
+ {
+ callback.call(Err(AuthenticatorError::HIDError(
+ HIDError::UnsupportedCommand,
+ )));
+ return false;
+ }
+
+ // If puap is provided, we can skip puap-determination (i.e. PIN entry)
+ let mut cred_management = match command {
+ CredManagementCmd::GetCredentials => {
+ CredentialManagement::new(CredManagementCommand::GetCredsMetadata, use_legacy_preview)
+ }
+ CredManagementCmd::DeleteCredential(cred_id) => CredentialManagement::new(
+ CredManagementCommand::DeleteCredential(cred_id),
+ use_legacy_preview,
+ ),
+ CredManagementCmd::UpdateUserInformation(cred_id, user) => CredentialManagement::new(
+ CredManagementCommand::UpdateUserInformation((cred_id, user)),
+ use_legacy_preview,
+ ),
+ };
+ let mut credential_result = CredentialList::new();
+ let mut remaining_rps = 0;
+ let mut remaining_cred_ids = 0;
+ let mut current_rp = 0;
+ let mut skip_puap = false;
+ let mut cached_puat = false; // If we were provided with a cached puat from the outside
+ let mut pin_uv_auth_result = puat_result
+ .clone()
+ .unwrap_or(PinUvAuthResult::NoAuthRequired);
+ // See, if we have a cached PUAT with matching permissions.
+ match puat_result {
+ Some(PinUvAuthResult::SuccessGetPinToken(t))
+ | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t))
+ | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t))
+ if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system
+ || use_legacy_preview // Preview doesn't use permissions
+ || t.permissions
+ .contains(PinUvAuthTokenPermission::CredentialManagement) =>
+ {
+ skip_puap = true;
+ cached_puat = true;
+ unwrap_result!(cred_management.set_pin_uv_auth_param(Some(t)), callback);
+ }
+ _ => {}
+ }
+ let mut pin = None;
+ while alive() {
+ if !skip_puap {
+ pin_uv_auth_result = unwrap_result!(
+ determine_puap_if_needed(
+ &mut cred_management,
+ dev,
+ skip_uv,
+ PinUvAuthTokenPermission::CredentialManagement,
+ UserVerificationRequirement::Preferred,
+ &status,
+ alive,
+ &mut pin,
+ ),
+ callback
+ );
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{cred_management:?} using {pin_uv_auth_result:?}");
+ debug!("------------------------------------------------------------------");
+
+ let resp = dev.send_cbor_cancellable(&cred_management, alive);
+ match resp {
+ Ok(result) => {
+ skip_puap = true;
+ match cred_management.subcommand {
+ CredManagementCommand::GetCredsMetadata => {
+ let existing_resident_credentials_count =
+ unwrap_option!(result.existing_resident_credentials_count, callback);
+ let max_possible_remaining_resident_credentials_count = unwrap_option!(
+ result.max_possible_remaining_resident_credentials_count,
+ callback
+ );
+ credential_result.existing_resident_credentials_count =
+ existing_resident_credentials_count;
+ credential_result.max_possible_remaining_resident_credentials_count =
+ max_possible_remaining_resident_credentials_count;
+ if existing_resident_credentials_count > 0 {
+ cred_management.subcommand = CredManagementCommand::EnumerateRPsBegin;
+ // We have to regenerate PUAP here. PUAT hasn't changed, but the content
+ // of the command has changed, and that is part of the PUAP-calculation
+ unwrap_result!(
+ cred_management.set_pin_uv_auth_param(
+ pin_uv_auth_result.get_pin_uv_auth_token()
+ ),
+ callback
+ );
+ continue;
+ } else {
+ // This token doesn't have any resident keys, but its not an error,
+ // so we send an empty list.
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::CredentialManagementUpdate((
+ CredentialManagementResult::CredentialList(
+ credential_result,
+ ),
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ }
+ CredManagementCommand::EnumerateRPsBegin
+ | CredManagementCommand::EnumerateRPsGetNextRP => {
+ if matches!(
+ cred_management.subcommand,
+ CredManagementCommand::EnumerateRPsBegin
+ ) {
+ let total_rps = unwrap_option!(result.total_rps, callback);
+ if total_rps == 0 {
+ // This token doesn't have any RPs, but its not an error,
+ // so we return an Ok with an empty list.
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::CredentialManagementUpdate((
+ CredentialManagementResult::CredentialList(
+ credential_result,
+ ),
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ remaining_rps = total_rps - 1;
+ } else {
+ remaining_rps -= 1;
+ }
+
+ let rp = unwrap_option!(result.rp, callback);
+ let rp_id_hash = unwrap_option!(result.rp_id_hash, callback);
+ let rp_res = CredentialRpListEntry {
+ rp,
+ rp_id_hash,
+ credentials: vec![],
+ };
+ credential_result.credential_list.push(rp_res);
+ if remaining_rps > 0 {
+ cred_management.subcommand =
+ CredManagementCommand::EnumerateRPsGetNextRP;
+ } else {
+ // We have queried all RPs, now start querying the corresponding credentials for each RP
+ cred_management.subcommand =
+ CredManagementCommand::EnumerateCredentialsBegin(
+ credential_result.credential_list[0].rp_id_hash.clone(),
+ );
+ }
+ // We have to regenerate PUAP here. PUAT hasn't changed, but the content
+ // of the command has changed, and that is part of the PUAP-calculation
+ unwrap_result!(
+ cred_management
+ .set_pin_uv_auth_param(pin_uv_auth_result.get_pin_uv_auth_token()),
+ callback
+ );
+ continue;
+ }
+ CredManagementCommand::EnumerateCredentialsBegin(..)
+ | CredManagementCommand::EnumerateCredentialsGetNextCredential => {
+ let user = unwrap_option!(result.user, callback);
+ let credential_id = unwrap_option!(result.credential_id, callback);
+ let public_key = unwrap_option!(result.public_key, callback);
+ let cred_protect = unwrap_option!(result.cred_protect, callback);
+ let large_blob_key = result.large_blob_key;
+
+ if matches!(
+ cred_management.subcommand,
+ CredManagementCommand::EnumerateCredentialsBegin(..)
+ ) {
+ remaining_cred_ids =
+ unwrap_option!(result.total_credentials, callback) - 1;
+ } else {
+ remaining_cred_ids -= 1;
+ }
+ // We might have to change the global variable, but need the unmodified below
+ let current_rp_backup = current_rp;
+ let mut we_are_done = false;
+ if remaining_cred_ids > 0 {
+ cred_management.subcommand =
+ CredManagementCommand::EnumerateCredentialsGetNextCredential;
+ } else {
+ current_rp += 1;
+ // We have all credentials from this RP. Starting with the next RP.
+ if current_rp < credential_result.credential_list.len() {
+ cred_management.subcommand =
+ CredManagementCommand::EnumerateCredentialsBegin(
+ credential_result.credential_list[current_rp]
+ .rp_id_hash
+ .clone(),
+ );
+ // We have to regenerate PUAP here. PUAT hasn't changed, but the content
+ // of the command has changed, and that is part of the PUAP-calculation
+ unwrap_result!(
+ cred_management.set_pin_uv_auth_param(
+ pin_uv_auth_result.get_pin_uv_auth_token()
+ ),
+ callback
+ );
+ } else {
+ // Finally done iterating over all RPs and their Credentials
+ we_are_done = true;
+ }
+ }
+ let key = CredentialListEntry {
+ user,
+ credential_id,
+ public_key,
+ cred_protect,
+ large_blob_key,
+ };
+ credential_result.credential_list[current_rp_backup]
+ .credentials
+ .push(key);
+ if we_are_done {
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::CredentialManagementUpdate((
+ CredentialManagementResult::CredentialList(
+ credential_result,
+ ),
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ } else {
+ continue;
+ }
+ }
+ CredManagementCommand::DeleteCredential(_) => {
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::CredentialManagementUpdate((
+ CredentialManagementResult::DeleteSucess,
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ CredManagementCommand::UpdateUserInformation(_) => {
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(
+ InteractiveUpdate::CredentialManagementUpdate((
+ CredentialManagementResult::UpdateSuccess,
+ Some(pin_uv_auth_result),
+ )),
+ ),
+ );
+ return true;
+ }
+ };
+ }
+ Err(e) => {
+ handle_errors!(
+ e,
+ status,
+ callback,
+ pin_uv_auth_result,
+ skip_uv,
+ skip_puap,
+ cached_puat
+ );
+ }
+ }
+ }
+ false
+}
+
+pub(crate) fn configure_authenticator(
+ dev: &mut Device,
+ puat_result: Option<PinUvAuthResult>,
+ cfg_subcommand: AuthConfigCommand,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ alive: &dyn Fn() -> bool,
+) -> bool {
+ let mut authcfg = AuthenticatorConfig::new(cfg_subcommand);
+ let mut skip_uv = false;
+ let authinfo = match dev.get_authenticator_info() {
+ Some(i) => i.clone(),
+ None => {
+ callback.call(Err(HIDError::DeviceNotInitialized.into()));
+ return false;
+ }
+ };
+
+ if authinfo.options.authnr_cfg != Some(true) {
+ callback.call(Err(AuthenticatorError::HIDError(
+ HIDError::UnsupportedCommand,
+ )));
+ return false;
+ }
+
+ let mut skip_puap = false;
+ let mut cached_puat = false; // If we were provided with a cached puat from the outside
+ let mut pin_uv_auth_result = puat_result
+ .clone()
+ .unwrap_or(PinUvAuthResult::NoAuthRequired);
+ match puat_result {
+ Some(PinUvAuthResult::SuccessGetPinToken(t))
+ | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t))
+ | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t))
+ if t.permissions
+ .contains(PinUvAuthTokenPermission::AuthenticatorConfiguration) =>
+ {
+ skip_puap = true;
+ cached_puat = true;
+ unwrap_result!(authcfg.set_pin_uv_auth_param(Some(t)), callback);
+ }
+ _ => {}
+ }
+ let mut pin = None;
+ while alive() {
+ // We can use the AuthenticatorConfiguration-command only in two cases:
+ // 1. The device also supports the uv_acfg-permission (otherwise we can't establish a PUAP)
+ // 2. The device is NOT protected by PIN/UV (yet). This allows organizations to configure
+ // the token, before handing them out.
+ // If authinfo.options.uv_acfg is not supported, this will return UnauthorizedPermission
+ if !skip_puap {
+ pin_uv_auth_result = unwrap_result!(
+ determine_puap_if_needed(
+ &mut authcfg,
+ dev,
+ skip_uv,
+ PinUvAuthTokenPermission::AuthenticatorConfiguration,
+ UserVerificationRequirement::Preferred,
+ &status,
+ alive,
+ &mut pin,
+ ),
+ callback
+ );
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{authcfg:?} using {pin_uv_auth_result:?}");
+ debug!("------------------------------------------------------------------");
+
+ let resp = dev.send_cbor_cancellable(&authcfg, alive);
+ match resp {
+ Ok(()) => {
+ let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback);
+ send_status(
+ &status,
+ StatusUpdate::InteractiveManagement(InteractiveUpdate::AuthConfigUpdate((
+ AuthConfigResult::Success(auth_info.clone()),
+ Some(pin_uv_auth_result),
+ ))),
+ );
+ return true;
+ }
+ Err(e) => {
+ handle_errors!(
+ e,
+ status,
+ callback,
+ pin_uv_auth_result,
+ skip_uv,
+ skip_puap,
+ cached_puat
+ );
+ }
+ }
+ }
+ false
+}
diff --git a/third_party/rust/authenticator/src/ctap2/preflight.rs b/third_party/rust/authenticator/src/ctap2/preflight.rs
new file mode 100644
index 0000000000..6574042136
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/preflight.rs
@@ -0,0 +1,530 @@
+use super::client_data::ClientDataHash;
+use super::commands::get_assertion::{GetAssertion, GetAssertionExtensions, GetAssertionOptions};
+use super::commands::{CtapResponse, PinUvAuthCommand, RequestCtap1, Retryable};
+use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED};
+use crate::crypto::PinUvAuthToken;
+use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::{FidoDevice, FidoProtocol, VirtualFidoDevice};
+use crate::u2ftypes::CTAP1RequestAPDU;
+use sha2::{Digest, Sha256};
+
+/// This command is used to check which key_handle is valid for this
+/// token. This is sent before a GetAssertion command, to determine which
+/// is valid for a specific token and which key_handle GetAssertion
+/// should send to the token. Or before a MakeCredential command, to determine
+/// if this token is already registered or not.
+#[derive(Debug)]
+pub struct CheckKeyHandle<'assertion> {
+ pub key_handle: &'assertion [u8],
+ pub client_data_hash: &'assertion [u8],
+ pub rp: &'assertion RelyingParty,
+}
+
+type EmptyResponse = ();
+impl CtapResponse for EmptyResponse {}
+
+impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> {
+ type Output = EmptyResponse;
+ type AdditionalInfo = ();
+
+ fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError> {
+ // In theory, we only need to do this for up=true, for up=false, we could
+ // use U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN instead and use the answer directly.
+ // But that would involve another major refactoring to implement, and so we accept
+ // that we will send the final request twice to the authenticator. Once with
+ // U2F_CHECK_IS_REGISTERED followed by U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN.
+ let flags = U2F_CHECK_IS_REGISTERED;
+ let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len());
+
+ auth_data.extend_from_slice(self.client_data_hash);
+ auth_data.extend_from_slice(self.rp.hash().as_ref());
+ auth_data.extend_from_slice(&[self.key_handle.len() as u8]);
+ auth_data.extend_from_slice(self.key_handle);
+ let cmd = U2F_AUTHENTICATE;
+ let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?;
+ Ok((apdu, ()))
+ }
+
+ fn handle_response_ctap1<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ _input: &[u8],
+ _add_info: &Self::AdditionalInfo,
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ // From the U2F-spec: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-request-message---u2f_register
+ // if the control byte is set to 0x07 by the FIDO Client, the U2F token is supposed to
+ // simply check whether the provided key handle was originally created by this token,
+ // and whether it was created for the provided application parameter. If so, the U2F
+ // token MUST respond with an authentication response
+ // message:error:test-of-user-presence-required (note that despite the name this
+ // signals a success condition). If the key handle was not created by this U2F
+ // token, or if it was created for a different application parameter, the token MUST
+ // respond with an authentication response message:error:bad-key-handle.
+ match status {
+ Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()),
+ Err(e) => Err(Retryable::Error(HIDError::ApduStatus(e))),
+ }
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ dev.check_key_handle(self)
+ }
+}
+
+/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or
+/// authenticatorGetAssertion's allowList contain credential IDs that are already
+/// present on an authenticator, a platform typically invokes authenticatorGetAssertion
+/// with the "up" option key set to false and optionally pinUvAuthParam one or more times.
+/// For CTAP1, the resulting list will always be of length 1.
+pub(crate) fn do_credential_list_filtering_ctap1<Dev: FidoDevice>(
+ dev: &mut Dev,
+ cred_list: &[PublicKeyCredentialDescriptor],
+ rp: &RelyingParty,
+ client_data_hash: &ClientDataHash,
+) -> Option<PublicKeyCredentialDescriptor> {
+ let key_handle = cred_list
+ .iter()
+ // key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2.
+ // Filter out key-handles that are too long (can happen if this is a CTAP2-request,
+ // but the token only speaks CTAP1).
+ .filter(|key_handle| key_handle.id.len() < 256)
+ .find_map(|key_handle| {
+ let check_command = CheckKeyHandle {
+ key_handle: key_handle.id.as_ref(),
+ client_data_hash: client_data_hash.as_ref(),
+ rp,
+ };
+ let res = dev.send_ctap1(&check_command);
+ match res {
+ Ok(_) => Some(key_handle.clone()),
+ _ => None,
+ }
+ });
+ key_handle
+}
+
+/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or
+/// authenticatorGetAssertion's allowList contain credential IDs that are already
+/// present on an authenticator, a platform typically invokes authenticatorGetAssertion
+/// with the "up" option key set to false and optionally pinUvAuthParam one or more times.
+pub(crate) fn do_credential_list_filtering_ctap2<Dev: FidoDevice>(
+ dev: &mut Dev,
+ cred_list: &[PublicKeyCredentialDescriptor],
+ rp: &RelyingParty,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+) -> Result<Vec<PublicKeyCredentialDescriptor>, AuthenticatorError> {
+ let info = dev
+ .get_authenticator_info()
+ .ok_or(HIDError::DeviceNotInitialized)?;
+ let mut cred_list = cred_list.to_vec();
+ // Step 1.0: Find out how long the exclude_list/allow_list is allowed to be
+ // If the token doesn't tell us, we assume a length of 1
+ let mut chunk_size = match info.max_credential_count_in_list {
+ // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means
+ // falling back to a chunk size of 1 as the bare minimum.
+ None | Some(0) => 1,
+ Some(x) => x,
+ };
+
+ // Step 1.1: The device only supports keys up to a certain length.
+ // Filter out all keys that are longer, because they can't be
+ // from this device anyways.
+ match info.max_credential_id_length {
+ None => { /* no-op */ }
+ // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means
+ // falling back to a chunk size of 1 as the bare minimum.
+ Some(0) => {
+ chunk_size = 1;
+ }
+ Some(max_key_length) => {
+ cred_list.retain(|k| k.id.len() <= max_key_length);
+ }
+ }
+
+ let chunked_list = cred_list.chunks(chunk_size);
+
+ // Step 2: If we have more than one chunk: Loop over all, doing GetAssertion
+ // and if one of them comes back with a success, use only that chunk.
+ let mut final_list = Vec::new();
+ for chunk in chunked_list {
+ let mut silent_assert = GetAssertion::new(
+ ClientDataHash(Sha256::digest("").into()),
+ rp.clone(),
+ chunk.to_vec(),
+ GetAssertionOptions {
+ user_verification: None, // defaults to Some(false) if puap is absent
+ user_presence: Some(false),
+ },
+ GetAssertionExtensions::default(),
+ );
+ silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?;
+ match dev.send_msg(&silent_assert) {
+ Ok(mut response) => {
+ // This chunk contains a key_handle that is already known to the device.
+ // Filter out all credentials the device returned. Those are valid.
+ let credential_ids = response
+ .iter_mut()
+ .filter_map(|result| {
+ // CTAP 2.0 devices can omit the credentials in their response,
+ // if the given allowList was only 1 entry long. If so, we have
+ // to fill it in ourselfs.
+ if chunk.len() == 1 && result.assertion.credentials.is_none() {
+ Some(chunk[0].clone())
+ } else {
+ result.assertion.credentials.take()
+ }
+ })
+ .collect();
+ // Replace credential_id_list with the valid credentials
+ final_list = credential_ids;
+ break;
+ }
+ Err(_) => {
+ // No-op: Go to next chunk.
+ // NOTE: while we expect a StatusCode::NoCredentials error here, some tokens return
+ // other values.
+ continue;
+ }
+ }
+ }
+
+ // Step 3: Now ExcludeList/AllowList is either empty or has one batch with a 'known' credential.
+ // Send it as a normal Request and expect a "CredentialExcluded"-error in case of
+ // MakeCredential or a Success in case of GetAssertion
+ Ok(final_list)
+}
+
+pub(crate) fn silently_discover_credentials<Dev: FidoDevice>(
+ dev: &mut Dev,
+ cred_list: &[PublicKeyCredentialDescriptor],
+ rp: &RelyingParty,
+ client_data_hash: &ClientDataHash,
+) -> Vec<PublicKeyCredentialDescriptor> {
+ if dev.get_protocol() == FidoProtocol::CTAP2 {
+ if let Ok(cred_list) = do_credential_list_filtering_ctap2(dev, cred_list, rp, None) {
+ return cred_list;
+ }
+ } else if let Some(key_handle) =
+ do_credential_list_filtering_ctap1(dev, cred_list, rp, client_data_hash)
+ {
+ return vec![key_handle];
+ }
+ vec![]
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::{
+ crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve},
+ ctap2::{
+ attestation::{
+ AAGuid, AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags,
+ Extension,
+ },
+ commands::{CommandError, StatusCode},
+ server::{AuthenticationExtensionsClientOutputs, AuthenticatorAttachment, Transport},
+ },
+ transport::{
+ device_selector::tests::{make_device_simple_u2f, make_device_with_pin},
+ hid::HIDDevice,
+ platform::device::Device,
+ },
+ Assertion, GetAssertionResult,
+ };
+
+ fn new_relying_party(name: &str) -> RelyingParty {
+ RelyingParty {
+ id: String::from(name),
+ name: Some(String::from(name)),
+ }
+ }
+
+ fn new_silent_assert(
+ rp: &RelyingParty,
+ allow_list: &[PublicKeyCredentialDescriptor],
+ ) -> GetAssertion {
+ GetAssertion::new(
+ ClientDataHash(Sha256::digest("").into()),
+ rp.clone(),
+ allow_list.to_vec(),
+ GetAssertionOptions {
+ user_verification: None, // defaults to Some(false) if puap is absent
+ user_presence: Some(false),
+ },
+ GetAssertionExtensions::default(),
+ )
+ }
+
+ fn new_credential(fill: u8, repeat: usize) -> PublicKeyCredentialDescriptor {
+ PublicKeyCredentialDescriptor {
+ id: vec![fill; repeat],
+ transports: vec![Transport::USB],
+ }
+ }
+
+ fn new_assertion_response(
+ rp: &RelyingParty,
+ cred: Option<&PublicKeyCredentialDescriptor>,
+ ) -> GetAssertionResult {
+ let credential_data = cred.map(|cred| AttestedCredentialData {
+ aaguid: AAGuid::default(),
+ credential_id: cred.id.clone(),
+ credential_public_key: COSEKey {
+ alg: COSEAlgorithm::RS256,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: Curve::SECP256R1,
+ x: vec![],
+ y: vec![],
+ }),
+ },
+ });
+ GetAssertionResult {
+ assertion: Assertion {
+ credentials: cred.cloned(),
+ auth_data: AuthenticatorData {
+ rp_id_hash: rp.hash(),
+ flags: AuthenticatorDataFlags::empty(),
+ counter: 0,
+ credential_data,
+ extensions: Extension::default(),
+ },
+ signature: vec![],
+ user: None,
+ },
+ attachment: AuthenticatorAttachment::Platform,
+ extensions: AuthenticationExtensionsClientOutputs::default(),
+ }
+ }
+
+ fn new_check_key_handle<'a>(
+ rp: &'a RelyingParty,
+ client_data_hash: &'a ClientDataHash,
+ cred: &'a PublicKeyCredentialDescriptor,
+ ) -> CheckKeyHandle<'a> {
+ CheckKeyHandle {
+ key_handle: cred.id.as_ref(),
+ client_data_hash: client_data_hash.as_ref(),
+ rp,
+ }
+ }
+
+ #[test]
+ fn test_preflight_ctap1_empty() {
+ let mut dev = Device::new("preflight").unwrap();
+ make_device_simple_u2f(&mut dev);
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let rp = new_relying_party("preflight test");
+ let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash);
+ assert!(res.is_empty());
+ }
+
+ #[test]
+ fn test_preflight_ctap1_multiple_replies() {
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_simple_u2f(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let cdh = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![
+ new_credential(4, 4),
+ new_credential(3, 4),
+ new_credential(2, 4),
+ new_credential(1, 4),
+ ];
+ dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[0]));
+ dev.add_upcoming_ctap_error(HIDError::ApduStatus(
+ ApduErrorStatus::WrongData, // Not a registered cred
+ ));
+ dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1]));
+ dev.add_upcoming_ctap_error(HIDError::ApduStatus(
+ ApduErrorStatus::WrongData, // Not a registered cred
+ ));
+ dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2]));
+ dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one
+
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh);
+ assert_eq!(res, vec![allow_list[2].clone()]);
+ }
+
+ #[test]
+ fn test_preflight_ctap1_too_long_entries() {
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_simple_u2f(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let cdh = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![
+ new_credential(4, 300), // ctap1 limit is 256
+ new_credential(3, 4),
+ new_credential(2, 4),
+ new_credential(1, 4),
+ ];
+ // allow_list[0] is filtered out due to its size
+ dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1]));
+ dev.add_upcoming_ctap_error(HIDError::ApduStatus(
+ ApduErrorStatus::WrongData, // Not a registered cred
+ ));
+ dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2]));
+ dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one
+
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh);
+ assert_eq!(res, vec![allow_list[2].clone()]);
+ }
+
+ #[test]
+ fn test_preflight_ctap2_empty() {
+ let mut dev = Device::new("preflight").unwrap();
+ make_device_with_pin(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash);
+ assert!(res.is_empty());
+ }
+
+ #[test]
+ fn test_preflight_ctap20_no_cred_data() {
+ // CTAP2.0 tokens are allowed to not send any credential-data in their
+ // response, if the allow-list is of length one. See https://github.com/mozilla/authenticator-rs/issues/319
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_with_pin(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![new_credential(1, 4)];
+ dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
+ dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, None)]);
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
+ assert_eq!(res, allow_list);
+ }
+
+ #[test]
+ fn test_preflight_ctap2_one_valid_entry() {
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_with_pin(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![new_credential(1, 4)];
+ dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
+ dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[0]))]);
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
+ assert_eq!(res, allow_list);
+ }
+
+ #[test]
+ fn test_preflight_ctap2_multiple_entries() {
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_with_pin(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![
+ new_credential(3, 4),
+ new_credential(2, 4),
+ new_credential(1, 4),
+ new_credential(0, 4),
+ ];
+ // Our test device doesn't say how many allow_list-entries it supports, so our code
+ // defaults to one. Thus three requests, with three answers. Only one of them
+ // valid.
+ dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[0].clone()]));
+ dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[1].clone()]));
+ dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[2].clone()]));
+ dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NoCredentials,
+ None,
+ )));
+ dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NoCredentials,
+ None,
+ )));
+ dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[2]))]);
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
+ assert_eq!(res, vec![allow_list[2].clone()]);
+ }
+
+ #[test]
+ fn test_preflight_ctap2_multiple_replies() {
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_with_pin(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![
+ new_credential(4, 4),
+ new_credential(3, 4),
+ new_credential(2, 4),
+ new_credential(1, 4),
+ ];
+ let mut info = dev.get_authenticator_info().unwrap().clone();
+ info.max_credential_count_in_list = Some(5);
+ dev.set_authenticator_info(info);
+ // Our test device now says that it supports 5 allow_list-entries,
+ // so we can send all of them in one request
+ dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
+ dev.add_upcoming_ctap_response(vec![
+ new_assertion_response(&rp, Some(&allow_list[1])),
+ new_assertion_response(&rp, Some(&allow_list[2])),
+ new_assertion_response(&rp, Some(&allow_list[3])),
+ ]);
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
+ assert_eq!(res, allow_list[1..].to_vec());
+ }
+
+ #[test]
+ fn test_preflight_ctap2_multiple_replies_some_invalid() {
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_with_pin(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![
+ new_credential(4, 4),
+ new_credential(3, 4),
+ new_credential(2, 4),
+ new_credential(1, 4),
+ ];
+ let mut info = dev.get_authenticator_info().unwrap().clone();
+ info.max_credential_count_in_list = Some(5);
+ dev.set_authenticator_info(info);
+ // Our test device now says that it supports 5 allow_list-entries,
+ // so we can send all of them in one request
+ dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
+ dev.add_upcoming_ctap_response(vec![
+ new_assertion_response(&rp, Some(&allow_list[1])),
+ new_assertion_response(&rp, None), // This will be ignored
+ new_assertion_response(&rp, Some(&allow_list[2])),
+ new_assertion_response(&rp, None), // This will be ignored
+ ]);
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
+ assert_eq!(res, allow_list[1..=2].to_vec());
+ }
+
+ #[test]
+ fn test_preflight_ctap2_too_long_entries() {
+ let mut dev = Device::new_skipping_serialization("preflight").unwrap();
+ make_device_with_pin(&mut dev);
+ let rp = new_relying_party("preflight test");
+ let client_data_hash = ClientDataHash(Sha256::digest("").into());
+ let allow_list = vec![
+ new_credential(4, 50), // too long
+ new_credential(3, 4),
+ new_credential(2, 50), // too long
+ new_credential(1, 4),
+ ];
+ let mut info = dev.get_authenticator_info().unwrap().clone();
+ info.max_credential_count_in_list = Some(5);
+ info.max_credential_id_length = Some(20);
+ dev.set_authenticator_info(info);
+ // Our test device now says that it supports 5 allow_list-entries,
+ // so we can send all of them in one request, except for those
+ // that got pre-filtered, as they were too long.
+ dev.add_upcoming_ctap2_request(&new_silent_assert(
+ &rp,
+ &[allow_list[1].clone(), allow_list[3].clone()],
+ ));
+ dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[1]))]);
+ let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
+ assert_eq!(res, vec![allow_list[1].clone()]);
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/server.rs b/third_party/rust/authenticator/src/ctap2/server.rs
new file mode 100644
index 0000000000..fdf2637464
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/server.rs
@@ -0,0 +1,629 @@
+use crate::crypto::COSEAlgorithm;
+use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle};
+use base64::Engine;
+use serde::de::MapAccess;
+use serde::{
+ de::{Error as SerdeError, Unexpected, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::{ByteBuf, Bytes};
+use sha2::{Digest, Sha256};
+use std::convert::{Into, TryFrom};
+use std::fmt;
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
+pub struct RpIdHash(pub [u8; 32]);
+
+impl fmt::Debug for RpIdHash {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.0);
+ write!(f, "RpIdHash({value})")
+ }
+}
+
+impl AsRef<[u8]> for RpIdHash {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+impl RpIdHash {
+ pub fn from(src: &[u8]) -> Result<RpIdHash, AuthenticatorError> {
+ let mut payload = [0u8; 32];
+ if src.len() != payload.len() {
+ Err(AuthenticatorError::InvalidRelyingPartyInput)
+ } else {
+ payload.copy_from_slice(src);
+ Ok(RpIdHash(payload))
+ }
+ }
+}
+
+// NOTE: WebAuthn requires all fields and CTAP2 does not.
+#[derive(Debug, Serialize, Clone, Default, Deserialize, PartialEq, Eq)]
+pub struct RelyingParty {
+ pub id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option<String>,
+}
+
+impl RelyingParty {
+ pub fn from<S>(id: S) -> Self
+ where
+ S: Into<String>,
+ {
+ Self {
+ id: id.into(),
+ name: None,
+ }
+ }
+
+ pub fn hash(&self) -> RpIdHash {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.id);
+
+ let mut output = [0u8; 32];
+ output.copy_from_slice(hasher.finalize().as_slice());
+
+ RpIdHash(output)
+ }
+}
+
+// NOTE: WebAuthn requires all fields and CTAP2 does not.
+#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize, Default)]
+pub struct PublicKeyCredentialUserEntity {
+ #[serde(with = "serde_bytes")]
+ pub id: Vec<u8>,
+ pub name: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")]
+ pub display_name: Option<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublicKeyCredentialParameters {
+ pub alg: COSEAlgorithm,
+}
+
+impl TryFrom<i32> for PublicKeyCredentialParameters {
+ type Error = AuthenticatorError;
+ fn try_from(arg: i32) -> Result<Self, Self::Error> {
+ let alg = COSEAlgorithm::try_from(arg as i64)?;
+ Ok(PublicKeyCredentialParameters { alg })
+ }
+}
+
+impl Serialize for PublicKeyCredentialParameters {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(2))?;
+ map.serialize_entry("alg", &self.alg)?;
+ map.serialize_entry("type", "public-key")?;
+ map.end()
+ }
+}
+
+impl<'de> Deserialize<'de> for PublicKeyCredentialParameters {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct PublicKeyCredentialParametersVisitor;
+
+ impl<'de> Visitor<'de> for PublicKeyCredentialParametersVisitor {
+ type Value = PublicKeyCredentialParameters;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut found_type = false;
+ let mut alg = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ "alg" => {
+ if alg.is_some() {
+ return Err(SerdeError::duplicate_field("alg"));
+ }
+ alg = Some(map.next_value()?);
+ }
+ "type" => {
+ if found_type {
+ return Err(SerdeError::duplicate_field("type"));
+ }
+
+ let v: &str = map.next_value()?;
+ if v != "public-key" {
+ return Err(SerdeError::custom(format!("invalid value: {v}")));
+ }
+ found_type = true;
+ }
+ v => {
+ return Err(SerdeError::unknown_field(v, &[]));
+ }
+ }
+ }
+
+ if !found_type {
+ return Err(SerdeError::missing_field("type"));
+ }
+
+ let alg = alg.ok_or_else(|| SerdeError::missing_field("alg"))?;
+
+ Ok(PublicKeyCredentialParameters { alg })
+ }
+ }
+
+ deserializer.deserialize_bytes(PublicKeyCredentialParametersVisitor)
+ }
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Eq, Clone)]
+#[serde(rename_all = "lowercase")]
+pub enum Transport {
+ USB,
+ NFC,
+ BLE,
+ Internal,
+}
+
+impl From<AuthenticatorTransports> for Vec<Transport> {
+ fn from(t: AuthenticatorTransports) -> Self {
+ let mut transports = Vec::new();
+ if t.contains(AuthenticatorTransports::USB) {
+ transports.push(Transport::USB);
+ }
+ if t.contains(AuthenticatorTransports::NFC) {
+ transports.push(Transport::NFC);
+ }
+ if t.contains(AuthenticatorTransports::BLE) {
+ transports.push(Transport::BLE);
+ }
+
+ transports
+ }
+}
+
+pub type PublicKeyCredentialId = Vec<u8>;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublicKeyCredentialDescriptor {
+ pub id: PublicKeyCredentialId,
+ pub transports: Vec<Transport>,
+}
+
+impl Serialize for PublicKeyCredentialDescriptor {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // TODO(MS): Transports is OPTIONAL, but some older tokens don't understand it
+ // and return a CBOR-Parsing error. It is only a hint for the token,
+ // so we'll leave it out for the moment
+ let mut map = serializer.serialize_map(Some(2))?;
+ // let mut map = serializer.serialize_map(Some(3))?;
+ map.serialize_entry("id", Bytes::new(&self.id))?;
+ map.serialize_entry("type", "public-key")?;
+ // map.serialize_entry("transports", &self.transports)?;
+ map.end()
+ }
+}
+
+impl<'de> Deserialize<'de> for PublicKeyCredentialDescriptor {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct PublicKeyCredentialDescriptorVisitor;
+
+ impl<'de> Visitor<'de> for PublicKeyCredentialDescriptorVisitor {
+ type Value = PublicKeyCredentialDescriptor;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut found_type = false;
+ let mut id = None;
+ let mut transports = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ "id" => {
+ if id.is_some() {
+ return Err(SerdeError::duplicate_field("id"));
+ }
+ let id_bytes: ByteBuf = map.next_value()?;
+ id = Some(id_bytes.into_vec());
+ }
+ "transports" => {
+ if transports.is_some() {
+ return Err(SerdeError::duplicate_field("transports"));
+ }
+ transports = Some(map.next_value()?);
+ }
+ "type" => {
+ if found_type {
+ return Err(SerdeError::duplicate_field("type"));
+ }
+ let v: &str = map.next_value()?;
+ if v != "public-key" {
+ return Err(SerdeError::custom(format!("invalid value: {v}")));
+ }
+ found_type = true;
+ }
+ v => {
+ return Err(SerdeError::unknown_field(v, &[]));
+ }
+ }
+ }
+
+ if !found_type {
+ return Err(SerdeError::missing_field("type"));
+ }
+
+ let id = id.ok_or_else(|| SerdeError::missing_field("id"))?;
+ let transports = transports.unwrap_or_default();
+
+ Ok(PublicKeyCredentialDescriptor { id, transports })
+ }
+ }
+
+ deserializer.deserialize_any(PublicKeyCredentialDescriptorVisitor)
+ }
+}
+
+impl From<&KeyHandle> for PublicKeyCredentialDescriptor {
+ fn from(kh: &KeyHandle) -> Self {
+ Self {
+ id: kh.credential.clone(),
+ transports: kh.transports.into(),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum ResidentKeyRequirement {
+ Discouraged,
+ Preferred,
+ Required,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum UserVerificationRequirement {
+ Discouraged,
+ Preferred,
+ Required,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum CredentialProtectionPolicy {
+ UserVerificationOptional = 1,
+ UserVerificationOptionalWithCredentialIDList = 2,
+ UserVerificationRequired = 3,
+}
+
+impl Serialize for CredentialProtectionPolicy {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_u64(*self as u64)
+ }
+}
+
+impl<'de> Deserialize<'de> for CredentialProtectionPolicy {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct CredentialProtectionPolicyVisitor;
+
+ impl<'de> Visitor<'de> for CredentialProtectionPolicyVisitor {
+ type Value = CredentialProtectionPolicy;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("an integer")
+ }
+
+ fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ match v {
+ 1 => Ok(CredentialProtectionPolicy::UserVerificationOptional),
+ 2 => Ok(
+ CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList,
+ ),
+ 3 => Ok(CredentialProtectionPolicy::UserVerificationRequired),
+ _ => Err(SerdeError::invalid_value(
+ Unexpected::Unsigned(v),
+ &"valid CredentialProtectionPolicy",
+ )),
+ }
+ }
+ }
+
+ deserializer.deserialize_any(CredentialProtectionPolicyVisitor)
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct AuthenticationExtensionsClientInputs {
+ pub app_id: Option<String>,
+ pub cred_props: Option<bool>,
+ pub credential_protection_policy: Option<CredentialProtectionPolicy>,
+ pub enforce_credential_protection_policy: Option<bool>,
+ pub hmac_create_secret: Option<bool>,
+ pub min_pin_length: Option<bool>,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct CredentialProperties {
+ pub rk: bool,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct AuthenticationExtensionsClientOutputs {
+ pub app_id: Option<bool>,
+ pub cred_props: Option<CredentialProperties>,
+ pub hmac_create_secret: Option<bool>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum AuthenticatorAttachment {
+ CrossPlatform,
+ Platform,
+ Unknown,
+}
+
+#[cfg(test)]
+mod test {
+ use super::{
+ COSEAlgorithm, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
+ PublicKeyCredentialUserEntity, RelyingParty, Transport,
+ };
+ use serde_cbor::from_slice;
+
+ fn create_user() -> PublicKeyCredentialUserEntity {
+ PublicKeyCredentialUserEntity {
+ id: vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30,
+ 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
+ 0x01, 0x93, 0x30, 0x82,
+ ],
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: Some(String::from("John P. Smith")),
+ }
+ }
+ #[test]
+ fn serialize_rp() {
+ let rp = RelyingParty {
+ id: String::from("Acme"),
+ name: None,
+ };
+
+ let payload = ser::to_vec(&rp).unwrap();
+ assert_eq!(
+ &payload,
+ &[
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x64, // text(4)
+ 0x41, 0x63, 0x6d, 0x65
+ ]
+ );
+ }
+
+ #[test]
+ fn test_deserialize_user() {
+ // This includes an obsolete "icon" field to test that deserialization
+ // ignores it.
+ let input = vec![
+ 0xa4, // map(4)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid
+ 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ...
+ 0x30, 0x82, // ...
+ 0x64, // text(4)
+ 0x69, 0x63, 0x6f, 0x6e, // "icon"
+ 0x78, 0x2b, // text(43)
+ 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x70,
+ 0x69, // "https://pics.example.com/00/p/aBjjjpqPb.png"
+ 0x63, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // ...
+ 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, 0x2f, // ...
+ 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, // ...
+ 0x70, 0x6e, 0x67, // ...
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x76, // text(22)
+ 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74,
+ 0x68, // "johnpsmith@example.com"
+ 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ...
+ 0x6f, 0x6d, // ...
+ 0x6b, // text(11)
+ 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, // "displayName"
+ 0x65, // ...
+ 0x6d, // text(13)
+ 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, // "John P. Smith"
+ 0x69, 0x74, 0x68, // ...
+ ];
+ let expected = create_user();
+ let actual: PublicKeyCredentialUserEntity = from_slice(&input).unwrap();
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn serialize_user() {
+ let user = create_user();
+
+ let payload = ser::to_vec(&user).unwrap();
+ println!("payload = {payload:?}");
+ assert_eq!(
+ payload,
+ vec![
+ 0xa3, // map(3)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid
+ 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ...
+ 0x30, 0x82, // ...
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x76, // text(22)
+ 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74,
+ 0x68, // "johnpsmith@example.com"
+ 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ...
+ 0x6f, 0x6d, // ...
+ 0x6b, // text(11)
+ 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, // "displayName"
+ 0x65, // ...
+ 0x6d, // text(13)
+ 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, // "John P. Smith"
+ 0x69, 0x74, 0x68, // ...
+ ]
+ );
+ }
+
+ #[test]
+ fn serialize_user_nodisplayname() {
+ let user = PublicKeyCredentialUserEntity {
+ id: vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30,
+ 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
+ 0x01, 0x93, 0x30, 0x82,
+ ],
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: None,
+ };
+
+ let payload = ser::to_vec(&user).unwrap();
+ println!("payload = {payload:?}");
+ assert_eq!(
+ payload,
+ vec![
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid
+ 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ...
+ 0x30, 0x82, // ...
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x76, // text(22)
+ 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74,
+ 0x68, // "johnpsmith@example.com"
+ 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ...
+ 0x6f, 0x6d, // ...
+ ]
+ );
+ }
+
+ use serde_cbor::ser;
+
+ #[test]
+ fn public_key() {
+ let keys = vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ];
+
+ let payload = ser::to_vec(&keys);
+ println!("payload = {payload:?}");
+ let payload = payload.unwrap();
+ assert_eq!(
+ payload,
+ vec![
+ 0x82, // array(2)
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x26, // -7 (ES256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
+ 0x2D, 0x6B, 0x65, 0x79, // ...
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x39, 0x01, 0x00, // -257 (RS256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
+ 0x2D, 0x6B, 0x65, 0x79 // ...
+ ]
+ );
+ }
+
+ #[test]
+ fn public_key_desc() {
+ let key = PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+ 0x1c, 0x1d, 0x1e, 0x1f,
+ ],
+ transports: vec![Transport::BLE, Transport::USB],
+ };
+
+ let payload = ser::to_vec(&key);
+ println!("payload = {payload:?}");
+ let payload = payload.unwrap();
+
+ assert_eq!(
+ payload,
+ vec![
+ // 0xa3, // map(3)
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // key id
+ 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, // ...
+ 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, // ...
+ 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // ...
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // ...
+ 0x1e, 0x1f, // ...
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
+ 0x2D, 0x6B, 0x65,
+ 0x79, // ...
+
+ // Deactivated for now
+ //0x6a, // text(10)
+ //0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, // "transports"
+ //0x6f, 0x72, 0x74, 0x73, // ...
+ //0x82, // array(2)
+ //0x63, // text(3)
+ //0x62, 0x6c, 0x65, // "ble"
+ //0x63, // text(3)
+ //0x75, 0x73, 0x62 // "usb"
+ ]
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/utils.rs b/third_party/rust/authenticator/src/ctap2/utils.rs
new file mode 100644
index 0000000000..b5d84c7d34
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/utils.rs
@@ -0,0 +1,39 @@
+use serde::de;
+use serde_cbor::Deserializer;
+use std::io::Read;
+
+pub fn serde_parse_err<E: de::Error>(s: &str) -> E {
+ E::custom(format!("Failed to parse {s}"))
+}
+
+pub fn from_slice_stream<'a, T, R: Read, E: de::Error>(data: &mut R) -> Result<T, E>
+where
+ T: de::Deserialize<'a>,
+{
+ let mut deserializer = Deserializer::from_reader(data);
+ de::Deserialize::deserialize(&mut deserializer)
+ .map_err(|x| serde_parse_err(&format!("{}: {}", stringify!(T), &x.to_string())))
+}
+
+// Parsing routines
+
+pub fn read_be_u32<R: Read, E: de::Error>(data: &mut R) -> Result<u32, E> {
+ let mut buf = [0; 4];
+ data.read_exact(&mut buf)
+ .map_err(|_| serde_parse_err("u32"))?;
+ Ok(u32::from_be_bytes(buf))
+}
+
+pub fn read_be_u16<R: Read, E: de::Error>(data: &mut R) -> Result<u16, E> {
+ let mut buf = [0; 2];
+ data.read_exact(&mut buf)
+ .map_err(|_| serde_parse_err("u16"))?;
+ Ok(u16::from_be_bytes(buf))
+}
+
+pub fn read_byte<R: Read, E: de::Error>(data: &mut R) -> Result<u8, E> {
+ match data.bytes().next() {
+ Some(Ok(s)) => Ok(s),
+ _ => Err(serde_parse_err("u8")),
+ }
+}
diff --git a/third_party/rust/authenticator/src/errors.rs b/third_party/rust/authenticator/src/errors.rs
new file mode 100644
index 0000000000..0e3b969bb3
--- /dev/null
+++ b/third_party/rust/authenticator/src/errors.rs
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub use crate::ctap2::commands::{client_pin::PinError, CommandError};
+pub use crate::transport::errors::HIDError;
+use std::fmt;
+use std::io;
+use std::sync::mpsc;
+
+// This composite error type is patterned from Phil Daniels' blog:
+// https://www.philipdaniels.com/blog/2019/defining-rust-error-types/
+
+#[derive(Debug)]
+pub enum UnsupportedOption {
+ CredProtect,
+ EmptyAllowList,
+ MaxPinLength,
+ PubCredParams,
+ ResidentKey,
+ UserVerification,
+}
+
+#[derive(Debug)]
+pub enum AuthenticatorError {
+ // Errors from external libraries...
+ Io(io::Error),
+ // Errors raised by us...
+ InvalidRelyingPartyInput,
+ NoConfiguredTransports,
+ Platform,
+ InternalError(String),
+ U2FToken(U2FTokenError),
+ Custom(String),
+ VersionMismatch(&'static str, u32),
+ HIDError(HIDError),
+ CryptoError,
+ PinError(PinError),
+ UnsupportedOption(UnsupportedOption),
+ CancelledByUser,
+ CredentialExcluded,
+}
+
+impl std::error::Error for AuthenticatorError {}
+
+impl fmt::Display for AuthenticatorError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ AuthenticatorError::Io(ref err) => err.fmt(f),
+ AuthenticatorError::InvalidRelyingPartyInput => {
+ write!(f, "invalid input from relying party")
+ }
+ AuthenticatorError::NoConfiguredTransports => write!(
+ f,
+ "no transports were configured in the authenticator service"
+ ),
+ AuthenticatorError::Platform => write!(f, "unknown platform error"),
+ AuthenticatorError::InternalError(ref err) => write!(f, "internal error: {err}"),
+ AuthenticatorError::U2FToken(ref err) => {
+ write!(f, "A u2f token error occurred {err:?}")
+ }
+ AuthenticatorError::Custom(ref err) => write!(f, "A custom error occurred {err:?}"),
+ AuthenticatorError::VersionMismatch(manager, version) => {
+ write!(f, "{manager} expected arguments of version CTAP{version}")
+ }
+ AuthenticatorError::HIDError(ref e) => write!(f, "Device error: {e}"),
+ AuthenticatorError::CryptoError => {
+ write!(f, "The cryptography implementation encountered an error")
+ }
+ AuthenticatorError::PinError(ref e) => write!(f, "PIN Error: {e}"),
+ AuthenticatorError::UnsupportedOption(ref e) => {
+ write!(f, "Unsupported option: {e:?}")
+ }
+ AuthenticatorError::CancelledByUser => {
+ write!(f, "Cancelled by user.")
+ }
+ AuthenticatorError::CredentialExcluded => {
+ write!(f, "Credential excluded.")
+ }
+ }
+ }
+}
+
+impl From<io::Error> for AuthenticatorError {
+ fn from(err: io::Error) -> AuthenticatorError {
+ AuthenticatorError::Io(err)
+ }
+}
+
+impl From<HIDError> for AuthenticatorError {
+ fn from(err: HIDError) -> AuthenticatorError {
+ AuthenticatorError::HIDError(err)
+ }
+}
+
+impl From<CommandError> for AuthenticatorError {
+ fn from(err: CommandError) -> AuthenticatorError {
+ AuthenticatorError::HIDError(HIDError::Command(err))
+ }
+}
+
+impl<T> From<mpsc::SendError<T>> for AuthenticatorError {
+ fn from(err: mpsc::SendError<T>) -> AuthenticatorError {
+ AuthenticatorError::InternalError(err.to_string())
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub enum U2FTokenError {
+ Unknown = 1,
+ NotSupported = 2,
+ InvalidState = 3,
+ ConstraintError = 4,
+ NotAllowed = 5,
+}
+
+impl U2FTokenError {
+ fn as_str(&self) -> &str {
+ match *self {
+ U2FTokenError::Unknown => "unknown",
+ U2FTokenError::NotSupported => "not supported",
+ U2FTokenError::InvalidState => "invalid state",
+ U2FTokenError::ConstraintError => "constraint error",
+ U2FTokenError::NotAllowed => "not allowed",
+ }
+ }
+}
+
+impl std::fmt::Display for U2FTokenError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
+impl std::error::Error for U2FTokenError {}
diff --git a/third_party/rust/authenticator/src/lib.rs b/third_party/rust/authenticator/src/lib.rs
new file mode 100644
index 0000000000..5dd4133b8f
--- /dev/null
+++ b/third_party/rust/authenticator/src/lib.rs
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![allow(clippy::large_enum_variant)]
+#![allow(clippy::upper_case_acronyms)]
+#![allow(clippy::bool_to_int_with_if)]
+
+#[macro_use]
+mod util;
+
+#[cfg(target_os = "linux")]
+extern crate libudev;
+
+#[cfg(target_os = "freebsd")]
+extern crate devd_rs;
+
+#[cfg(target_os = "macos")]
+extern crate core_foundation;
+
+extern crate libc;
+#[macro_use]
+extern crate log;
+extern crate rand;
+extern crate runloop;
+
+#[macro_use]
+extern crate bitflags;
+
+mod consts;
+mod manager;
+mod statemachine;
+mod status_update;
+mod transport;
+mod u2ftypes;
+
+pub mod authenticatorservice;
+pub mod crypto;
+pub mod ctap2;
+pub mod errors;
+pub mod statecallback;
+pub use ctap2::attestation::AttestationObject;
+pub use ctap2::commands::bio_enrollment::BioEnrollmentResult;
+pub use ctap2::commands::client_pin::{Pin, PinError};
+pub use ctap2::commands::credential_management::CredentialManagementResult;
+pub use ctap2::commands::get_assertion::{Assertion, GetAssertionResult};
+pub use ctap2::commands::get_info::AuthenticatorInfo;
+pub use ctap2::commands::make_credentials::MakeCredentialsResult;
+use serde::Serialize;
+pub use statemachine::StateMachine;
+pub use status_update::{
+ BioEnrollmentCmd, CredManagementCmd, InteractiveRequest, InteractiveUpdate, StatusPinUv,
+ StatusUpdate,
+};
+pub use transport::{FidoDevice, FidoDeviceIO, FidoProtocol, VirtualFidoDevice};
+
+// Keep this in sync with the constants in u2fhid-capi.h.
+bitflags! {
+ pub struct RegisterFlags: u64 {
+ const REQUIRE_RESIDENT_KEY = 1;
+ const REQUIRE_USER_VERIFICATION = 2;
+ const REQUIRE_PLATFORM_ATTACHMENT = 4;
+ }
+}
+bitflags! {
+ pub struct SignFlags: u64 {
+ const REQUIRE_USER_VERIFICATION = 1;
+ }
+}
+bitflags! {
+ pub struct AuthenticatorTransports: u8 {
+ const USB = 1;
+ const NFC = 2;
+ const BLE = 4;
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct KeyHandle {
+ pub credential: Vec<u8>,
+ pub transports: AuthenticatorTransports,
+}
+
+pub type AppId = Vec<u8>;
+
+pub type RegisterResult = MakeCredentialsResult;
+pub type SignResult = GetAssertionResult;
+
+#[derive(Debug, Serialize)]
+pub enum ManageResult {
+ Success,
+ CredManagement(CredentialManagementResult),
+ BioEnrollment(BioEnrollmentResult),
+}
+
+pub type ResetResult = ();
+
+impl From<ResetResult> for ManageResult {
+ fn from(_value: ResetResult) -> Self {
+ ManageResult::Success
+ }
+}
+
+pub type Result<T> = std::result::Result<T, errors::AuthenticatorError>;
+
+#[cfg(test)]
+#[macro_use]
+extern crate assert_matches;
+
+#[cfg(fuzzing)]
+pub use consts::*;
+#[cfg(fuzzing)]
+pub use u2ftypes::*;
diff --git a/third_party/rust/authenticator/src/manager.rs b/third_party/rust/authenticator/src/manager.rs
new file mode 100644
index 0000000000..47e3a06c10
--- /dev/null
+++ b/third_party/rust/authenticator/src/manager.rs
@@ -0,0 +1,218 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::authenticatorservice::AuthenticatorTransport;
+use crate::authenticatorservice::{RegisterArgs, SignArgs};
+use crate::errors::*;
+use crate::statecallback::StateCallback;
+use crate::statemachine::StateMachine;
+use crate::Pin;
+use runloop::RunLoop;
+use std::io;
+use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
+use std::time::Duration;
+
+enum QueueAction {
+ Register {
+ timeout: u64,
+ register_args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ },
+ Sign {
+ timeout: u64,
+ sign_args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ },
+ Cancel,
+ Reset {
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ },
+ SetPin {
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ },
+ InteractiveManagement {
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ },
+}
+
+pub struct Manager {
+ queue: RunLoop,
+ tx: Sender<QueueAction>,
+}
+
+impl Manager {
+ pub fn new() -> io::Result<Self> {
+ let (tx, rx) = channel();
+
+ // Start a new work queue thread.
+ let queue = RunLoop::new(move |alive| {
+ let mut sm = StateMachine::new();
+
+ while alive() {
+ match rx.recv_timeout(Duration::from_millis(50)) {
+ Ok(QueueAction::Register {
+ timeout,
+ register_args,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.register(timeout, register_args, status, callback);
+ }
+
+ Ok(QueueAction::Sign {
+ timeout,
+ sign_args,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.sign(timeout, sign_args, status, callback);
+ }
+
+ Ok(QueueAction::Cancel) => {
+ // Cancelling must block so that we don't start a new
+ // polling thread before the old one has shut down.
+ sm.cancel();
+ }
+
+ Ok(QueueAction::Reset {
+ timeout,
+ status,
+ callback,
+ }) => {
+ // Reset the token: Delete all keypairs, reset PIN
+ sm.reset(timeout, status, callback);
+ }
+
+ Ok(QueueAction::SetPin {
+ timeout,
+ new_pin,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.set_pin(timeout, new_pin, status, callback);
+ }
+
+ Ok(QueueAction::InteractiveManagement {
+ timeout,
+ status,
+ callback,
+ }) => {
+ // Manage token interactively
+ sm.manage(timeout, status, callback);
+ }
+
+ Err(RecvTimeoutError::Disconnected) => {
+ break;
+ }
+
+ _ => { /* continue */ }
+ }
+ }
+
+ // Cancel any ongoing activity.
+ sm.cancel();
+ })?;
+
+ Ok(Self { queue, tx })
+ }
+}
+
+impl Drop for Manager {
+ fn drop(&mut self) {
+ self.queue.cancel();
+ }
+}
+
+impl AuthenticatorTransport for Manager {
+ fn register(
+ &mut self,
+ timeout: u64,
+ register_args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> Result<(), AuthenticatorError> {
+ let action = QueueAction::Register {
+ timeout,
+ register_args,
+ status,
+ callback,
+ };
+ Ok(self.tx.send(action)?)
+ }
+
+ fn sign(
+ &mut self,
+ timeout: u64,
+ sign_args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ let action = QueueAction::Sign {
+ timeout,
+ sign_args,
+ status,
+ callback,
+ };
+
+ self.tx.send(action)?;
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> Result<(), AuthenticatorError> {
+ Ok(self.tx.send(QueueAction::Cancel)?)
+ }
+
+ fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> Result<(), AuthenticatorError> {
+ Ok(self.tx.send(QueueAction::Reset {
+ timeout,
+ status,
+ callback,
+ })?)
+ }
+
+ fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ Ok(self.tx.send(QueueAction::SetPin {
+ timeout,
+ new_pin,
+ status,
+ callback,
+ })?)
+ }
+
+ fn manage(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ ) -> Result<(), AuthenticatorError> {
+ Ok(self.tx.send(QueueAction::InteractiveManagement {
+ timeout,
+ status,
+ callback,
+ })?)
+ }
+}
diff --git a/third_party/rust/authenticator/src/statecallback.rs b/third_party/rust/authenticator/src/statecallback.rs
new file mode 100644
index 0000000000..17f3881571
--- /dev/null
+++ b/third_party/rust/authenticator/src/statecallback.rs
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::sync::{Arc, Condvar, Mutex};
+
+pub struct StateCallback<T> {
+ callback: Arc<Mutex<Option<Box<dyn FnOnce(T) + Send>>>>,
+ observer: Arc<Mutex<Option<Box<dyn FnOnce() + Send>>>>,
+ condition: Arc<(Mutex<bool>, Condvar)>,
+}
+
+impl<T> StateCallback<T> {
+ // This is used for the Condvar, which requires this kind of construction
+ #[allow(clippy::mutex_atomic)]
+ pub fn new(cb: Box<dyn FnOnce(T) + Send>) -> Self {
+ Self {
+ callback: Arc::new(Mutex::new(Some(cb))),
+ observer: Arc::new(Mutex::new(None)),
+ condition: Arc::new((Mutex::new(true), Condvar::new())),
+ }
+ }
+
+ pub fn add_uncloneable_observer(&mut self, obs: Box<dyn FnOnce() + Send>) {
+ let mut opt = self.observer.lock().unwrap();
+ if opt.is_some() {
+ error!("Replacing an already-set observer.")
+ }
+ opt.replace(obs);
+ }
+
+ pub fn call(&self, rv: T) {
+ if let Some(cb) = self.callback.lock().unwrap().take() {
+ cb(rv);
+
+ if let Some(obs) = self.observer.lock().unwrap().take() {
+ obs();
+ }
+ }
+
+ let (lock, cvar) = &*self.condition;
+ let mut pending = lock.lock().unwrap();
+ *pending = false;
+ cvar.notify_all();
+ }
+
+ pub fn wait(&self) {
+ let (lock, cvar) = &*self.condition;
+ let _useless_guard = cvar
+ .wait_while(lock.lock().unwrap(), |pending| *pending)
+ .unwrap();
+ }
+}
+
+impl<T> Clone for StateCallback<T> {
+ fn clone(&self) -> Self {
+ Self {
+ callback: self.callback.clone(),
+ observer: Arc::new(Mutex::new(None)),
+ condition: self.condition.clone(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::StateCallback;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::sync::{Arc, Barrier};
+ use std::thread;
+
+ #[test]
+ fn test_statecallback_is_single_use() {
+ let counter = Arc::new(AtomicUsize::new(0));
+ let counter_clone = counter.clone();
+ let sc = StateCallback::new(Box::new(move |_| {
+ counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ assert_eq!(counter.load(Ordering::SeqCst), 0);
+ for _ in 0..10 {
+ sc.call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+
+ for _ in 0..10 {
+ sc.clone().call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+ }
+
+ #[test]
+ fn test_statecallback_observer_is_single_use() {
+ let counter = Arc::new(AtomicUsize::new(0));
+ let counter_clone = counter.clone();
+ let mut sc = StateCallback::<()>::new(Box::new(move |_| {}));
+
+ sc.add_uncloneable_observer(Box::new(move || {
+ counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ assert_eq!(counter.load(Ordering::SeqCst), 0);
+ for _ in 0..10 {
+ sc.call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+
+ for _ in 0..10 {
+ sc.clone().call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+ }
+
+ #[test]
+ fn test_statecallback_observer_only_runs_for_completing_callback() {
+ let cb_counter = Arc::new(AtomicUsize::new(0));
+ let cb_counter_clone = cb_counter.clone();
+ let sc = StateCallback::new(Box::new(move |_| {
+ cb_counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ let obs_counter = Arc::new(AtomicUsize::new(0));
+
+ for _ in 0..10 {
+ let obs_counter_clone = obs_counter.clone();
+ let mut c = sc.clone();
+ c.add_uncloneable_observer(Box::new(move || {
+ obs_counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ c.call(());
+
+ assert_eq!(cb_counter.load(Ordering::SeqCst), 1);
+ assert_eq!(obs_counter.load(Ordering::SeqCst), 1);
+ }
+ }
+
+ #[test]
+ #[allow(clippy::redundant_clone)]
+ fn test_statecallback_observer_unclonable() {
+ let mut sc = StateCallback::<()>::new(Box::new(move |_| {}));
+ sc.add_uncloneable_observer(Box::new(move || {}));
+
+ assert!(sc.observer.lock().unwrap().is_some());
+ // This is deliberate, to force an extra clone
+ assert!(sc.clone().observer.lock().unwrap().is_none());
+ }
+
+ #[test]
+ fn test_statecallback_wait() {
+ let sc = StateCallback::<()>::new(Box::new(move |_| {}));
+ let barrier = Arc::new(Barrier::new(2));
+
+ {
+ let c = sc.clone();
+ let b = barrier.clone();
+ thread::spawn(move || {
+ b.wait();
+ c.call(());
+ });
+ }
+
+ barrier.wait();
+ sc.wait();
+ }
+}
diff --git a/third_party/rust/authenticator/src/statemachine.rs b/third_party/rust/authenticator/src/statemachine.rs
new file mode 100644
index 0000000000..71bda827f0
--- /dev/null
+++ b/third_party/rust/authenticator/src/statemachine.rs
@@ -0,0 +1,427 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::authenticatorservice::{RegisterArgs, SignArgs};
+
+use crate::ctap2;
+
+use crate::ctap2::commands::client_pin::Pin;
+
+use crate::errors::AuthenticatorError;
+use crate::statecallback::StateCallback;
+use crate::status_update::{send_status, InteractiveUpdate};
+use crate::transport::device_selector::{
+ BlinkResult, Device, DeviceBuildParameters, DeviceCommand, DeviceSelectorEvent,
+};
+use crate::transport::platform::transaction::Transaction;
+use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol};
+use crate::{InteractiveRequest, ManageResult};
+use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
+
+use std::time::Duration;
+
+#[derive(Default)]
+pub struct StateMachine {
+ transaction: Option<Transaction>,
+}
+
+impl StateMachine {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ fn init_device(
+ info: DeviceBuildParameters,
+ selector: &Sender<DeviceSelectorEvent>,
+ ) -> Option<Device> {
+ // Create a new device.
+ let mut dev = match Device::new(info) {
+ Ok(dev) => dev,
+ Err((e, id)) => {
+ info!("error happened with device: {}", e);
+ let _ = selector.send(DeviceSelectorEvent::NotAToken(id));
+ return None;
+ }
+ };
+
+ // Try initializing it.
+ if let Err(e) = dev.init() {
+ warn!("error while initializing device: {}", e);
+ let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id()));
+ return None;
+ }
+
+ Some(dev)
+ }
+
+ fn wait_for_device_selector(
+ dev: &mut Device,
+ selector: &Sender<DeviceSelectorEvent>,
+ status: &Sender<crate::StatusUpdate>,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> bool {
+ let (tx, rx) = channel();
+ if selector
+ .send(DeviceSelectorEvent::ImAToken((dev.id(), tx)))
+ .is_err()
+ {
+ // If we fail to register with the device selector, then we're not going
+ // to be selected.
+ return false;
+ }
+
+ // We can be cancelled from the user (through keep_alive()) or from the device selector
+ // (through a DeviceCommand::Cancel on rx). We'll combine those signals into a single
+ // predicate to pass to Device::block_and_blink.
+ let keep_blinking = || keep_alive() && !matches!(rx.try_recv(), Ok(DeviceCommand::Cancel));
+
+ // Blocking recv. DeviceSelector will tell us what to do
+ match rx.recv() {
+ Ok(DeviceCommand::Blink) => {
+ // The caller wants the user to choose a device. Send a status update and blink
+ // this device. NOTE: We send one status update per device, so the recipient should be
+ // prepared to receive the message multiple times.
+ send_status(status, crate::StatusUpdate::SelectDeviceNotice);
+ match dev.block_and_blink(&keep_blinking) {
+ BlinkResult::DeviceSelected => {
+ // User selected us. Let DeviceSelector know, so it can cancel all other
+ // outstanding open blink-requests. If we fail to send the SelectedToken
+ // message to the device selector, then don't consider this token as having
+ // been selected.
+ selector
+ .send(DeviceSelectorEvent::SelectedToken(dev.id()))
+ .is_ok()
+ }
+ BlinkResult::Cancelled => {
+ info!("Device {:?} was not selected", dev.id());
+ false
+ }
+ }
+ }
+ Ok(DeviceCommand::Cancel) => {
+ info!("Device {:?} was not selected", dev.id());
+ false
+ }
+ Ok(DeviceCommand::Removed) => {
+ info!("Device {:?} was removed", dev.id());
+ false
+ }
+ Ok(DeviceCommand::Continue) => true,
+ Err(_) => {
+ warn!("Error when trying to receive messages from DeviceSelector! Exiting.");
+ false
+ }
+ }
+ }
+
+ pub fn register(
+ &mut self,
+ timeout: u64,
+ args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+ let cbc = callback.clone();
+ let transaction = Transaction::new(
+ timeout,
+ cbc.clone(),
+ status,
+ move |info, selector, status, alive| {
+ let mut dev = match Self::init_device(info, &selector) {
+ Some(dev) => dev,
+ None => return,
+ };
+ if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) {
+ return;
+ };
+
+ if args.use_ctap1_fallback {
+ dev.downgrade_to_ctap1();
+ }
+
+ info!("Device {:?} continues with the register process", dev.id());
+ if ctap2::register(&mut dev, args.clone(), status, callback.clone(), alive) {
+ // ctap2::register returns true if it called the callback with Ok(..). Cancel
+ // all other tokens in case we skipped the "blinking"-action and went straight
+ // for the actual request.
+ let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
+ }
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
+ }
+
+ pub fn sign(
+ &mut self,
+ timeout: u64,
+ args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ callback.clone(),
+ status,
+ move |info, selector, status, alive| {
+ let mut dev = match Self::init_device(info, &selector) {
+ Some(dev) => dev,
+ None => return,
+ };
+ if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) {
+ return;
+ };
+
+ if args.use_ctap1_fallback {
+ dev.downgrade_to_ctap1();
+ }
+
+ info!("Device {:?} continues with the signing process", dev.id());
+ if ctap2::sign(&mut dev, args.clone(), status, callback.clone(), alive) {
+ // ctap2::sign returns true if it called the callback with Ok(..). Cancel all
+ // other tokens in case we skipped the "blinking"-action and went straight for
+ // the actual request.
+ let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
+ }
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+
+ // This blocks.
+ pub fn cancel(&mut self) {
+ if let Some(mut transaction) = self.transaction.take() {
+ info!("Statemachine was cancelled. Cancelling transaction now.");
+ transaction.cancel();
+ }
+ }
+
+ pub fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ callback.clone(),
+ status,
+ move |info, selector, status, alive| {
+ let mut dev = match Self::init_device(info, &selector) {
+ Some(dev) => dev,
+ None => return,
+ };
+
+ if dev.get_protocol() != FidoProtocol::CTAP2 {
+ info!("Device does not support CTAP2");
+ let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id()));
+ return;
+ }
+
+ if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) {
+ return;
+ };
+ ctap2::reset_helper(&mut dev, selector, status, callback.clone(), alive);
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+
+ pub fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ callback.clone(),
+ status,
+ move |info, selector, status, alive| {
+ let mut dev = match Self::init_device(info, &selector) {
+ Some(dev) => dev,
+ None => return,
+ };
+
+ if dev.get_protocol() != FidoProtocol::CTAP2 {
+ info!("Device does not support CTAP2");
+ let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id()));
+ return;
+ }
+
+ if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) {
+ return;
+ };
+
+ ctap2::set_or_change_pin_helper(
+ &mut dev,
+ None,
+ new_pin.clone(),
+ status,
+ callback.clone(),
+ alive,
+ );
+ },
+ );
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+
+ // Function to interactively manage a specific token.
+ // Difference to register/sign: These want to do something and don't care
+ // with which token they do it.
+ // This function wants to manipulate a specific token. For this, we first
+ // have to select one and then do something with it, based on what it
+ // supports (Set PIN, Change PIN, Reset, etc.).
+ // Hence, we first go through the discovery-phase, then provide the user
+ // with the AuthenticatorInfo and then let them interactively decide what to do
+ pub fn manage(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ callback.clone(),
+ status,
+ move |info, selector, status, alive| {
+ let mut dev = match Self::init_device(info, &selector) {
+ Some(dev) => dev,
+ None => return,
+ };
+
+ if dev.get_protocol() != FidoProtocol::CTAP2 {
+ info!("Device does not support CTAP2");
+ let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id()));
+ return;
+ }
+
+ if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) {
+ return;
+ };
+
+ info!("Device {:?} selected for interactive management.", dev.id());
+
+ // Sending the user the info about the token
+ let (tx, rx) = channel();
+ send_status(
+ &status,
+ crate::StatusUpdate::InteractiveManagement(InteractiveUpdate::StartManagement(
+ (tx, dev.get_authenticator_info().cloned()),
+ )),
+ );
+ while alive() {
+ match rx.recv_timeout(Duration::from_millis(400)) {
+ Ok(InteractiveRequest::Quit) => {
+ callback.call(Ok(ManageResult::Success));
+ break;
+ }
+ Ok(InteractiveRequest::Reset) => {
+ ctap2::reset_helper(
+ &mut dev,
+ selector,
+ status,
+ callback.clone(),
+ alive,
+ );
+ }
+ Ok(InteractiveRequest::ChangePIN(curr_pin, new_pin)) => {
+ ctap2::set_or_change_pin_helper(
+ &mut dev,
+ Some(curr_pin),
+ new_pin,
+ status,
+ callback.clone(),
+ alive,
+ );
+ }
+ Ok(InteractiveRequest::SetPIN(pin)) => {
+ ctap2::set_or_change_pin_helper(
+ &mut dev,
+ None,
+ pin,
+ status,
+ callback.clone(),
+ alive,
+ );
+ }
+ Ok(InteractiveRequest::ChangeConfig(authcfg, puat)) => {
+ ctap2::configure_authenticator(
+ &mut dev,
+ puat,
+ authcfg,
+ status.clone(),
+ callback.clone(),
+ alive,
+ );
+ continue;
+ }
+ Ok(InteractiveRequest::CredentialManagement(cred_management, puat)) => {
+ ctap2::credential_management(
+ &mut dev,
+ puat,
+ cred_management,
+ status.clone(),
+ callback.clone(),
+ alive,
+ );
+ continue;
+ }
+ Ok(InteractiveRequest::BioEnrollment(bio_enrollment, puat)) => {
+ ctap2::bio_enrollment(
+ &mut dev,
+ puat,
+ bio_enrollment,
+ status.clone(),
+ callback.clone(),
+ alive,
+ );
+ continue;
+ }
+ Err(RecvTimeoutError::Timeout) => {
+ if !alive() {
+ // We got stopped at some point
+ callback.call(Err(AuthenticatorError::CancelledByUser));
+ break;
+ }
+ continue;
+ }
+ Err(RecvTimeoutError::Disconnected) => {
+ // recv() failed, because the other side is dropping the Sender.
+ info!(
+ "Callback dropped the channel, so we abort the interactive session"
+ );
+ callback.call(Err(AuthenticatorError::CancelledByUser));
+ }
+ }
+ break;
+ }
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+}
diff --git a/third_party/rust/authenticator/src/status_update.rs b/third_party/rust/authenticator/src/status_update.rs
new file mode 100644
index 0000000000..c4a7fee75a
--- /dev/null
+++ b/third_party/rust/authenticator/src/status_update.rs
@@ -0,0 +1,117 @@
+use super::Pin;
+use crate::{
+ ctap2::{
+ commands::{
+ authenticator_config::{AuthConfigCommand, AuthConfigResult},
+ bio_enrollment::BioTemplateId,
+ get_info::AuthenticatorInfo,
+ PinUvAuthResult,
+ },
+ server::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity},
+ },
+ BioEnrollmentResult, CredentialManagementResult,
+};
+use serde::{Deserialize, Serialize as DeriveSer, Serializer};
+use std::sync::mpsc::Sender;
+
+#[derive(Debug, Deserialize, DeriveSer)]
+pub enum CredManagementCmd {
+ GetCredentials,
+ DeleteCredential(PublicKeyCredentialDescriptor),
+ UpdateUserInformation(PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity),
+}
+
+#[derive(Debug, Deserialize, DeriveSer)]
+pub enum BioEnrollmentCmd {
+ GetFingerprintSensorInfo,
+ GetEnrollments,
+ StartNewEnrollment(Option<String>),
+ DeleteEnrollment(BioTemplateId),
+ ChangeName(BioTemplateId, String),
+}
+
+#[derive(Debug)]
+pub enum InteractiveRequest {
+ Quit,
+ Reset,
+ ChangePIN(Pin, Pin),
+ SetPIN(Pin),
+ ChangeConfig(AuthConfigCommand, Option<PinUvAuthResult>),
+ CredentialManagement(CredManagementCmd, Option<PinUvAuthResult>),
+ BioEnrollment(BioEnrollmentCmd, Option<PinUvAuthResult>),
+}
+
+// Simply ignoring the Sender when serializing
+pub(crate) fn serialize_pin_required<S>(_: &Sender<Pin>, s: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ s.serialize_none()
+}
+
+// Simply ignoring the Sender when serializing
+pub(crate) fn serialize_pin_invalid<S>(
+ _: &Sender<Pin>,
+ retries: &Option<u8>,
+ s: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ if let Some(r) = retries {
+ s.serialize_u8(*r)
+ } else {
+ s.serialize_none()
+ }
+}
+
+#[derive(Debug, DeriveSer)]
+pub enum StatusPinUv {
+ #[serde(serialize_with = "serialize_pin_required")]
+ PinRequired(Sender<Pin>),
+ #[serde(serialize_with = "serialize_pin_invalid")]
+ InvalidPin(Sender<Pin>, Option<u8>),
+ PinIsTooShort,
+ PinIsTooLong(usize),
+ InvalidUv(Option<u8>),
+ // This SHOULD ever only happen for CTAP2.0 devices that
+ // use internal UV (e.g. fingerprint sensors) and failed (e.g. wrong
+ // finger used).
+ // PinAuthInvalid, // Folded into InvalidUv
+ PinAuthBlocked,
+ PinBlocked,
+ PinNotSet,
+ UvBlocked,
+}
+
+#[derive(Debug)]
+pub enum InteractiveUpdate {
+ StartManagement((Sender<InteractiveRequest>, Option<AuthenticatorInfo>)),
+ // We provide the already determined PUAT to be able to issue more requests without
+ // forcing the user to enter another PIN.
+ BioEnrollmentUpdate((BioEnrollmentResult, Option<PinUvAuthResult>)),
+ CredentialManagementUpdate((CredentialManagementResult, Option<PinUvAuthResult>)),
+ AuthConfigUpdate((AuthConfigResult, Option<PinUvAuthResult>)),
+}
+
+#[derive(Debug)]
+pub enum StatusUpdate {
+ /// We're waiting for the user to touch their token
+ PresenceRequired,
+ /// Sent if a PIN is needed (or was wrong), or some other kind of PIN-related
+ /// error occurred. The Sender is for sending back a PIN (if needed).
+ PinUvError(StatusPinUv),
+ /// Sent, if multiple devices are found and the user has to select one
+ SelectDeviceNotice,
+ /// Sent when a token was selected for interactive management
+ InteractiveManagement(InteractiveUpdate),
+ /// Sent when a token returns multiple results for a getAssertion request
+ SelectResultNotice(Sender<Option<usize>>, Vec<PublicKeyCredentialUserEntity>),
+}
+
+pub(crate) fn send_status(status: &Sender<StatusUpdate>, msg: StatusUpdate) {
+ match status.send(msg) {
+ Ok(_) => {}
+ Err(e) => error!("Couldn't send status: {:?}", e),
+ };
+}
diff --git a/third_party/rust/authenticator/src/transport/device_selector.rs b/third_party/rust/authenticator/src/transport/device_selector.rs
new file mode 100644
index 0000000000..f745b456d9
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/device_selector.rs
@@ -0,0 +1,477 @@
+use crate::transport::hid::HIDDevice;
+
+pub use crate::transport::platform::device::Device;
+
+use runloop::RunLoop;
+use std::collections::{HashMap, HashSet};
+use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
+use std::time::Duration;
+
+pub type DeviceID = <Device as HIDDevice>::Id;
+pub type DeviceBuildParameters = <Device as HIDDevice>::BuildParameters;
+
+trait DeviceSelectorEventMarker {}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum BlinkResult {
+ DeviceSelected,
+ Cancelled,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DeviceCommand {
+ Blink,
+ Cancel,
+ Continue,
+ Removed,
+}
+
+#[derive(Debug)]
+pub enum DeviceSelectorEvent {
+ Cancel,
+ Timeout,
+ DevicesAdded(Vec<DeviceID>),
+ DeviceRemoved(DeviceID),
+ NotAToken(DeviceID),
+ ImAToken((DeviceID, Sender<DeviceCommand>)),
+ SelectedToken(DeviceID),
+}
+
+pub struct DeviceSelector {
+ /// How to send a message to the event loop
+ sender: Sender<DeviceSelectorEvent>,
+ /// Thread of the event loop
+ runloop: RunLoop,
+}
+
+impl DeviceSelector {
+ pub fn run() -> Self {
+ let (selector_send, selector_rec) = channel();
+ // let new_device_callback = Arc::new(new_device_cb);
+ let runloop = RunLoop::new(move |alive| {
+ let mut blinking = false;
+ // Device was added, but we wait for its response, if it is a token or not
+ // We save both a write-only copy of the device (for cancellation) and it's thread
+ let mut waiting_for_response = HashSet::new();
+ // Device IDs of devices that responded with "ImAToken" mapping to channels that are
+ // waiting to receive a DeviceCommand
+ let mut tokens = HashMap::new();
+ while alive() {
+ let d = Duration::from_secs(100);
+ let res = match selector_rec.recv_timeout(d) {
+ Err(RecvTimeoutError::Disconnected) => {
+ break;
+ }
+ Err(RecvTimeoutError::Timeout) => DeviceSelectorEvent::Timeout,
+ Ok(res) => res,
+ };
+
+ match res {
+ DeviceSelectorEvent::Timeout | DeviceSelectorEvent::Cancel => {
+ /* TODO */
+ Self::cancel_all(tokens, None);
+ break;
+ }
+ DeviceSelectorEvent::SelectedToken(ref id) => {
+ Self::cancel_all(tokens, Some(id));
+ break; // We are done here. The selected device continues without us.
+ }
+ DeviceSelectorEvent::DevicesAdded(ids) => {
+ for id in ids {
+ debug!("Device added event: {:?}", id);
+ waiting_for_response.insert(id);
+ }
+ continue;
+ }
+ DeviceSelectorEvent::DeviceRemoved(ref id) => {
+ debug!("Device removed event: {:?}", id);
+ if !waiting_for_response.remove(id) {
+ // Note: We _could_ check here if we had multiple tokens and are already blinking
+ // and the removal of this one leads to only one token left. So we could in theory
+ // stop blinking and select it right away. At the moment, I think this is a
+ // too surprising behavior and therefore, we let the remaining device keep on blinking
+ // since the user could add yet another device, instead of using the remaining one.
+ tokens.iter().for_each(|(dev_id, tx)| {
+ if dev_id == id {
+ let _ = tx.send(DeviceCommand::Removed);
+ }
+ });
+ tokens.retain(|dev_id, _| dev_id != id);
+ if tokens.is_empty() {
+ blinking = false;
+ continue;
+ }
+ }
+ // We are already blinking, so no need to run the code below this match
+ // that figures out if we should blink or not. In fact, currently, we do
+ // NOT want to run this code again, because if you have 2 blinking tokens
+ // and one got removed, we WANT the remaining one to continue blinking.
+ // This is a design choice, because I currently think it is the "less surprising"
+ // option to the user.
+ if blinking {
+ continue;
+ }
+ }
+ DeviceSelectorEvent::NotAToken(ref id) => {
+ debug!("Device not a token event: {:?}", id);
+ waiting_for_response.remove(id);
+ }
+ DeviceSelectorEvent::ImAToken((id, tx)) => {
+ let _ = waiting_for_response.remove(&id);
+ if blinking {
+ // We are already blinking, so this new device should blink too.
+ if tx.send(DeviceCommand::Blink).is_ok() {
+ tokens.insert(id, tx.clone());
+ }
+ continue;
+ } else {
+ tokens.insert(id, tx.clone());
+ }
+ }
+ }
+
+ // All known devices told us, whether they are tokens or not and we have at least one token
+ if waiting_for_response.is_empty() && !tokens.is_empty() {
+ if tokens.len() == 1 {
+ let (dev_id, tx) = tokens.drain().next().unwrap(); // We just checked that it can't be empty
+ if tx.send(DeviceCommand::Continue).is_err() {
+ // Device thread died in the meantime (which shouldn't happen).
+ // Tokens is empty, so we just start over again
+ continue;
+ }
+ Self::cancel_all(tokens, Some(&dev_id));
+ break; // We are done here
+ } else {
+ blinking = true;
+
+ tokens.iter().for_each(|(_dev, tx)| {
+ // A send operation can only fail if the receiving end of a channel is disconnected, implying that the data could never be received.
+ // We ignore errors here for now, but should probably remove the device in such a case (even though it theoretically can't happen)
+ let _ = tx.send(DeviceCommand::Blink);
+ });
+ }
+ }
+ }
+ });
+ Self {
+ runloop: runloop.unwrap(), // TODO
+ sender: selector_send,
+ }
+ }
+
+ pub fn clone_sender(&self) -> Sender<DeviceSelectorEvent> {
+ self.sender.clone()
+ }
+
+ fn cancel_all(tokens: HashMap<DeviceID, Sender<DeviceCommand>>, exclude: Option<&DeviceID>) {
+ for (dev_id, tx) in tokens.iter() {
+ if Some(dev_id) != exclude {
+ let _ = tx.send(DeviceCommand::Cancel);
+ }
+ }
+ }
+
+ pub fn stop(&mut self) {
+ // We ignore a possible error here, since we don't really care
+ let _ = self.sender.send(DeviceSelectorEvent::Cancel);
+ self.runloop.cancel();
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::{
+ consts::Capability,
+ ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorOptions},
+ transport::FidoDevice,
+ u2ftypes::U2FDeviceInfo,
+ };
+
+ pub(crate) fn gen_info(id: String) -> U2FDeviceInfo {
+ U2FDeviceInfo {
+ vendor_name: String::from("ExampleVendor").into_bytes(),
+ device_name: id.into_bytes(),
+ version_interface: 1,
+ version_major: 3,
+ version_minor: 2,
+ version_build: 1,
+ cap_flags: Capability::WINK | Capability::CBOR | Capability::NMSG,
+ }
+ }
+
+ pub(crate) fn make_device_simple_u2f(dev: &mut Device) {
+ dev.set_device_info(gen_info(dev.id()));
+ dev.set_cid([1, 2, 3, 4]); // Need to set something other than broadcast
+ dev.downgrade_to_ctap1();
+ dev.create_channel();
+ }
+
+ pub(crate) fn make_device_with_pin(dev: &mut Device) {
+ dev.set_device_info(gen_info(dev.id()));
+ dev.set_cid([1, 2, 3, 4]); // Need to set something other than broadcast
+ dev.create_channel();
+ let info = AuthenticatorInfo {
+ options: AuthenticatorOptions {
+ client_pin: Some(true),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+ dev.set_authenticator_info(info);
+ }
+
+ fn send_i_am_token(dev: &Device, selector: &DeviceSelector) {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::ImAToken((
+ dev.id(),
+ dev.sender.clone().unwrap(),
+ )))
+ .unwrap();
+ }
+
+ fn send_no_token(dev: &Device, selector: &DeviceSelector) {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::NotAToken(dev.id()))
+ .unwrap()
+ }
+
+ fn remove_device(dev: &Device, selector: &DeviceSelector) {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::DeviceRemoved(dev.id()))
+ .unwrap();
+ assert_eq!(
+ dev.receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Removed
+ );
+ }
+
+ fn add_devices<'a, T>(iter: T, selector: &DeviceSelector)
+ where
+ T: Iterator<Item = &'a Device>,
+ {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::DevicesAdded(
+ iter.map(|f| f.id()).collect(),
+ ))
+ .unwrap();
+ }
+
+ #[test]
+ fn test_device_selector_one_token_no_late_adds() {
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_with_pin(&mut devices[2]);
+ let selector = DeviceSelector::run();
+
+ // Adding all
+ add_devices(devices.iter(), &selector);
+ devices.iter_mut().for_each(|d| {
+ if !d.is_u2f() {
+ send_no_token(d, &selector);
+ }
+ });
+
+ send_i_am_token(&devices[2], &selector);
+
+ assert_eq!(
+ devices[2].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Continue
+ );
+ }
+
+ // This test is mostly for testing stop() and clone_sender()
+ #[test]
+ fn test_device_selector_stop() {
+ let device = Device::new("device selector 1").unwrap();
+
+ let mut selector = DeviceSelector::run();
+
+ // Adding all
+ selector
+ .clone_sender()
+ .send(DeviceSelectorEvent::DevicesAdded(vec![device.id()]))
+ .unwrap();
+
+ selector
+ .clone_sender()
+ .send(DeviceSelectorEvent::NotAToken(device.id()))
+ .unwrap();
+ selector.stop();
+ }
+
+ #[test]
+ fn test_device_selector_all_pins_with_late_add() {
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ Device::new("device selector 5").unwrap(),
+ Device::new("device selector 6").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_with_pin(&mut devices[2]);
+ make_device_with_pin(&mut devices[4]);
+ make_device_with_pin(&mut devices[5]);
+
+ let selector = DeviceSelector::run();
+
+ // Adding all, except the last one (we simulate that this one is not yet plugged in)
+ add_devices(devices.iter().take(5), &selector);
+
+ // Interleave tokens and non-tokens
+ send_i_am_token(&devices[2], &selector);
+
+ devices.iter_mut().for_each(|d| {
+ if !d.is_u2f() {
+ send_no_token(d, &selector);
+ }
+ });
+
+ send_i_am_token(&devices[4], &selector);
+
+ // We added 2 devices that are tokens. They should get the blink-command now
+ assert_eq!(
+ devices[2].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ assert_eq!(
+ devices[4].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+
+ // Plug in late device
+ send_i_am_token(&devices[5], &selector);
+ assert_eq!(
+ devices[5].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ }
+
+ #[test]
+ fn test_device_selector_no_pins_late_mixed_adds() {
+ // Multiple tokes, none of them support a PIN
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ Device::new("device selector 5").unwrap(),
+ Device::new("device selector 6").unwrap(),
+ Device::new("device selector 7").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_simple_u2f(&mut devices[2]);
+ make_device_simple_u2f(&mut devices[4]);
+ make_device_simple_u2f(&mut devices[5]);
+
+ let selector = DeviceSelector::run();
+
+ // Adding all, except the last one (we simulate that this one is not yet plugged in)
+ add_devices(devices.iter().take(5), &selector);
+
+ // Interleave tokens and non-tokens
+ send_i_am_token(&devices[2], &selector);
+
+ devices.iter_mut().for_each(|d| {
+ if !d.is_u2f() {
+ send_no_token(d, &selector);
+ }
+ });
+
+ send_i_am_token(&devices[4], &selector);
+
+ // We added 2 devices that are tokens. They should get the blink-command now
+ assert_eq!(
+ devices[2].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ assert_eq!(
+ devices[4].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+
+ // Plug in late device
+ send_i_am_token(&devices[5], &selector);
+ assert_eq!(
+ devices[5].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ // Remove device again
+ remove_device(&devices[5], &selector);
+
+ // Now we add a token that has a PIN, it should not get "Continue" but "Blink"
+ make_device_with_pin(&mut devices[6]);
+ send_i_am_token(&devices[6], &selector);
+ assert_eq!(
+ devices[6].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ }
+
+ #[test]
+ fn test_device_selector_mixed_pins_remove_all() {
+ // Multiple tokes, none of them support a PIN, so we should get Continue-commands
+ // for all of them
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ Device::new("device selector 5").unwrap(),
+ Device::new("device selector 6").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_with_pin(&mut devices[2]);
+ make_device_with_pin(&mut devices[4]);
+ make_device_with_pin(&mut devices[5]);
+
+ let selector = DeviceSelector::run();
+
+ // Adding all, except the last one (we simulate that this one is not yet plugged in)
+ add_devices(devices.iter(), &selector);
+
+ devices.iter_mut().for_each(|d| {
+ if d.is_u2f() {
+ send_i_am_token(d, &selector);
+ } else {
+ send_no_token(d, &selector);
+ }
+ });
+
+ for idx in [2, 4, 5] {
+ assert_eq!(
+ devices[idx].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ }
+
+ // Remove all tokens
+ for idx in [2, 4, 5] {
+ remove_device(&devices[idx], &selector);
+ }
+
+ // Adding one again
+ send_i_am_token(&devices[4], &selector);
+
+ // This should now get a "Continue" instead of "Blinking", because it's the only device
+ assert_eq!(
+ devices[4].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Continue
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/errors.rs b/third_party/rust/authenticator/src/transport/errors.rs
new file mode 100644
index 0000000000..451c27d6e8
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/errors.rs
@@ -0,0 +1,98 @@
+use crate::consts::{SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, SW_WRONG_DATA, SW_WRONG_LENGTH};
+use crate::ctap2::commands::CommandError;
+use std::fmt;
+use std::io;
+use std::path;
+
+#[allow(unused)]
+#[derive(Debug, PartialEq, Eq)]
+pub enum ApduErrorStatus {
+ ConditionsNotSatisfied,
+ WrongData,
+ WrongLength,
+ Unknown([u8; 2]),
+}
+
+impl ApduErrorStatus {
+ pub fn from(status: [u8; 2]) -> Result<(), ApduErrorStatus> {
+ match status {
+ s if s == SW_NO_ERROR => Ok(()),
+ s if s == SW_CONDITIONS_NOT_SATISFIED => Err(ApduErrorStatus::ConditionsNotSatisfied),
+ s if s == SW_WRONG_DATA => Err(ApduErrorStatus::WrongData),
+ s if s == SW_WRONG_LENGTH => Err(ApduErrorStatus::WrongLength),
+ other => Err(ApduErrorStatus::Unknown(other)),
+ }
+ }
+}
+
+impl fmt::Display for ApduErrorStatus {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ ApduErrorStatus::ConditionsNotSatisfied => write!(f, "Apdu: condition not satisfied"),
+ ApduErrorStatus::WrongData => write!(f, "Apdu: wrong data"),
+ ApduErrorStatus::WrongLength => write!(f, "Apdu: wrong length"),
+ ApduErrorStatus::Unknown(ref u) => write!(f, "Apdu: unknown error: {u:?}"),
+ }
+ }
+}
+
+#[allow(unused)]
+#[derive(Debug)]
+pub enum HIDError {
+ /// Transport replied with a status not expected
+ DeviceError,
+ UnexpectedInitReplyLen,
+ NonceMismatch,
+ DeviceNotInitialized,
+ DeviceNotSupported,
+ UnsupportedCommand,
+ UnexpectedVersion,
+ IO(Option<path::PathBuf>, io::Error),
+ UnexpectedCmd(u8),
+ Command(CommandError),
+ ApduStatus(ApduErrorStatus),
+}
+
+impl From<io::Error> for HIDError {
+ fn from(e: io::Error) -> HIDError {
+ HIDError::IO(None, e)
+ }
+}
+
+impl From<CommandError> for HIDError {
+ fn from(e: CommandError) -> HIDError {
+ HIDError::Command(e)
+ }
+}
+
+impl From<ApduErrorStatus> for HIDError {
+ fn from(e: ApduErrorStatus) -> HIDError {
+ HIDError::ApduStatus(e)
+ }
+}
+
+impl fmt::Display for HIDError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ HIDError::UnexpectedInitReplyLen => {
+ write!(f, "Error: Unexpected reply len when initilizaling")
+ }
+ HIDError::NonceMismatch => write!(f, "Error: Nonce mismatch"),
+ HIDError::DeviceError => write!(f, "Error: device returned error"),
+ HIDError::DeviceNotInitialized => write!(f, "Error: using not initiliazed device"),
+ HIDError::DeviceNotSupported => {
+ write!(f, "Error: requested operation is not available on device")
+ }
+ HIDError::UnexpectedVersion => write!(f, "Error: Unexpected protocol version"),
+ HIDError::UnsupportedCommand => {
+ write!(f, "Error: command is not supported on this device")
+ }
+ HIDError::IO(ref p, ref e) => write!(f, "Error: Ioerror({p:?}): {e}"),
+ HIDError::Command(ref e) => write!(f, "Error: Error issuing command: {e}"),
+ HIDError::UnexpectedCmd(s) => write!(f, "Error: Unexpected status: {s}"),
+ HIDError::ApduStatus(ref status) => {
+ write!(f, "Error: Unexpected apdu status: {status:?}")
+ }
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/device.rs b/third_party/rust/authenticator/src/transport/freebsd/device.rs
new file mode 100644
index 0000000000..0e8c7d20a7
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/device.rs
@@ -0,0 +1,235 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use crate::consts::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::uhid;
+use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret};
+use crate::u2ftypes::U2FDeviceInfo;
+use crate::util::from_unix_result;
+use crate::util::io_err;
+use std::ffi::{CString, OsString};
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::mem;
+use std::os::unix::prelude::*;
+
+#[derive(Debug)]
+pub struct Device {
+ path: OsString,
+ fd: libc::c_int,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<SharedSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+ protocol: FidoProtocol,
+}
+
+impl Device {
+ fn ping(&mut self) -> io::Result<()> {
+ for i in 0..10 {
+ let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE];
+
+ buf[0] = 0; // report number
+ buf[1] = 0xff; // CID_BROADCAST
+ buf[2] = 0xff;
+ buf[3] = 0xff;
+ buf[4] = 0xff;
+ buf[5] = 0x81; // ping
+ buf[6] = 0;
+ buf[7] = 1; // one byte
+
+ if self.write(&buf)? != buf.len() {
+ return Err(io_err("write ping failed"));
+ }
+
+ // Wait for response
+ let mut pfd: libc::pollfd = unsafe { mem::zeroed() };
+ pfd.fd = self.fd;
+ pfd.events = libc::POLLIN;
+ let nfds = unsafe { libc::poll(&mut pfd, 1, 100) };
+ if nfds == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ if nfds == 0 {
+ debug!("device timeout {}", i);
+ continue;
+ }
+
+ // Read response. When reports come in they are all
+ // exactly the same size, with no report id byte because
+ // there is only one report.
+ let n = self.read(&mut buf[1..])?;
+ if n != buf.len() - 1 {
+ return Err(io_err("read pong failed"));
+ }
+
+ return Ok(());
+ }
+
+ Err(io_err("no response from device"))
+ }
+}
+
+impl Drop for Device {
+ fn drop(&mut self) {
+ // Close the fd, ignore any errors.
+ let _ = unsafe { libc::close(self.fd) };
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let bufp = buf.as_mut_ptr() as *mut libc::c_void;
+ let rv = unsafe { libc::read(self.fd, bufp, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ let report_id = buf[0] as i64;
+ // Skip report number when not using numbered reports.
+ let start = if report_id == 0x0 { 1 } else { 0 };
+ let data = &buf[start..];
+
+ let data_ptr = data.as_ptr() as *const libc::c_void;
+ let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) };
+ from_unix_result(rv as usize + 1)
+ }
+
+ // USB HID writes don't buffer, so this will be a nop.
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = OsString;
+ type Id = OsString;
+
+ fn new(path: OsString) -> Result<Self, (HIDError, Self::Id)> {
+ let cstr =
+ CString::new(path.as_bytes()).map_err(|_| (HIDError::DeviceError, path.clone()))?;
+ let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) };
+ let fd = from_unix_result(fd).map_err(|e| (e.into(), path.clone()))?;
+ let mut res = Self {
+ path,
+ fd,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ protocol: FidoProtocol::CTAP2,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ HIDDevice::pre_init(self)
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ HIDDevice::get_device_info(self)
+ .cap_flags
+ .contains(Capability::CBOR)
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ if !uhid::is_u2f_device(self.fd) {
+ return false;
+ }
+ if self.ping().is_err() {
+ return false;
+ }
+ true
+ }
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: SharedSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1;
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/mod.rs b/third_party/rust/authenticator/src/transport/freebsd/mod.rs
new file mode 100644
index 0000000000..7ed5727157
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod monitor;
+mod uhid;
diff --git a/third_party/rust/authenticator/src/transport/freebsd/monitor.rs b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs
new file mode 100644
index 0000000000..340ebef836
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use devd_rs;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::ffi::OsString;
+use std::sync::{mpsc::Sender, Arc};
+use std::{fs, io};
+
+const POLL_TIMEOUT: usize = 100;
+
+pub enum Event {
+ Add(OsString),
+ Remove(OsString),
+}
+
+impl Event {
+ fn from_devd(event: devd_rs::Event) -> Option<Self> {
+ match event {
+ devd_rs::Event::Attach {
+ ref dev,
+ parent: _,
+ location: _,
+ } if dev.starts_with("uhid") => Some(Event::Add(("/dev/".to_owned() + dev).into())),
+ devd_rs::Event::Detach {
+ ref dev,
+ parent: _,
+ location: _,
+ } if dev.starts_with("uhid") => Some(Event::Remove(("/dev/".to_owned() + dev).into())),
+ _ => None,
+ }
+ }
+}
+
+fn convert_error(e: devd_rs::Error) -> io::Error {
+ e.into()
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Sync,
+{
+ runloops: HashMap<OsString, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ let mut ctx = devd_rs::Context::new().map_err(convert_error)?;
+
+ let mut initial_devs = Vec::new();
+ // Iterate all existing devices.
+ for dev in (fs::read_dir("/dev")?).flatten() {
+ let filename_ = dev.file_name();
+ let filename = filename_.to_str().unwrap_or("");
+ if filename.starts_with("uhid") {
+ let path = OsString::from("/dev/".to_owned() + filename);
+ initial_devs.push(path.clone());
+ self.add_device(path);
+ }
+ }
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(initial_devs));
+
+ // Loop until we're stopped by the controlling thread, or fail.
+ while alive() {
+ // Wait for new events, break on failure.
+ match ctx.wait_for_event(POLL_TIMEOUT) {
+ Err(devd_rs::Error::Timeout) => (),
+ Err(e) => return Err(convert_error(e).into()),
+ Ok(event) => {
+ if let Some(event) = Event::from_devd(event) {
+ self.process_event(event);
+ }
+ }
+ }
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn process_event(&mut self, event: Event) {
+ match event {
+ Event::Add(path) => {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()]));
+ self.add_device(path);
+ }
+ Event::Remove(path) => {
+ self.remove_device(path);
+ }
+ }
+ }
+
+ fn add_device(&mut self, path: OsString) {
+ let f = self.new_device_cb.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ let key = path.clone();
+ debug!("Adding device {}", key.to_string_lossy());
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(path, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: OsString) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path.to_string_lossy());
+ if let Some(runloop) = self.runloops.remove(&path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/transaction.rs b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs
new file mode 100644
index 0000000000..6b15f6751a
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let device_selector = DeviceSelector::run();
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/uhid.rs b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs
new file mode 100644
index 0000000000..681b09a768
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::io;
+use std::os::unix::io::RawFd;
+use std::ptr;
+
+use crate::transport::hidproto::*;
+use crate::util::from_unix_result;
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+#[derive(Debug)]
+pub struct GenDescriptor {
+ ugd_data: *mut u8,
+ ugd_lang_id: u16,
+ ugd_maxlen: u16,
+ ugd_actlen: u16,
+ ugd_offset: u16,
+ ugd_config_index: u8,
+ ugd_string_index: u8,
+ ugd_iface_index: u8,
+ ugd_altif_index: u8,
+ ugd_endpt_index: u8,
+ ugd_report_index: u8,
+ reserved: [u8; 16],
+}
+
+impl Default for GenDescriptor {
+ fn default() -> GenDescriptor {
+ GenDescriptor {
+ ugd_data: ptr::null_mut(),
+ ugd_lang_id: 0,
+ ugd_maxlen: 65535,
+ ugd_actlen: 0,
+ ugd_offset: 0,
+ ugd_config_index: 0,
+ ugd_string_index: 0,
+ ugd_iface_index: 0,
+ ugd_altif_index: 0,
+ ugd_endpt_index: 0,
+ ugd_report_index: 0,
+ reserved: [0; 16],
+ }
+ }
+}
+
+const IOWR: u32 = 0x40000000 | 0x80000000;
+
+const IOCPARM_SHIFT: u32 = 13;
+const IOCPARM_MASK: u32 = (1 << IOCPARM_SHIFT) - 1;
+
+const TYPESHIFT: u32 = 8;
+const SIZESHIFT: u32 = 16;
+
+macro_rules! ioctl {
+ ($dir:expr, $name:ident, $ioty:expr, $nr:expr, $size:expr; $ty:ty) => {
+ pub unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> {
+ let ioc = ($dir as u32)
+ | (($size as u32 & IOCPARM_MASK) << SIZESHIFT)
+ | (($ioty as u32) << TYPESHIFT)
+ | ($nr as u32);
+ from_unix_result(libc::ioctl(fd, ioc as libc::c_ulong, val))
+ }
+ };
+}
+
+// https://github.com/freebsd/freebsd/blob/master/sys/dev/usb/usb_ioctl.h
+ioctl!(IOWR, usb_get_report_desc, b'U', 21, 32; /*struct*/ GenDescriptor);
+
+fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> {
+ let mut desc = GenDescriptor::default();
+ let _ = unsafe { usb_get_report_desc(fd, &mut desc)? };
+ desc.ugd_maxlen = desc.ugd_actlen;
+ let mut value = vec![0; desc.ugd_actlen as usize];
+ desc.ugd_data = value.as_mut_ptr();
+ let _ = unsafe { usb_get_report_desc(fd, &mut desc)? };
+ Ok(ReportDescriptor { value })
+}
+
+pub fn is_u2f_device(fd: RawFd) -> bool {
+ match read_report_descriptor(fd) {
+ Ok(desc) => has_fido_usage(desc),
+ Err(_) => false, // Upon failure, just say it's not a U2F device.
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/hid.rs b/third_party/rust/authenticator/src/transport/hid.rs
new file mode 100644
index 0000000000..339cd05d71
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/hid.rs
@@ -0,0 +1,252 @@
+use super::TestDevice;
+use crate::consts::{HIDCmd, CID_BROADCAST};
+use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, Retryable, StatusCode};
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol};
+use crate::u2ftypes::{U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp};
+use crate::util::io_err;
+use rand::{thread_rng, RngCore};
+use std::cmp::Eq;
+use std::fmt;
+use std::hash::Hash;
+use std::io;
+use std::io::{Read, Write};
+use std::thread;
+use std::time::Duration;
+
+pub trait HIDDevice: FidoDevice + Read + Write {
+ type BuildParameters: Sized;
+ type Id: fmt::Debug + PartialEq + Eq + Hash + Sized;
+
+ // Open device, verify that it is indeed a CTAP device and potentially read initial values
+ fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)>;
+ fn id(&self) -> Self::Id;
+
+ fn get_device_info(&self) -> U2FDeviceInfo;
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo);
+
+ // Channel ID management
+ fn get_cid(&self) -> &[u8; 4];
+ fn set_cid(&mut self, cid: [u8; 4]);
+
+ // HID report sizes
+ fn in_rpt_size(&self) -> usize;
+ fn out_rpt_size(&self) -> usize;
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String>;
+
+ // Initialize on a protocol-level
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ if self.initialized() {
+ return Ok(());
+ }
+
+ let mut nonce = [0u8; 8];
+ thread_rng().fill_bytes(&mut nonce);
+
+ // Send Init to broadcast address to create a new channel
+ self.set_cid(CID_BROADCAST);
+ let (cmd, raw) = HIDDevice::sendrecv(self, HIDCmd::Init, &nonce, &|| true)?;
+ if cmd != HIDCmd::Init {
+ return Err(HIDError::DeviceError);
+ }
+
+ let rsp = U2FHIDInitResp::read(&raw, &nonce)?;
+ // Set the new Channel ID
+ self.set_cid(rsp.cid);
+
+ let vendor = self
+ .get_property("Manufacturer")
+ .unwrap_or_else(|_| String::from("Unknown Vendor"));
+ let product = self
+ .get_property("Product")
+ .unwrap_or_else(|_| String::from("Unknown Device"));
+
+ let info = U2FDeviceInfo {
+ vendor_name: vendor.as_bytes().to_vec(),
+ device_name: product.as_bytes().to_vec(),
+ version_interface: rsp.version_interface,
+ version_major: rsp.version_major,
+ version_minor: rsp.version_minor,
+ version_build: rsp.version_build,
+ cap_flags: rsp.cap_flags,
+ };
+ debug!("{:?}: {:?}", self.id(), info);
+ self.set_device_info(info);
+
+ // A CTAPHID host SHALL accept a response size that is longer than the
+ // anticipated size to allow for future extensions of the protocol, yet
+ // maintaining backwards compatibility. Future versions will maintain
+ // the response structure of the current version, but additional fields
+ // may be added.
+
+ Ok(())
+ }
+
+ fn sendrecv(
+ &mut self,
+ cmd: HIDCmd,
+ send: &[u8],
+ keep_alive: &dyn Fn() -> bool,
+ ) -> io::Result<(HIDCmd, Vec<u8>)> {
+ self.u2f_write(cmd.into(), send)?;
+ debug!("sent to Device {:?} cmd={:?}: {:?}", self.id(), cmd, send);
+ loop {
+ let (cmd, data) = self.u2f_read()?;
+ if cmd != HIDCmd::Keepalive {
+ debug!(
+ "got from Device {:?} status={:?}: {:?}",
+ self.id(),
+ cmd,
+ data
+ );
+ return Ok((cmd, data));
+ }
+ // The authenticator might send us HIDCmd::Keepalive messages indefinitely, e.g. if
+ // it's waiting for user presence. The keep_alive function is used to cancel the
+ // transaction.
+ if !keep_alive() {
+ break;
+ }
+ }
+
+ // If this is a CTAP2 device we can tell the authenticator to cancel the transaction on its
+ // side as well. There's nothing to do for U2F/CTAP1 devices.
+ if self.get_protocol() == FidoProtocol::CTAP2 {
+ self.u2f_write(u8::from(HIDCmd::Cancel), &[])?;
+ }
+ // For CTAP2 devices we expect to read
+ // (HIDCmd::Cbor, [CTAP2_ERR_KEEPALIVE_CANCEL])
+ // for U2F/CTAP1 we expect to read
+ // (HIDCmd::Keepalive, [status]).
+ self.u2f_read()
+ }
+
+ fn u2f_write(&mut self, cmd: u8, send: &[u8]) -> io::Result<()> {
+ let mut count = U2FHIDInit::write(self, cmd, send)?;
+
+ // Send continuation packets.
+ let mut sequence = 0u8;
+ while count < send.len() {
+ count += U2FHIDCont::write(self, sequence, &send[count..])?;
+ sequence += 1;
+ }
+
+ Ok(())
+ }
+
+ fn u2f_read(&mut self) -> io::Result<(HIDCmd, Vec<u8>)> {
+ // Now we read. This happens in 2 chunks: The initial packet, which has
+ // the size we expect overall, then continuation packets, which will
+ // fill in data until we have everything.
+ let (cmd, data) = {
+ let (cmd, mut data) = U2FHIDInit::read(self)?;
+
+ trace!("init frame data read: {:04X?}", &data);
+ let mut sequence = 0u8;
+ while data.len() < data.capacity() {
+ let max = data.capacity() - data.len();
+ data.extend_from_slice(&U2FHIDCont::read(self, sequence, max)?);
+ sequence += 1;
+ }
+ (cmd, data)
+ };
+ trace!("u2f_read({:?}) cmd={:?}: {:04X?}", self.id(), cmd, &data);
+ Ok((cmd, data))
+ }
+}
+
+#[cfg(not(test))]
+impl<T: HIDDevice> TestDevice for T {}
+
+impl<T: HIDDevice + TestDevice> FidoDeviceIO for T {
+ fn send_msg_cancellable<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Out, HIDError> {
+ if !self.initialized() {
+ return Err(HIDError::DeviceNotInitialized);
+ }
+
+ match self.get_protocol() {
+ FidoProtocol::CTAP1 => self.send_ctap1_cancellable(msg, keep_alive),
+ FidoProtocol::CTAP2 => self.send_cbor_cancellable(msg, keep_alive),
+ }
+ }
+
+ fn send_cbor_cancellable<Req: RequestCtap2>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Req::Output, HIDError> {
+ debug!("sending {:?} to {:?}", msg, self);
+ #[cfg(test)]
+ {
+ if self.skip_serialization() {
+ return self.send_ctap2_unserialized(msg);
+ }
+ }
+
+ let mut data = msg.wire_format()?;
+ let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1);
+ // CTAP2 command
+ buf.push(msg.command() as u8);
+ // payload
+ buf.append(&mut data);
+ let buf = buf;
+
+ let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf, keep_alive)?;
+ if cmd == HIDCmd::Cbor {
+ Ok(msg.handle_response_ctap2(self, &resp)?)
+ } else {
+ Err(HIDError::UnexpectedCmd(cmd.into()))
+ }
+ }
+
+ fn send_ctap1_cancellable<Req: RequestCtap1>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Req::Output, HIDError> {
+ debug!("sending {:?} to {:?}", msg, self);
+ #[cfg(test)]
+ {
+ if self.skip_serialization() {
+ return self.send_ctap1_unserialized(msg);
+ }
+ }
+ let (data, add_info) = msg.ctap1_format()?;
+
+ while keep_alive() {
+ // sendrecv will not block with a CTAP1 device
+ let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data, &|| true)?;
+ if cmd == HIDCmd::Msg {
+ if data.len() < 2 {
+ return Err(io_err("Unexpected Response: shorter than expected").into());
+ }
+ let split_at = data.len() - 2;
+ let status = data.split_off(split_at);
+ // This will bubble up error if status != no error
+ let status = ApduErrorStatus::from([status[0], status[1]]);
+
+ match msg.handle_response_ctap1(self, status, &data, &add_info) {
+ Ok(out) => return Ok(out),
+ Err(Retryable::Retry) => {
+ // sleep 100ms then loop again
+ // TODO(baloo): meh, use tokio instead?
+ thread::sleep(Duration::from_millis(100));
+ }
+ Err(Retryable::Error(e)) => return Err(e),
+ }
+ } else {
+ return Err(HIDError::UnexpectedCmd(cmd.into()));
+ }
+ }
+
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::KeepaliveCancel,
+ None,
+ )))
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/hidproto.rs b/third_party/rust/authenticator/src/transport/hidproto.rs
new file mode 100644
index 0000000000..5a0a9d2605
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/hidproto.rs
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Shared code for platforms that use raw HID access (Linux, FreeBSD, etc.)
+
+#![cfg_attr(
+ feature = "cargo-clippy",
+ allow(clippy::cast_lossless, clippy::needless_lifetimes)
+)]
+
+#[cfg(target_os = "linux")]
+use std::io;
+use std::mem;
+
+use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID};
+#[cfg(target_os = "linux")]
+use crate::consts::{INIT_HEADER_SIZE, MAX_HID_RPT_SIZE};
+
+// The 4 MSBs (the tag) are set when it's a long item.
+const HID_MASK_LONG_ITEM_TAG: u8 = 0b1111_0000;
+// The 2 LSBs denote the size of a short item.
+const HID_MASK_SHORT_ITEM_SIZE: u8 = 0b0000_0011;
+// The 6 MSBs denote the tag (4) and type (2).
+const HID_MASK_ITEM_TAGTYPE: u8 = 0b1111_1100;
+// tag=0000, type=10 (local)
+const HID_ITEM_TAGTYPE_USAGE: u8 = 0b0000_1000;
+// tag=0000, type=01 (global)
+const HID_ITEM_TAGTYPE_USAGE_PAGE: u8 = 0b0000_0100;
+// tag=1000, type=00 (main)
+const HID_ITEM_TAGTYPE_INPUT: u8 = 0b1000_0000;
+// tag=1001, type=00 (main)
+const HID_ITEM_TAGTYPE_OUTPUT: u8 = 0b1001_0000;
+// tag=1001, type=01 (global)
+const HID_ITEM_TAGTYPE_REPORT_COUNT: u8 = 0b1001_0100;
+
+pub struct ReportDescriptor {
+ pub value: Vec<u8>,
+}
+
+impl ReportDescriptor {
+ fn iter(self) -> ReportDescriptorIterator {
+ ReportDescriptorIterator::new(self)
+ }
+}
+
+#[derive(Debug)]
+pub enum Data {
+ UsagePage { data: u32 },
+ Usage { data: u32 },
+ Input,
+ Output,
+ ReportCount { data: u32 },
+}
+
+pub struct ReportDescriptorIterator {
+ desc: ReportDescriptor,
+ pos: usize,
+}
+
+impl ReportDescriptorIterator {
+ fn new(desc: ReportDescriptor) -> Self {
+ Self { desc, pos: 0 }
+ }
+
+ fn next_item(&mut self) -> Option<Data> {
+ let item = get_hid_item(&self.desc.value[self.pos..]);
+ if item.is_none() {
+ self.pos = self.desc.value.len(); // Close, invalid data.
+ return None;
+ }
+
+ let (tag_type, key_len, data) = item.unwrap();
+
+ // Advance if we have a valid item.
+ self.pos += key_len + data.len();
+
+ // We only check short items.
+ if key_len > 1 {
+ return None; // Check next item.
+ }
+
+ // Short items have max. length of 4 bytes.
+ assert!(data.len() <= mem::size_of::<u32>());
+
+ // Convert data bytes to a uint.
+ let data = read_uint_le(data);
+ match tag_type {
+ HID_ITEM_TAGTYPE_USAGE_PAGE => Some(Data::UsagePage { data }),
+ HID_ITEM_TAGTYPE_USAGE => Some(Data::Usage { data }),
+ HID_ITEM_TAGTYPE_INPUT => Some(Data::Input),
+ HID_ITEM_TAGTYPE_OUTPUT => Some(Data::Output),
+ HID_ITEM_TAGTYPE_REPORT_COUNT => Some(Data::ReportCount { data }),
+ _ => None,
+ }
+ }
+}
+
+impl Iterator for ReportDescriptorIterator {
+ type Item = Data;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.pos >= self.desc.value.len() {
+ return None;
+ }
+
+ self.next_item().or_else(|| self.next())
+ }
+}
+
+fn get_hid_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> {
+ if (buf[0] & HID_MASK_LONG_ITEM_TAG) == HID_MASK_LONG_ITEM_TAG {
+ get_hid_long_item(buf)
+ } else {
+ get_hid_short_item(buf)
+ }
+}
+
+fn get_hid_long_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> {
+ // A valid long item has at least three bytes.
+ if buf.len() < 3 {
+ return None;
+ }
+
+ let len = buf[1] as usize;
+
+ // Ensure that there are enough bytes left in the buffer.
+ if len > buf.len() - 3 {
+ return None;
+ }
+
+ Some((buf[2], 3 /* key length */, &buf[3..]))
+}
+
+fn get_hid_short_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> {
+ // This is a short item. The bottom two bits of the key
+ // contain the length of the data section (value) for this key.
+ let len = match buf[0] & HID_MASK_SHORT_ITEM_SIZE {
+ s @ 0..=2 => s as usize,
+ _ => 4, /* _ == 3 */
+ };
+
+ // Ensure that there are enough bytes left in the buffer.
+ if len > buf.len() - 1 {
+ return None;
+ }
+
+ Some((
+ buf[0] & HID_MASK_ITEM_TAGTYPE,
+ 1, /* key length */
+ &buf[1..=len],
+ ))
+}
+
+fn read_uint_le(buf: &[u8]) -> u32 {
+ assert!(buf.len() <= 4);
+ // Parse the number in little endian byte order.
+ buf.iter()
+ .rev()
+ .fold(0, |num, b| (num << 8) | (u32::from(*b)))
+}
+
+pub fn has_fido_usage(desc: ReportDescriptor) -> bool {
+ let mut usage_page = None;
+ let mut usage = None;
+
+ for data in desc.iter() {
+ match data {
+ Data::UsagePage { data } => usage_page = Some(data),
+ Data::Usage { data } => usage = Some(data),
+ _ => {}
+ }
+
+ // Check the values we found.
+ if let (Some(usage_page), Some(usage)) = (usage_page, usage) {
+ return usage_page == u32::from(FIDO_USAGE_PAGE)
+ && usage == u32::from(FIDO_USAGE_U2FHID);
+ }
+ }
+
+ false
+}
+
+#[cfg(target_os = "linux")]
+pub fn read_hid_rpt_sizes(desc: ReportDescriptor) -> io::Result<(usize, usize)> {
+ let mut in_rpt_count = None;
+ let mut out_rpt_count = None;
+ let mut last_rpt_count = None;
+
+ for data in desc.iter() {
+ match data {
+ Data::ReportCount { data } => {
+ if last_rpt_count.is_some() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Duplicate HID_ReportCount",
+ ));
+ }
+ last_rpt_count = Some(data as usize);
+ }
+ Data::Input => {
+ if last_rpt_count.is_none() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "HID_Input should be preceded by HID_ReportCount",
+ ));
+ }
+ if in_rpt_count.is_some() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Duplicate HID_ReportCount",
+ ));
+ }
+ in_rpt_count = last_rpt_count;
+ last_rpt_count = None
+ }
+ Data::Output => {
+ if last_rpt_count.is_none() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "HID_Output should be preceded by HID_ReportCount",
+ ));
+ }
+ if out_rpt_count.is_some() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Duplicate HID_ReportCount",
+ ));
+ }
+ out_rpt_count = last_rpt_count;
+ last_rpt_count = None;
+ }
+ _ => {}
+ }
+ }
+
+ match (in_rpt_count, out_rpt_count) {
+ (Some(in_count), Some(out_count)) => {
+ if in_count > INIT_HEADER_SIZE
+ && in_count <= MAX_HID_RPT_SIZE
+ && out_count > INIT_HEADER_SIZE
+ && out_count <= MAX_HID_RPT_SIZE
+ {
+ Ok((in_count, out_count))
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Report size is too small or too large",
+ ))
+ }
+ }
+ _ => Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Failed to extract report sizes from report descriptor",
+ )),
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/linux/device.rs b/third_party/rust/authenticator/src/transport/linux/device.rs
new file mode 100644
index 0000000000..6abd462d48
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/device.rs
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+use crate::consts::{Capability, CID_BROADCAST};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::{hidraw, monitor};
+use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret};
+use crate::u2ftypes::U2FDeviceInfo;
+use crate::util::from_unix_result;
+use std::fs::OpenOptions;
+use std::hash::{Hash, Hasher};
+use std::io;
+use std::io::{Read, Write};
+use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
+
+#[derive(Debug)]
+pub struct Device {
+ path: PathBuf,
+ fd: std::fs::File,
+ in_rpt_size: usize,
+ out_rpt_size: usize,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<SharedSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+ protocol: FidoProtocol,
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let bufp = buf.as_mut_ptr() as *mut libc::c_void;
+ let rv = unsafe { libc::read(self.fd.as_raw_fd(), bufp, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ let bufp = buf.as_ptr() as *const libc::c_void;
+ let rv = unsafe { libc::write(self.fd.as_raw_fd(), bufp, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+
+ // USB HID writes don't buffer, so this will be a nop.
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = PathBuf;
+ type Id = PathBuf;
+
+ fn new(path: PathBuf) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("Opening device {:?}", path);
+ let fd = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&path)
+ .map_err(|e| (HIDError::IO(Some(path.clone()), e), path.clone()))?;
+ let (in_rpt_size, out_rpt_size) = hidraw::read_hid_rpt_sizes_or_defaults(fd.as_raw_fd());
+ let mut res = Self {
+ path,
+ fd,
+ in_rpt_size,
+ out_rpt_size,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ protocol: FidoProtocol::CTAP2,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path))
+ }
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ self.in_rpt_size
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ self.out_rpt_size
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ monitor::get_property_linux(&self.path, prop_name)
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ HIDDevice::pre_init(self)
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ HIDDevice::get_device_info(self)
+ .cap_flags
+ .contains(Capability::CBOR)
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets replaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ hidraw::is_u2f_device(self.fd.as_raw_fd())
+ }
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: SharedSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1;
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/linux/hidraw.rs b/third_party/rust/authenticator/src/transport/linux/hidraw.rs
new file mode 100644
index 0000000000..16d687f358
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/hidraw.rs
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))]
+
+extern crate libc;
+
+use std::io;
+use std::os::unix::io::RawFd;
+
+use super::hidwrapper::{_HIDIOCGRDESC, _HIDIOCGRDESCSIZE};
+use crate::consts::MAX_HID_RPT_SIZE;
+use crate::transport::hidproto::*;
+use crate::util::{from_unix_result, io_err};
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+pub struct LinuxReportDescriptor {
+ size: ::libc::c_int,
+ value: [u8; 4096],
+}
+
+const HID_MAX_DESCRIPTOR_SIZE: usize = 4096;
+
+#[cfg(not(target_env = "musl"))]
+type IocType = libc::c_ulong;
+#[cfg(target_env = "musl")]
+type IocType = libc::c_int;
+
+pub unsafe fn hidiocgrdescsize(
+ fd: libc::c_int,
+ val: *mut ::libc::c_int,
+) -> io::Result<libc::c_int> {
+ from_unix_result(libc::ioctl(fd, _HIDIOCGRDESCSIZE as IocType, val))
+}
+
+pub unsafe fn hidiocgrdesc(
+ fd: libc::c_int,
+ val: *mut LinuxReportDescriptor,
+) -> io::Result<libc::c_int> {
+ from_unix_result(libc::ioctl(fd, _HIDIOCGRDESC as IocType, val))
+}
+
+pub fn is_u2f_device(fd: RawFd) -> bool {
+ match read_report_descriptor(fd) {
+ Ok(desc) => has_fido_usage(desc),
+ Err(_) => false, // Upon failure, just say it's not a U2F device.
+ }
+}
+
+pub fn read_hid_rpt_sizes_or_defaults(fd: RawFd) -> (usize, usize) {
+ let default_rpt_sizes = (MAX_HID_RPT_SIZE, MAX_HID_RPT_SIZE);
+ let desc = read_report_descriptor(fd);
+ if let Ok(desc) = desc {
+ if let Ok(rpt_sizes) = read_hid_rpt_sizes(desc) {
+ rpt_sizes
+ } else {
+ default_rpt_sizes
+ }
+ } else {
+ default_rpt_sizes
+ }
+}
+
+fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> {
+ let mut desc = LinuxReportDescriptor {
+ size: 0,
+ value: [0; HID_MAX_DESCRIPTOR_SIZE],
+ };
+
+ let _ = unsafe { hidiocgrdescsize(fd, &mut desc.size)? };
+ if desc.size == 0 || desc.size as usize > desc.value.len() {
+ return Err(io_err("unexpected hidiocgrdescsize() result"));
+ }
+
+ let _ = unsafe { hidiocgrdesc(fd, &mut desc)? };
+ let mut value = Vec::from(&desc.value[..]);
+ value.truncate(desc.size as usize);
+ Ok(ReportDescriptor { value })
+}
diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.h b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h
new file mode 100644
index 0000000000..ce77e0f1ca
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h
@@ -0,0 +1,12 @@
+#include<sys/ioctl.h>
+#include<linux/hidraw.h>
+
+/* we define these constants to work around the fact that bindgen
+ can't deal with the _IOR macro function. We let cpp deal with it
+ for us. */
+
+const __u32 _HIDIOCGRDESCSIZE = HIDIOCGRDESCSIZE;
+#undef HIDIOCGRDESCSIZE
+
+const __u32 _HIDIOCGRDESC = HIDIOCGRDESC;
+#undef HIDIOCGRDESC
diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs
new file mode 100644
index 0000000000..bc8582c5b1
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs
@@ -0,0 +1,54 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// sadly we need this file so we can avoid the suprious warnings that
+// would come with bindgen, as well as to avoid cluttering the mod.rs
+// with spurious architecture specific modules.
+
+#[cfg(target_arch = "x86")]
+include!("ioctl_x86.rs");
+
+#[cfg(target_arch = "x86_64")]
+include!("ioctl_x86_64.rs");
+
+#[cfg(all(target_arch = "mips", target_endian = "little"))]
+include!("ioctl_mipsle.rs");
+
+#[cfg(all(target_arch = "mips", target_endian = "big"))]
+include!("ioctl_mipsbe.rs");
+
+#[cfg(all(target_arch = "mips64", target_endian = "little"))]
+include!("ioctl_mips64le.rs");
+
+#[cfg(all(target_arch = "powerpc", target_endian = "little"))]
+include!("ioctl_powerpcle.rs");
+
+#[cfg(all(target_arch = "powerpc", target_endian = "big"))]
+include!("ioctl_powerpcbe.rs");
+
+#[cfg(all(target_arch = "powerpc64", target_endian = "little"))]
+include!("ioctl_powerpc64le.rs");
+
+#[cfg(all(target_arch = "powerpc64", target_endian = "big"))]
+include!("ioctl_powerpc64be.rs");
+
+#[cfg(all(target_arch = "arm", target_endian = "little"))]
+include!("ioctl_armle.rs");
+
+#[cfg(all(target_arch = "arm", target_endian = "big"))]
+include!("ioctl_armbe.rs");
+
+#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
+include!("ioctl_aarch64le.rs");
+
+#[cfg(all(target_arch = "aarch64", target_endian = "big"))]
+include!("ioctl_aarch64be.rs");
+
+#[cfg(all(target_arch = "s390x", target_endian = "big"))]
+include!("ioctl_s390xbe.rs");
+
+#[cfg(all(target_arch = "riscv64", target_endian = "little"))]
+include!("ioctl_riscv64.rs");
+
+#[cfg(all(target_arch = "loongarch64", target_endian = "little"))]
+include!("ioctl_loongarch64.rs");
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_loongarch64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_loongarch64.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_loongarch64.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/mod.rs b/third_party/rust/authenticator/src/transport/linux/mod.rs
new file mode 100644
index 0000000000..c4d490ecee
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/mod.rs
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![allow(clippy::unreadable_literal)]
+
+pub mod device;
+pub mod transaction;
+
+mod hidraw;
+mod hidwrapper;
+mod monitor;
diff --git a/third_party/rust/authenticator/src/transport/linux/monitor.rs b/third_party/rust/authenticator/src/transport/linux/monitor.rs
new file mode 100644
index 0000000000..ee88622de9
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/monitor.rs
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use libc::{c_int, c_short, c_ulong};
+use libudev::EventType;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::io;
+use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
+use std::sync::{mpsc::Sender, Arc};
+
+const UDEV_SUBSYSTEM: &str = "hidraw";
+const POLLIN: c_short = 0x0001;
+const POLL_TIMEOUT: c_int = 100;
+
+fn poll(fds: &mut Vec<::libc::pollfd>) -> io::Result<()> {
+ let nfds = fds.len() as c_ulong;
+
+ let rv = unsafe { ::libc::poll((fds[..]).as_mut_ptr(), nfds, POLL_TIMEOUT) };
+
+ if rv < 0 {
+ Err(io::Error::from_raw_os_error(rv))
+ } else {
+ Ok(())
+ }
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Sync,
+{
+ runloops: HashMap<PathBuf, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ let ctx = libudev::Context::new()?;
+
+ let mut enumerator = libudev::Enumerator::new(&ctx)?;
+ enumerator.match_subsystem(UDEV_SUBSYSTEM)?;
+
+ // Iterate all existing devices.
+ let paths: Vec<PathBuf> = enumerator
+ .scan_devices()?
+ .filter_map(|dev| dev.devnode().map(|p| p.to_owned()))
+ .collect();
+
+ // Add them all in one go to avoid race conditions in DeviceSelector
+ // (8 devices should be added, but the first returns already before all
+ // others are known to DeviceSelector)
+ self.selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?;
+ for path in paths {
+ self.add_device(path);
+ }
+
+ let mut monitor = libudev::Monitor::new(&ctx)?;
+ monitor.match_subsystem(UDEV_SUBSYSTEM)?;
+
+ // Start listening for new devices.
+ let mut socket = monitor.listen()?;
+ let mut fds = vec![::libc::pollfd {
+ fd: socket.as_raw_fd(),
+ events: POLLIN,
+ revents: 0,
+ }];
+
+ while alive() {
+ // Wait for new events, break on failure.
+ poll(&mut fds)?;
+
+ if let Some(event) = socket.receive_event() {
+ self.process_event(&event);
+ }
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn process_event(&mut self, event: &libudev::Event) {
+ let path = event.device().devnode().map(|dn| dn.to_owned());
+
+ match (event.event_type(), path) {
+ (EventType::Add, Some(path)) => {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()]));
+ self.add_device(path);
+ }
+ (EventType::Remove, Some(path)) => {
+ self.remove_device(&path);
+ }
+ _ => { /* ignore other types and failures */ }
+ }
+ }
+
+ fn add_device(&mut self, path: PathBuf) {
+ let f = self.new_device_cb.clone();
+ let key = path.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ debug!("Adding device {}", path.to_string_lossy());
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(path, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: &PathBuf) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path.to_string_lossy());
+ if let Some(runloop) = self.runloops.remove(path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(&path);
+ }
+ }
+}
+
+pub fn get_property_linux(path: &PathBuf, prop_name: &str) -> io::Result<String> {
+ let ctx = libudev::Context::new()?;
+
+ let mut enumerator = libudev::Enumerator::new(&ctx)?;
+ enumerator.match_subsystem(UDEV_SUBSYSTEM)?;
+
+ // Iterate all existing devices, since we don't have a syspath
+ // and libudev-rs doesn't implement opening by devnode.
+ for dev in enumerator.scan_devices()? {
+ if dev.devnode().is_some() && dev.devnode().unwrap() == path {
+ debug!(
+ "get_property_linux Querying property {} from {}",
+ prop_name,
+ dev.syspath().display()
+ );
+
+ let value = dev
+ .attribute_value(prop_name)
+ .ok_or(io::ErrorKind::Other)?
+ .to_string_lossy();
+
+ debug!("get_property_linux Fetched Result, {}={}", prop_name, value);
+ return Ok(value.to_string());
+ }
+ }
+
+ Err(io::Error::new(
+ io::ErrorKind::Other,
+ "Unable to find device",
+ ))
+}
diff --git a/third_party/rust/authenticator/src/transport/linux/transaction.rs b/third_party/rust/authenticator/src/transport/linux/transaction.rs
new file mode 100644
index 0000000000..6b15f6751a
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/transaction.rs
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let device_selector = DeviceSelector::run();
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/macos/device.rs b/third_party/rust/authenticator/src/transport/macos/device.rs
new file mode 100644
index 0000000000..9acce3aa29
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/device.rs
@@ -0,0 +1,226 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate log;
+
+use crate::consts::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::iokit::*;
+use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret};
+use crate::u2ftypes::U2FDeviceInfo;
+use core_foundation::base::*;
+use core_foundation::string::*;
+use std::convert::TryInto;
+use std::fmt;
+use std::hash::{Hash, Hasher};
+use std::io;
+use std::io::{Read, Write};
+use std::sync::mpsc::{Receiver, RecvTimeoutError};
+use std::time::Duration;
+
+const READ_TIMEOUT: u64 = 15;
+
+pub struct Device {
+ device_ref: IOHIDDeviceRef,
+ cid: [u8; 4],
+ report_rx: Option<Receiver<Vec<u8>>>,
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<SharedSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+ protocol: FidoProtocol,
+}
+
+impl Device {
+ unsafe fn get_property_macos(&self, prop_name: &str) -> io::Result<String> {
+ let prop_ref = IOHIDDeviceGetProperty(
+ self.device_ref,
+ CFString::new(prop_name).as_concrete_TypeRef(),
+ );
+ if prop_ref.is_null() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!("IOHIDDeviceGetProperty received nullptr for property {prop_name}"),
+ ));
+ }
+
+ if CFGetTypeID(prop_ref) != CFStringGetTypeID() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ format!("IOHIDDeviceGetProperty returned non-string type for property {prop_name}"),
+ ));
+ }
+
+ Ok(CFString::from_void(prop_ref).to_string())
+ }
+}
+
+impl fmt::Debug for Device {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Device").field("cid", &self.cid).finish()
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other_device: &Device) -> bool {
+ self.device_ref == other_device.device_ref
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.device_ref.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, mut bytes: &mut [u8]) -> io::Result<usize> {
+ if let Some(rx) = &self.report_rx {
+ let timeout = Duration::from_secs(READ_TIMEOUT);
+ let data = match rx.recv_timeout(timeout) {
+ Ok(v) => v,
+ Err(e) if e == RecvTimeoutError::Timeout => {
+ return Err(io::Error::new(io::ErrorKind::TimedOut, e));
+ }
+ Err(e) => {
+ return Err(io::Error::new(io::ErrorKind::UnexpectedEof, e));
+ }
+ };
+ bytes.write(&data)
+ } else {
+ Err(io::Error::from(io::ErrorKind::Unsupported))
+ }
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
+ assert_eq!(bytes.len(), self.out_rpt_size() + 1);
+
+ let report_id = i64::from(bytes[0]);
+ // Skip report number when not using numbered reports.
+ let start = if report_id == 0x0 { 1 } else { 0 };
+ let data = &bytes[start..];
+
+ let result = unsafe {
+ IOHIDDeviceSetReport(
+ self.device_ref,
+ kIOHIDReportTypeOutput,
+ report_id.try_into().unwrap(),
+ data.as_ptr(),
+ data.len() as CFIndex,
+ )
+ };
+ if result != 0 {
+ warn!("set_report sending failure = {0:X}", result);
+ return Err(io::Error::from_raw_os_error(result));
+ }
+ trace!("set_report sending success = {0:X}", result);
+
+ Ok(bytes.len())
+ }
+
+ // USB HID writes don't buffer, so this will be a nop.
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = (IOHIDDeviceRef, Receiver<Vec<u8>>);
+ type Id = IOHIDDeviceRef;
+
+ fn new(dev_ids: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
+ let (device_ref, report_rx) = dev_ids;
+ Ok(Self {
+ device_ref,
+ cid: CID_BROADCAST,
+ report_rx: Some(report_rx),
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ protocol: FidoProtocol::CTAP2,
+ })
+ }
+
+ fn id(&self) -> Self::Id {
+ self.device_ref
+ }
+
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ unsafe { self.get_property_macos(prop_name) }
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ HIDDevice::pre_init(self)
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ HIDDevice::get_device_info(self)
+ .cap_flags
+ .contains(Capability::CBOR)
+ }
+
+ fn initialized(&self) -> bool {
+ self.cid != CID_BROADCAST
+ }
+ fn is_u2f(&mut self) -> bool {
+ true
+ }
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: SharedSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1;
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/macos/iokit.rs b/third_party/rust/authenticator/src/transport/macos/iokit.rs
new file mode 100644
index 0000000000..656cdb045d
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/iokit.rs
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]
+
+extern crate libc;
+
+use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID};
+use core_foundation::array::*;
+use core_foundation::base::*;
+use core_foundation::dictionary::*;
+use core_foundation::number::*;
+use core_foundation::runloop::*;
+use core_foundation::string::*;
+use std::ops::Deref;
+use std::os::raw::c_void;
+
+type IOOptionBits = u32;
+
+pub type IOReturn = libc::c_int;
+
+pub type IOHIDManagerRef = *mut __IOHIDManager;
+pub type IOHIDManagerOptions = IOOptionBits;
+
+pub type IOHIDDeviceCallback = extern "C" fn(
+ context: *mut c_void,
+ result: IOReturn,
+ sender: *mut c_void,
+ device: IOHIDDeviceRef,
+);
+
+pub type IOHIDReportType = IOOptionBits;
+pub type IOHIDReportCallback = extern "C" fn(
+ context: *mut c_void,
+ result: IOReturn,
+ sender: IOHIDDeviceRef,
+ report_type: IOHIDReportType,
+ report_id: u32,
+ report: *mut u8,
+ report_len: CFIndex,
+);
+
+pub const kIOHIDManagerOptionNone: IOHIDManagerOptions = 0;
+
+pub const kIOHIDReportTypeOutput: IOHIDReportType = 1;
+
+#[repr(C)]
+pub struct __IOHIDManager {
+ __private: c_void,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
+pub struct IOHIDDeviceRef(*const c_void);
+
+unsafe impl Send for IOHIDDeviceRef {}
+unsafe impl Sync for IOHIDDeviceRef {}
+
+pub struct SendableRunLoop(CFRunLoopRef);
+
+impl SendableRunLoop {
+ pub fn new(runloop: CFRunLoopRef) -> Self {
+ // Keep the CFRunLoop alive for as long as we are.
+ unsafe { CFRetain(runloop as *mut c_void) };
+
+ SendableRunLoop(runloop)
+ }
+}
+
+unsafe impl Send for SendableRunLoop {}
+
+impl Deref for SendableRunLoop {
+ type Target = CFRunLoopRef;
+
+ fn deref(&self) -> &CFRunLoopRef {
+ &self.0
+ }
+}
+
+impl Drop for SendableRunLoop {
+ fn drop(&mut self) {
+ unsafe { CFRelease(self.0 as *mut c_void) };
+ }
+}
+
+#[repr(C)]
+pub struct CFRunLoopObserverContext {
+ pub version: CFIndex,
+ pub info: *mut c_void,
+ pub retain: Option<extern "C" fn(info: *const c_void) -> *const c_void>,
+ pub release: Option<extern "C" fn(info: *const c_void)>,
+ pub copyDescription: Option<extern "C" fn(info: *const c_void) -> CFStringRef>,
+}
+
+impl CFRunLoopObserverContext {
+ pub fn new(context: *mut c_void) -> Self {
+ Self {
+ version: 0 as CFIndex,
+ info: context,
+ retain: None,
+ release: None,
+ copyDescription: None,
+ }
+ }
+}
+
+pub struct CFRunLoopEntryObserver {
+ observer: CFRunLoopObserverRef,
+ // Keep alive until the observer goes away.
+ context_ptr: *mut CFRunLoopObserverContext,
+}
+
+impl CFRunLoopEntryObserver {
+ pub fn new(callback: CFRunLoopObserverCallBack, context: *mut c_void) -> Self {
+ let context = CFRunLoopObserverContext::new(context);
+ let context_ptr = Box::into_raw(Box::new(context));
+
+ let observer = unsafe {
+ CFRunLoopObserverCreate(
+ kCFAllocatorDefault,
+ kCFRunLoopEntry,
+ false as Boolean,
+ 0,
+ callback,
+ context_ptr,
+ )
+ };
+
+ Self {
+ observer,
+ context_ptr,
+ }
+ }
+
+ pub fn add_to_current_runloop(&self) {
+ unsafe {
+ CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.observer, kCFRunLoopDefaultMode)
+ };
+ }
+}
+
+impl Drop for CFRunLoopEntryObserver {
+ fn drop(&mut self) {
+ unsafe {
+ CFRelease(self.observer as *mut c_void);
+
+ // Drop the CFRunLoopObserverContext.
+ let _ = Box::from_raw(self.context_ptr);
+ };
+ }
+}
+
+pub struct IOHIDDeviceMatcher {
+ pub dict: CFDictionary<CFString, CFNumber>,
+}
+
+impl IOHIDDeviceMatcher {
+ pub fn new() -> Self {
+ let dict = CFDictionary::<CFString, CFNumber>::from_CFType_pairs(&[
+ (
+ CFString::from_static_string("DeviceUsage"),
+ CFNumber::from(i32::from(FIDO_USAGE_U2FHID)),
+ ),
+ (
+ CFString::from_static_string("DeviceUsagePage"),
+ CFNumber::from(i32::from(FIDO_USAGE_PAGE)),
+ ),
+ ]);
+ Self { dict }
+ }
+}
+
+#[link(name = "IOKit", kind = "framework")]
+extern "C" {
+ // CFRunLoop
+ pub fn CFRunLoopObserverCreate(
+ allocator: CFAllocatorRef,
+ activities: CFOptionFlags,
+ repeats: Boolean,
+ order: CFIndex,
+ callout: CFRunLoopObserverCallBack,
+ context: *mut CFRunLoopObserverContext,
+ ) -> CFRunLoopObserverRef;
+
+ // IOHIDManager
+ pub fn IOHIDManagerCreate(
+ allocator: CFAllocatorRef,
+ options: IOHIDManagerOptions,
+ ) -> IOHIDManagerRef;
+ pub fn IOHIDManagerSetDeviceMatching(manager: IOHIDManagerRef, matching: CFDictionaryRef);
+ pub fn IOHIDManagerRegisterDeviceMatchingCallback(
+ manager: IOHIDManagerRef,
+ callback: IOHIDDeviceCallback,
+ context: *mut c_void,
+ );
+ pub fn IOHIDManagerRegisterDeviceRemovalCallback(
+ manager: IOHIDManagerRef,
+ callback: IOHIDDeviceCallback,
+ context: *mut c_void,
+ );
+ pub fn IOHIDManagerRegisterInputReportCallback(
+ manager: IOHIDManagerRef,
+ callback: IOHIDReportCallback,
+ context: *mut c_void,
+ );
+ pub fn IOHIDManagerOpen(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn;
+ pub fn IOHIDManagerClose(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn;
+ pub fn IOHIDManagerScheduleWithRunLoop(
+ manager: IOHIDManagerRef,
+ runLoop: CFRunLoopRef,
+ runLoopMode: CFStringRef,
+ );
+
+ // IOHIDDevice
+ pub fn IOHIDDeviceSetReport(
+ device: IOHIDDeviceRef,
+ reportType: IOHIDReportType,
+ reportID: CFIndex,
+ report: *const u8,
+ reportLength: CFIndex,
+ ) -> IOReturn;
+ pub fn IOHIDDeviceGetProperty(device: IOHIDDeviceRef, key: CFStringRef) -> CFTypeRef;
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::os::raw::c_void;
+ use std::ptr;
+ use std::sync::mpsc::{channel, Sender};
+ use std::thread;
+
+ extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) {
+ let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) };
+
+ // Send the current runloop to the receiver to unblock it.
+ let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() }));
+ }
+
+ #[test]
+ fn test_sendable_runloop() {
+ let (tx, rx) = channel();
+
+ let thread = thread::spawn(move || {
+ // Send the runloop to the owning thread.
+ let context = &tx as *const _ as *mut c_void;
+ let obs = CFRunLoopEntryObserver::new(observe, context);
+ obs.add_to_current_runloop();
+
+ unsafe {
+ // We need some source for the runloop to run.
+ let manager = IOHIDManagerCreate(kCFAllocatorDefault, 0);
+ assert!(!manager.is_null());
+
+ IOHIDManagerScheduleWithRunLoop(
+ manager,
+ CFRunLoopGetCurrent(),
+ kCFRunLoopDefaultMode,
+ );
+ IOHIDManagerSetDeviceMatching(manager, ptr::null_mut());
+
+ let rv = IOHIDManagerOpen(manager, 0);
+ assert_eq!(rv, 0);
+
+ // This will run until `CFRunLoopStop()` is called.
+ CFRunLoopRun();
+
+ let rv = IOHIDManagerClose(manager, 0);
+ assert_eq!(rv, 0);
+
+ CFRelease(manager as *mut c_void);
+ }
+ });
+
+ // Block until we enter the CFRunLoop.
+ let runloop: SendableRunLoop = rx.recv().expect("failed to receive runloop");
+
+ // Stop the runloop.
+ unsafe { CFRunLoopStop(*runloop) };
+
+ // Stop the thread.
+ thread.join().expect("failed to join the thread");
+
+ // Try to stop the runloop again (without crashing).
+ unsafe { CFRunLoopStop(*runloop) };
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/macos/mod.rs b/third_party/rust/authenticator/src/transport/macos/mod.rs
new file mode 100644
index 0000000000..44e85094d0
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod iokit;
+mod monitor;
diff --git a/third_party/rust/authenticator/src/transport/macos/monitor.rs b/third_party/rust/authenticator/src/transport/macos/monitor.rs
new file mode 100644
index 0000000000..32200ee7a4
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/monitor.rs
@@ -0,0 +1,212 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+extern crate log;
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use crate::transport::platform::iokit::*;
+use crate::util::io_err;
+use core_foundation::base::*;
+use core_foundation::runloop::*;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::os::raw::c_void;
+use std::sync::mpsc::{channel, Receiver, Sender};
+use std::{io, slice};
+
+struct DeviceData {
+ tx: Sender<Vec<u8>>,
+ runloop: RunLoop,
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(
+ (IOHIDDeviceRef, Receiver<Vec<u8>>),
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ manager: IOHIDManagerRef,
+ // Keep alive until the monitor goes away.
+ _matcher: IOHIDDeviceMatcher,
+ map: HashMap<IOHIDDeviceRef, DeviceData>,
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(
+ (IOHIDDeviceRef, Receiver<Vec<u8>>),
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) };
+
+ // Match FIDO devices only.
+ let _matcher = IOHIDDeviceMatcher::new();
+ unsafe { IOHIDManagerSetDeviceMatching(manager, _matcher.dict.as_concrete_TypeRef()) };
+
+ Self {
+ manager,
+ _matcher,
+ new_device_cb,
+ map: HashMap::new(),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn start(&mut self) -> io::Result<()> {
+ let context = self as *mut Self as *mut c_void;
+
+ unsafe {
+ IOHIDManagerRegisterDeviceMatchingCallback(
+ self.manager,
+ Monitor::<F>::on_device_matching,
+ context,
+ );
+ IOHIDManagerRegisterDeviceRemovalCallback(
+ self.manager,
+ Monitor::<F>::on_device_removal,
+ context,
+ );
+ IOHIDManagerRegisterInputReportCallback(
+ self.manager,
+ Monitor::<F>::on_input_report,
+ context,
+ );
+
+ IOHIDManagerScheduleWithRunLoop(
+ self.manager,
+ CFRunLoopGetCurrent(),
+ kCFRunLoopDefaultMode,
+ );
+
+ let rv = IOHIDManagerOpen(self.manager, kIOHIDManagerOptionNone);
+ if rv == 0 {
+ Ok(())
+ } else {
+ Err(io_err(&format!("Couldn't open HID Manager, rv={rv}")))
+ }
+ }
+ }
+
+ pub fn stop(&mut self) {
+ // Remove all devices.
+ while !self.map.is_empty() {
+ let device_ref = *self.map.keys().next().unwrap();
+ self.remove_device(device_ref);
+ }
+
+ // Close the manager and its devices.
+ unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) };
+ }
+
+ fn remove_device(&mut self, device_ref: IOHIDDeviceRef) {
+ if let Some(DeviceData { tx, runloop }) = self.map.remove(&device_ref) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(device_ref));
+ // Dropping `tx` will make Device::read() fail eventually.
+ drop(tx);
+
+ // Wait until the runloop stopped.
+ runloop.cancel();
+ }
+ }
+
+ extern "C" fn on_input_report(
+ context: *mut c_void,
+ _: IOReturn,
+ device_ref: IOHIDDeviceRef,
+ _: IOHIDReportType,
+ _: u32,
+ report: *mut u8,
+ report_len: CFIndex,
+ ) {
+ let this = unsafe { &mut *(context as *mut Self) };
+ let mut send_failed = false;
+
+ // Ignore the report if we can't find a device for it.
+ if let Some(DeviceData { tx, .. }) = this.map.get(&device_ref) {
+ let data = unsafe { slice::from_raw_parts(report, report_len as usize).to_vec() };
+ send_failed = tx.send(data).is_err();
+ }
+
+ // Remove the device if sending fails.
+ if send_failed {
+ this.remove_device(device_ref);
+ }
+ }
+
+ extern "C" fn on_device_matching(
+ context: *mut c_void,
+ _: IOReturn,
+ _: *mut c_void,
+ device_ref: IOHIDDeviceRef,
+ ) {
+ let this = unsafe { &mut *(context as *mut Self) };
+ let _ = this
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![device_ref]));
+ let selector_sender = this.selector_sender.clone();
+ let status_sender = this.status_sender.clone();
+ let (tx, rx) = channel();
+ let f = &this.new_device_cb;
+
+ // Create a new per-device runloop.
+ let runloop = RunLoop::new(move |alive| {
+ // Ensure that the runloop is still alive.
+ if alive() {
+ f((device_ref, rx), selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ this.map.insert(device_ref, DeviceData { tx, runloop });
+ }
+ }
+
+ extern "C" fn on_device_removal(
+ context: *mut c_void,
+ _: IOReturn,
+ _: *mut c_void,
+ device_ref: IOHIDDeviceRef,
+ ) {
+ let this = unsafe { &mut *(context as *mut Self) };
+ this.remove_device(device_ref);
+ }
+}
+
+impl<F> Drop for Monitor<F>
+where
+ F: Fn(
+ (IOHIDDeviceRef, Receiver<Vec<u8>>),
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ fn drop(&mut self) {
+ unsafe { CFRelease(self.manager as *mut c_void) };
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/macos/transaction.rs b/third_party/rust/authenticator/src/transport/macos/transaction.rs
new file mode 100644
index 0000000000..d9709e7364
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/transaction.rs
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::iokit::{CFRunLoopEntryObserver, SendableRunLoop};
+use crate::transport::platform::monitor::Monitor;
+use core_foundation::runloop::*;
+use std::os::raw::c_void;
+use std::sync::mpsc::{channel, Sender};
+use std::thread;
+
+// A transaction will run the given closure in a new thread, thereby using a
+// separate per-thread state machine for each HID. It will either complete or
+// fail through user action, timeout, or be cancelled when overridden by a new
+// transaction.
+pub struct Transaction {
+ runloop: Option<SendableRunLoop>,
+ thread: Option<thread::JoinHandle<()>>,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let (tx, rx) = channel();
+ let timeout = (timeout as f64) / 1000.0;
+ let device_selector = DeviceSelector::run();
+ let selector_sender = device_selector.clone_sender();
+ let builder = thread::Builder::new();
+ let thread = builder
+ .spawn(move || {
+ // Add a runloop observer that will be notified when we enter the
+ // runloop and tx.send() the current runloop to the owning thread.
+ // We need to ensure the runloop was entered before unblocking
+ // Transaction::new(), so we can always properly cancel.
+ let context = &tx as *const _ as *mut c_void;
+ let obs = CFRunLoopEntryObserver::new(Transaction::observe, context);
+ obs.add_to_current_runloop();
+
+ // Create a new HID device monitor and start polling.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
+ try_or!(monitor.start(), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // This will block until completion, abortion, or timeout.
+ unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, 0) };
+
+ // Close the monitor and its devices.
+ monitor.stop();
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ })
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ // Block until we enter the CFRunLoop.
+ let runloop = rx
+ .recv()
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ runloop: Some(runloop),
+ thread: Some(thread),
+ device_selector,
+ })
+ }
+
+ extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) {
+ let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) };
+
+ // Send the current runloop to the receiver to unblock it.
+ let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() }));
+ }
+
+ pub fn cancel(&mut self) {
+ // This must never be None. This won't block.
+ unsafe { CFRunLoopStop(*self.runloop.take().unwrap()) };
+
+ self.device_selector.stop();
+ // This must never be None. Ignore return value.
+ let _ = self.thread.take().unwrap().join();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/mock/device.rs b/third_party/rust/authenticator/src/transport/mock/device.rs
new file mode 100644
index 0000000000..ac4e156d8a
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mock/device.rs
@@ -0,0 +1,328 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use crate::consts::{Capability, HIDCmd, CID_BROADCAST};
+use crate::crypto::SharedSecret;
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::ctap2::commands::{CtapResponse, RequestCtap1, RequestCtap2};
+use crate::transport::device_selector::DeviceCommand;
+use crate::transport::TestDevice;
+use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol, HIDError};
+use crate::u2ftypes::{U2FDeviceInfo, U2FHIDInitResp};
+use std::any::Any;
+use std::collections::VecDeque;
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::sync::mpsc::{channel, Receiver, Sender};
+
+pub(crate) const IN_HID_RPT_SIZE: usize = 64;
+const OUT_HID_RPT_SIZE: usize = 64;
+
+#[derive(Debug)]
+pub struct Device {
+ pub id: String,
+ pub cid: [u8; 4],
+ pub reads: Vec<[u8; IN_HID_RPT_SIZE]>,
+ pub writes: Vec<[u8; OUT_HID_RPT_SIZE + 1]>,
+ pub dev_info: Option<U2FDeviceInfo>,
+ pub authenticator_info: Option<AuthenticatorInfo>,
+ pub sender: Option<Sender<DeviceCommand>>,
+ pub receiver: Option<Receiver<DeviceCommand>>,
+ pub protocol: FidoProtocol,
+ skip_serialization: bool,
+ pub upcoming_requests: VecDeque<Vec<u8>>,
+ pub upcoming_responses: VecDeque<Result<Box<dyn Any>, HIDError>>,
+}
+
+impl Device {
+ pub fn add_write(&mut self, packet: &[u8], fill_value: u8) {
+ // Add one to deal with record index check
+ let mut write = [fill_value; OUT_HID_RPT_SIZE + 1];
+ // Make sure we start with a 0, for HID record index
+ write[0] = 0;
+ // Clone packet data in at 1, since front is padded with HID record index
+ write[1..=packet.len()].clone_from_slice(packet);
+ self.writes.push(write);
+ }
+
+ pub fn add_read(&mut self, packet: &[u8], fill_value: u8) {
+ let mut read = [fill_value; IN_HID_RPT_SIZE];
+ read[..packet.len()].clone_from_slice(packet);
+ self.reads.push(read);
+ }
+
+ pub fn add_upcoming_ctap2_request(&mut self, msg: &impl RequestCtap2) {
+ self.upcoming_requests
+ .push_back(msg.wire_format().expect("Failed to serialize CTAP request"));
+ }
+
+ pub fn add_upcoming_ctap1_request(&mut self, msg: &impl RequestCtap1) {
+ let (upcoming, _) = msg
+ .ctap1_format()
+ .expect("Failed to serialize CTAP request");
+ self.upcoming_requests.push_back(upcoming);
+ }
+
+ pub fn add_upcoming_ctap_response(&mut self, msg: impl CtapResponse) {
+ self.upcoming_responses.push_back(Ok(Box::new(msg)));
+ }
+
+ pub fn add_upcoming_ctap_error(&mut self, msg: HIDError) {
+ self.upcoming_responses.push_back(Err(msg));
+ }
+
+ pub fn create_channel(&mut self) {
+ let (tx, rx) = channel();
+ self.sender = Some(tx);
+ self.receiver = Some(rx);
+ }
+
+ pub fn new_skipping_serialization(id: &str) -> Result<Self, (HIDError, String)> {
+ Ok(Device {
+ id: id.to_string(),
+ cid: CID_BROADCAST,
+ reads: vec![],
+ writes: vec![],
+ dev_info: None,
+ authenticator_info: None,
+ sender: None,
+ receiver: None,
+ protocol: FidoProtocol::CTAP2,
+ skip_serialization: true,
+ upcoming_requests: VecDeque::new(),
+ upcoming_responses: VecDeque::new(),
+ })
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
+ // Pop a vector from the expected writes, check for quality
+ // against bytes array.
+ assert!(
+ !self.writes.is_empty(),
+ "Ran out of expected write values! Wanted to write {:?}",
+ bytes
+ );
+ let check = self.writes.remove(0);
+ assert_eq!(check.len(), bytes.len());
+ assert_eq!(&check, bytes);
+ Ok(bytes.len())
+ }
+
+ // nop
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
+ assert!(!self.reads.is_empty(), "Ran out of read values!");
+ let check = self.reads.remove(0);
+ assert_eq!(check.len(), bytes.len());
+ bytes.clone_from_slice(&check);
+ Ok(check.len())
+ }
+}
+
+impl Drop for Device {
+ fn drop(&mut self) {
+ if !std::thread::panicking() {
+ assert!(self.reads.is_empty());
+ assert!(self.writes.is_empty());
+ }
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.id == other.id
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.id.hash(state);
+ }
+}
+
+impl HIDDevice for Device {
+ type Id = String;
+ type BuildParameters = &'static str; // None used
+
+ fn id(&self) -> Self::Id {
+ self.id.clone()
+ }
+
+ fn new(id: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
+ Ok(Device {
+ id: id.to_string(),
+ cid: CID_BROADCAST,
+ reads: vec![],
+ writes: vec![],
+ dev_info: None,
+ authenticator_info: None,
+ sender: None,
+ receiver: None,
+ protocol: FidoProtocol::CTAP2,
+ skip_serialization: false,
+ upcoming_requests: VecDeque::new(),
+ upcoming_responses: VecDeque::new(),
+ })
+ }
+
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ IN_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ OUT_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ Ok(format!("{prop_name} not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ if self.initialized() {
+ return Ok(());
+ }
+
+ let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
+
+ // Send Init to broadcast address to create a new channel
+ self.set_cid(CID_BROADCAST);
+ let (cmd, raw) = HIDDevice::sendrecv(self, HIDCmd::Init, &nonce, &|| true)?;
+ if cmd != HIDCmd::Init {
+ return Err(HIDError::DeviceError);
+ }
+
+ let rsp = U2FHIDInitResp::read(&raw, &nonce)?;
+
+ // Set the new Channel ID
+ self.set_cid(rsp.cid);
+
+ let info = U2FDeviceInfo {
+ vendor_name: "Test vendor".as_bytes().to_vec(),
+ device_name: "Test device".as_bytes().to_vec(),
+ version_interface: rsp.version_interface,
+ version_major: rsp.version_major,
+ version_minor: rsp.version_minor,
+ version_build: rsp.version_build,
+ cap_flags: rsp.cap_flags,
+ };
+ debug!("{:?}: {:?}", self.id(), info);
+ self.set_device_info(info);
+
+ Ok(())
+ }
+}
+
+impl TestDevice for Device {
+ fn skip_serialization(&self) -> bool {
+ self.skip_serialization
+ }
+
+ fn send_ctap1_unserialized<Req: RequestCtap1>(
+ &mut self,
+ msg: &Req,
+ ) -> Result<Req::Output, HIDError> {
+ let expected = self
+ .upcoming_requests
+ .pop_front()
+ .expect("No expected CTAP1 command left");
+ let (incoming, _) = msg.ctap1_format().expect("Can't serialize CTAP1 request");
+ assert_eq!(expected, incoming);
+ let response = self
+ .upcoming_responses
+ .pop_front()
+ .expect("No response given!");
+ response.map(|x| {
+ *x.downcast()
+ .expect("Failed to downcast given CTAP response")
+ })
+ }
+
+ fn send_ctap2_unserialized<Req: RequestCtap2>(
+ &mut self,
+ msg: &Req,
+ ) -> Result<Req::Output, HIDError> {
+ let expected = self
+ .upcoming_requests
+ .pop_front()
+ .expect("No expected CTAP2 command left");
+ let incoming = msg.wire_format().expect("Can't serialize CTAP2 request");
+ assert_eq!(expected, incoming);
+ let response = self
+ .upcoming_responses
+ .pop_front()
+ .expect("No response given!");
+ response.map(|x| {
+ *x.downcast()
+ .expect("Failed to downcast given CTAP response")
+ })
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ HIDDevice::pre_init(self)
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ HIDDevice::get_device_info(self)
+ .cap_flags
+ .contains(Capability::CBOR)
+ }
+
+ fn initialized(&self) -> bool {
+ self.get_cid() != &CID_BROADCAST
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ self.sender.is_some()
+ }
+
+ fn get_shared_secret(&self) -> std::option::Option<&SharedSecret> {
+ None
+ }
+
+ fn set_shared_secret(&mut self, _: SharedSecret) {
+ // Nothing
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1;
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/mock/mod.rs b/third_party/rust/authenticator/src/transport/mock/mod.rs
new file mode 100644
index 0000000000..d0e200a7ef
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mock/mod.rs
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
diff --git a/third_party/rust/authenticator/src/transport/mock/transaction.rs b/third_party/rust/authenticator/src/transport/mock/transaction.rs
new file mode 100644
index 0000000000..e19b1cb56f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mock/transaction.rs
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{DeviceBuildParameters, DeviceSelectorEvent};
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {}
+
+impl Transaction {
+ pub fn new<F, T>(
+ _timeout: u64,
+ _callback: StateCallback<crate::Result<T>>,
+ _status: Sender<crate::StatusUpdate>,
+ _new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ Ok(Self {})
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/mod.rs b/third_party/rust/authenticator/src/transport/mod.rs
new file mode 100644
index 0000000000..318934ed36
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mod.rs
@@ -0,0 +1,369 @@
+use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret};
+use crate::ctap2::commands::client_pin::{
+ ClientPIN, ClientPinResponse, GetKeyAgreement, GetPinToken,
+ GetPinUvAuthTokenUsingPinWithPermissions, GetPinUvAuthTokenUsingUvWithPermissions,
+ PinUvAuthTokenPermission,
+};
+use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult};
+use crate::ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorVersion, GetInfo};
+use crate::ctap2::commands::get_version::{GetVersion, U2FInfo};
+use crate::ctap2::commands::make_credentials::{
+ dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsResult,
+};
+use crate::ctap2::commands::reset::Reset;
+use crate::ctap2::commands::selection::Selection;
+use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, StatusCode};
+use crate::ctap2::preflight::CheckKeyHandle;
+use crate::transport::device_selector::BlinkResult;
+use crate::transport::errors::HIDError;
+
+use crate::Pin;
+use std::convert::TryFrom;
+use std::fmt;
+
+pub mod device_selector;
+pub mod errors;
+pub mod hid;
+
+#[cfg(all(
+ any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"),
+ not(test)
+))]
+pub mod hidproto;
+
+#[cfg(all(target_os = "linux", not(test)))]
+#[path = "linux/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "freebsd", not(test)))]
+#[path = "freebsd/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "netbsd", not(test)))]
+#[path = "netbsd/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "openbsd", not(test)))]
+#[path = "openbsd/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "macos", not(test)))]
+#[path = "macos/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "windows", not(test)))]
+#[path = "windows/mod.rs"]
+pub mod platform;
+
+#[cfg(not(any(
+ target_os = "linux",
+ target_os = "freebsd",
+ target_os = "openbsd",
+ target_os = "netbsd",
+ target_os = "macos",
+ target_os = "windows",
+ test
+)))]
+#[path = "stub/mod.rs"]
+pub mod platform;
+
+#[cfg(test)]
+#[path = "mock/mod.rs"]
+pub mod platform;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FidoProtocol {
+ CTAP1,
+ CTAP2,
+}
+
+pub trait FidoDeviceIO {
+ fn send_msg<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>(
+ &mut self,
+ msg: &Req,
+ ) -> Result<Out, HIDError> {
+ self.send_msg_cancellable(msg, &|| true)
+ }
+
+ fn send_cbor<Req: RequestCtap2>(&mut self, msg: &Req) -> Result<Req::Output, HIDError> {
+ self.send_cbor_cancellable(msg, &|| true)
+ }
+
+ fn send_ctap1<Req: RequestCtap1>(&mut self, msg: &Req) -> Result<Req::Output, HIDError> {
+ self.send_ctap1_cancellable(msg, &|| true)
+ }
+
+ fn send_msg_cancellable<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Out, HIDError>;
+
+ fn send_cbor_cancellable<Req: RequestCtap2>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Req::Output, HIDError>;
+
+ fn send_ctap1_cancellable<Req: RequestCtap1>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Req::Output, HIDError>;
+}
+
+pub trait TestDevice {
+ #[cfg(test)]
+ fn skip_serialization(&self) -> bool;
+ #[cfg(test)]
+ fn send_ctap1_unserialized<Req: RequestCtap1>(
+ &mut self,
+ msg: &Req,
+ ) -> Result<Req::Output, HIDError>;
+ #[cfg(test)]
+ fn send_ctap2_unserialized<Req: RequestCtap2>(
+ &mut self,
+ msg: &Req,
+ ) -> Result<Req::Output, HIDError>;
+}
+
+pub trait FidoDevice: FidoDeviceIO
+where
+ Self: Sized,
+ Self: fmt::Debug,
+{
+ fn pre_init(&mut self) -> Result<(), HIDError>;
+ fn initialized(&self) -> bool;
+
+ // Check if the device is actually a token
+ fn is_u2f(&mut self) -> bool;
+ fn should_try_ctap2(&self) -> bool;
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>;
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo);
+ fn refresh_authenticator_info(&mut self) -> Option<&AuthenticatorInfo> {
+ let command = GetInfo::default();
+ if let Ok(info) = self.send_cbor(&command) {
+ debug!("Refreshed authenticator info: {:?}", info);
+ self.set_authenticator_info(info);
+ }
+ self.get_authenticator_info()
+ }
+
+ // `get_protocol()` indicates whether we're using CTAP1 or CTAP2.
+ // Prior to initializing the device, `get_protocol()` should return CTAP2 unless
+ // there's a reason to believe that the device does not support CTAP2 (e.g. if
+ // it's a HID device and it does not have the CBOR capability).
+ fn get_protocol(&self) -> FidoProtocol;
+
+ // We do not provide a generic `set_protocol(..)` function as this would have complicated
+ // interactions with the AuthenticatorInfo state.
+ fn downgrade_to_ctap1(&mut self);
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret>;
+ fn set_shared_secret(&mut self, secret: SharedSecret);
+
+ fn init(&mut self) -> Result<(), HIDError> {
+ self.pre_init()?;
+
+ if self.should_try_ctap2() {
+ let command = GetInfo::default();
+ if let Ok(info) = self.send_cbor(&command) {
+ debug!("{:?}", info);
+ if info.max_supported_version() == AuthenticatorVersion::U2F_V2 {
+ self.downgrade_to_ctap1();
+ }
+ self.set_authenticator_info(info);
+ return Ok(());
+ }
+ }
+
+ self.downgrade_to_ctap1();
+ // We want to return an error here if this device doesn't support CTAP1,
+ // so we send a U2F_VERSION command.
+ let command = GetVersion::default();
+ self.send_ctap1(&command)?;
+ Ok(())
+ }
+
+ fn block_and_blink(&mut self, keep_alive: &dyn Fn() -> bool) -> BlinkResult {
+ let supports_select_cmd = self.get_protocol() == FidoProtocol::CTAP2
+ && self.get_authenticator_info().map_or(false, |i| {
+ i.versions.contains(&AuthenticatorVersion::FIDO_2_1)
+ });
+ let resp = if supports_select_cmd {
+ let msg = Selection {};
+ self.send_cbor_cancellable(&msg, keep_alive)
+ } else {
+ // We need to fake a blink-request, because FIDO2.0 forgot to specify one
+ // See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential
+ let msg = dummy_make_credentials_cmd();
+ info!("Trying to blink: {:?}", &msg);
+ // We don't care about the Ok-value, just if it is Ok or not
+ self.send_msg_cancellable(&msg, keep_alive).map(|_| ())
+ };
+
+ match resp {
+ // Spec only says PinInvalid or PinNotSet should be returned on the fake touch-request,
+ // but Yubikeys for example return PinAuthInvalid. A successful return is also possible
+ // for CTAP1-tokens so we catch those here as well.
+ Ok(_)
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => {
+ BlinkResult::DeviceSelected
+ }
+ // We cancelled the receive, because another device was selected.
+ Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => {
+ // TODO: Repeat the request, if it is a UserActionTimeout?
+ debug!("Device {:?} got cancelled", &self);
+ BlinkResult::Cancelled
+ }
+ // Something unexpected happened, so we assume this device is not usable and
+ // interpreting this equivalent to being cancelled.
+ e => {
+ info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e);
+ BlinkResult::Cancelled
+ }
+ }
+ }
+
+ fn establish_shared_secret(
+ &mut self,
+ alive: &dyn Fn() -> bool,
+ ) -> Result<SharedSecret, HIDError> {
+ // CTAP1 devices don't support establishing a shared secret
+ let info = match (self.get_protocol(), self.get_authenticator_info()) {
+ (FidoProtocol::CTAP2, Some(info)) => info,
+ _ => return Err(HIDError::UnsupportedCommand),
+ };
+
+ let pin_protocol = PinUvAuthProtocol::try_from(info)?;
+
+ // Not reusing the shared secret here, if it exists, since we might start again
+ // with a different PIN (e.g. if the last one was wrong)
+ let pin_command = GetKeyAgreement::new(pin_protocol.clone());
+ let resp = self.send_cbor_cancellable(&pin_command, alive)?;
+ if let Some(device_key_agreement_key) = resp.key_agreement {
+ let shared_secret = pin_protocol
+ .encapsulate(&device_key_agreement_key)
+ .map_err(CommandError::from)?;
+ self.set_shared_secret(shared_secret.clone());
+ Ok(shared_secret)
+ } else {
+ Err(HIDError::Command(CommandError::MissingRequiredField(
+ "key_agreement",
+ )))
+ }
+ }
+
+ /// CTAP 2.0-only version:
+ /// "Getting pinUvAuthToken using getPinToken (superseded)"
+ fn get_pin_token(
+ &mut self,
+ pin: &Option<Pin>,
+ alive: &dyn Fn() -> bool,
+ ) -> Result<PinUvAuthToken, HIDError> {
+ // Asking the user for PIN before establishing the shared secret
+ let pin = pin
+ .as_ref()
+ .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?;
+
+ // Not reusing the shared secret here, if it exists, since we might start again
+ // with a different PIN (e.g. if the last one was wrong)
+ let shared_secret = self.establish_shared_secret(alive)?;
+
+ let pin_command = GetPinToken::new(&shared_secret, pin);
+ let resp = self.send_cbor_cancellable(&pin_command, alive)?;
+ if let Some(encrypted_pin_token) = resp.pin_token {
+ // CTAP 2.1 spec:
+ // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions
+ // of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken.
+ let default_permissions = PinUvAuthTokenPermission::default();
+ let pin_token = shared_secret
+ .decrypt_pin_token(default_permissions, encrypted_pin_token.as_ref())
+ .map_err(CommandError::from)?;
+ Ok(pin_token)
+ } else {
+ Err(HIDError::Command(CommandError::MissingRequiredField(
+ "pin_token",
+ )))
+ }
+ }
+
+ fn get_pin_uv_auth_token_using_uv_with_permissions(
+ &mut self,
+ permission: PinUvAuthTokenPermission,
+ rp_id: Option<&String>,
+ alive: &dyn Fn() -> bool,
+ ) -> Result<PinUvAuthToken, HIDError> {
+ // Explicitly not reusing the shared secret here
+ let shared_secret = self.establish_shared_secret(alive)?;
+ let pin_command = GetPinUvAuthTokenUsingUvWithPermissions::new(
+ &shared_secret,
+ permission,
+ rp_id.cloned(),
+ );
+
+ let resp = self.send_cbor_cancellable(&pin_command, alive)?;
+
+ if let Some(encrypted_pin_token) = resp.pin_token {
+ let pin_token = shared_secret
+ .decrypt_pin_token(permission, encrypted_pin_token.as_ref())
+ .map_err(CommandError::from)?;
+ Ok(pin_token)
+ } else {
+ Err(HIDError::Command(CommandError::MissingRequiredField(
+ "pin_token",
+ )))
+ }
+ }
+
+ fn get_pin_uv_auth_token_using_pin_with_permissions(
+ &mut self,
+ pin: &Option<Pin>,
+ permission: PinUvAuthTokenPermission,
+ rp_id: Option<&String>,
+ alive: &dyn Fn() -> bool,
+ ) -> Result<PinUvAuthToken, HIDError> {
+ // Asking the user for PIN before establishing the shared secret
+ let pin = pin
+ .as_ref()
+ .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?;
+
+ // Not reusing the shared secret here, if it exists, since we might start again
+ // with a different PIN (e.g. if the last one was wrong)
+ let shared_secret = self.establish_shared_secret(alive)?;
+ let pin_command = GetPinUvAuthTokenUsingPinWithPermissions::new(
+ &shared_secret,
+ pin,
+ permission,
+ rp_id.cloned(),
+ );
+
+ let resp = self.send_cbor_cancellable(&pin_command, alive)?;
+
+ if let Some(encrypted_pin_token) = resp.pin_token {
+ let pin_token = shared_secret
+ .decrypt_pin_token(permission, encrypted_pin_token.as_ref())
+ .map_err(CommandError::from)?;
+ Ok(pin_token)
+ } else {
+ Err(HIDError::Command(CommandError::MissingRequiredField(
+ "pin_token",
+ )))
+ }
+ }
+}
+
+pub trait VirtualFidoDevice: FidoDevice {
+ fn check_key_handle(&self, req: &CheckKeyHandle) -> Result<(), HIDError>;
+ fn client_pin(&self, req: &ClientPIN) -> Result<ClientPinResponse, HIDError>;
+ fn get_assertion(&self, req: &GetAssertion) -> Result<Vec<GetAssertionResult>, HIDError>;
+ fn get_info(&self) -> Result<AuthenticatorInfo, HIDError>;
+ fn get_version(&self, req: &GetVersion) -> Result<U2FInfo, HIDError>;
+ fn make_credentials(&self, req: &MakeCredentials) -> Result<MakeCredentialsResult, HIDError>;
+ fn reset(&self, req: &Reset) -> Result<(), HIDError>;
+ fn selection(&self, req: &Selection) -> Result<(), HIDError>;
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/device.rs b/third_party/rust/authenticator/src/transport/netbsd/device.rs
new file mode 100644
index 0000000000..4b426b3b9b
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/device.rs
@@ -0,0 +1,248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+use crate::consts::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::fd::Fd;
+use crate::transport::platform::monitor::WrappedOpenDevice;
+use crate::transport::platform::uhid;
+use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret};
+use crate::u2ftypes::U2FDeviceInfo;
+use crate::util::io_err;
+use std::ffi::OsString;
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::mem;
+
+#[derive(Debug)]
+pub struct Device {
+ path: OsString,
+ fd: Fd,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<SharedSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+ protocol: FidoProtocol,
+}
+
+impl Device {
+ fn ping(&mut self) -> io::Result<()> {
+ for i in 0..10 {
+ let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE];
+
+ buf[0] = 0; // report number
+ buf[1] = 0xff; // CID_BROADCAST
+ buf[2] = 0xff;
+ buf[3] = 0xff;
+ buf[4] = 0xff;
+ buf[5] = 0x81; // ping
+ buf[6] = 0;
+ buf[7] = 1; // one byte
+
+ // Write ping request. Each write to the device contains
+ // exactly one report id byte[*] followed by exactly as
+ // many bytes as are in a report, and will be consumed all
+ // at once by /dev/uhidN. So we use plain write, not
+ // write_all to issue writes in a loop.
+ //
+ // [*] This is only for the internal authenticator-rs API,
+ // not for the USB HID protocol, which for a device with
+ // only one report id excludes the report id byte from the
+ // interrupt in/out pipe transfer format.
+ if self.write(&buf)? != buf.len() {
+ return Err(io_err("write ping failed"));
+ }
+
+ // Wait for response
+ let mut pfd: libc::pollfd = unsafe { mem::zeroed() };
+ pfd.fd = self.fd.fileno;
+ pfd.events = libc::POLLIN;
+ let nfds = unsafe { libc::poll(&mut pfd, 1, 100) };
+ if nfds == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ if nfds == 0 {
+ debug!("device timeout {}", i);
+ continue;
+ }
+
+ // Read response. When reports come in they are all
+ // exactly the same size, with no report id byte because
+ // there is only one report.
+ let n = self.read(&mut buf[1..])?;
+ if n != buf.len() - 1 {
+ return Err(io_err("read pong failed"));
+ }
+
+ return Ok(());
+ }
+
+ Err(io_err("no response from device"))
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.fd == other.fd
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.fd.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let bufp = buf.as_mut_ptr() as *mut libc::c_void;
+ let nread = unsafe { libc::read(self.fd.fileno, bufp, buf.len()) };
+ if nread == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(nread as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ // Always skip the first byte (report number)
+ let data = &buf[1..];
+ let data_ptr = data.as_ptr() as *const libc::c_void;
+ let nwrit = unsafe { libc::write(self.fd.fileno, data_ptr, data.len()) };
+ if nwrit == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ // Pretend we wrote the report number byte
+ Ok(nwrit as usize + 1)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = WrappedOpenDevice;
+ type Id = OsString;
+
+ fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("device found: {:?}", fido);
+ let mut res = Self {
+ path: fido.os_path,
+ fd: fido.fd,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ protocol: FidoProtocol::CTAP2,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ HIDDevice::pre_init(self)
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ HIDDevice::get_device_info(self)
+ .cap_flags
+ .contains(Capability::CBOR)
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ if !uhid::is_u2f_device(&self.fd) {
+ return false;
+ }
+ // This step is not strictly necessary -- NetBSD puts fido
+ // devices into raw mode automatically by default, but in
+ // principle that might change, and this serves as a test to
+ // verify that we're running on a kernel with support for raw
+ // mode at all so we don't get confused issuing writes that try
+ // to set the report descriptor rather than transfer data on
+ // the output interrupt pipe as we need.
+ match uhid::hid_set_raw(&self.fd, true) {
+ Ok(_) => (),
+ Err(_) => return false,
+ }
+ if self.ping().is_err() {
+ return false;
+ }
+ true
+ }
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: SharedSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1;
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/fd.rs b/third_party/rust/authenticator/src/transport/netbsd/fd.rs
new file mode 100644
index 0000000000..d45410843b
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/fd.rs
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::ffi::CString;
+use std::ffi::OsStr;
+use std::hash::{Hash, Hasher};
+use std::io;
+use std::mem;
+use std::os::raw::c_int;
+use std::os::unix::{ffi::OsStrExt, io::RawFd};
+
+#[derive(Debug)]
+pub struct Fd {
+ pub fileno: RawFd,
+}
+
+impl Fd {
+ pub fn open(path: &OsStr, flags: c_int) -> io::Result<Fd> {
+ let cpath = CString::new(path.as_bytes())?;
+ let rv = unsafe { libc::open(cpath.as_ptr(), flags) };
+ if rv == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(Fd { fileno: rv })
+ }
+}
+
+impl Drop for Fd {
+ fn drop(&mut self) {
+ unsafe { libc::close(self.fileno) };
+ }
+}
+
+impl PartialEq for Fd {
+ fn eq(&self, other: &Fd) -> bool {
+ let mut st: libc::stat = unsafe { mem::zeroed() };
+ let mut sto: libc::stat = unsafe { mem::zeroed() };
+ if unsafe { libc::fstat(self.fileno, &mut st) } == -1 {
+ return false;
+ }
+ if unsafe { libc::fstat(other.fileno, &mut sto) } == -1 {
+ return false;
+ }
+ (st.st_dev == sto.st_dev) & (st.st_ino == sto.st_ino)
+ }
+}
+
+impl Eq for Fd {}
+
+impl Hash for Fd {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ let mut st: libc::stat = unsafe { mem::zeroed() };
+ if unsafe { libc::fstat(self.fileno, &mut st) } == -1 {
+ return;
+ }
+ st.st_dev.hash(state);
+ st.st_ino.hash(state);
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/mod.rs b/third_party/rust/authenticator/src/transport/netbsd/mod.rs
new file mode 100644
index 0000000000..a0eabb6e06
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/mod.rs
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod fd;
+mod monitor;
+mod uhid;
diff --git a/third_party/rust/authenticator/src/transport/netbsd/monitor.rs b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs
new file mode 100644
index 0000000000..c521bdea8b
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use crate::transport::platform::fd::Fd;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::ffi::OsString;
+use std::sync::{mpsc::Sender, Arc};
+use std::thread;
+use std::time::Duration;
+
+// XXX Should use drvctl, but it doesn't do pubsub properly yet so
+// DRVGETEVENT requires write access to /dev/drvctl. Instead, for now,
+// just poll every 500ms.
+const POLL_TIMEOUT: u64 = 500;
+
+#[derive(Debug)]
+pub struct WrappedOpenDevice {
+ pub fd: Fd,
+ pub os_path: OsString,
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync,
+{
+ runloops: HashMap<OsString, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ // Loop until we're stopped by the controlling thread, or fail.
+ while alive() {
+ for n in 0..100 {
+ let uhidpath = OsString::from(format!("/dev/uhid{n}"));
+ match Fd::open(&uhidpath, libc::O_RDWR | libc::O_CLOEXEC) {
+ Ok(uhid) => {
+ // The device is available if it can be opened.
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![uhidpath.clone()]));
+ self.add_device(WrappedOpenDevice {
+ fd: uhid,
+ os_path: uhidpath,
+ });
+ }
+ Err(ref err) => match err.raw_os_error() {
+ Some(libc::EBUSY) => continue,
+ Some(libc::ENOENT) => break,
+ _ => self.remove_device(uhidpath),
+ },
+ }
+ }
+ thread::sleep(Duration::from_millis(POLL_TIMEOUT));
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn add_device(&mut self, fido: WrappedOpenDevice) {
+ let f = self.new_device_cb.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ let key = fido.os_path.clone();
+ debug!("Adding device {}", key.to_string_lossy());
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(fido, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: OsString) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path.to_string_lossy());
+ if let Some(runloop) = self.runloops.remove(&path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/transaction.rs b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs
new file mode 100644
index 0000000000..6b15f6751a
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let device_selector = DeviceSelector::run();
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/uhid.rs b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs
new file mode 100644
index 0000000000..ea183db998
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::io;
+use std::mem;
+use std::os::raw::c_int;
+use std::os::raw::c_uchar;
+
+use crate::transport::hidproto::has_fido_usage;
+use crate::transport::hidproto::ReportDescriptor;
+use crate::transport::platform::fd::Fd;
+use crate::util::io_err;
+
+/* sys/ioccom.h */
+
+const IOCPARM_MASK: u32 = 0x1fff;
+const IOCPARM_SHIFT: u32 = 16;
+const IOCGROUP_SHIFT: u32 = 8;
+
+//const IOC_VOID: u32 = 0x20000000;
+const IOC_OUT: u32 = 0x40000000;
+const IOC_IN: u32 = 0x80000000;
+//const IOC_INOUT: u32 = IOC_IN|IOC_OUT;
+
+macro_rules! ioctl {
+ ($dir:expr, $name:ident, $group:expr, $nr:expr, $ty:ty) => {
+ unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> {
+ let ioc = ($dir as u32)
+ | ((mem::size_of::<$ty>() as u32 & IOCPARM_MASK) << IOCPARM_SHIFT)
+ | (($group as u32) << IOCGROUP_SHIFT)
+ | ($nr as u32);
+ let rv = libc::ioctl(fd, ioc as libc::c_ulong, val);
+ if rv == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(rv)
+ }
+ };
+}
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+struct usb_ctl_report_desc {
+ ucrd_size: c_int,
+ ucrd_data: [c_uchar; 1024],
+}
+
+ioctl!(IOC_OUT, usb_get_report_desc, b'U', 21, usb_ctl_report_desc);
+
+fn read_report_descriptor(fd: &Fd) -> io::Result<ReportDescriptor> {
+ let mut desc = unsafe { mem::zeroed() };
+ unsafe { usb_get_report_desc(fd.fileno, &mut desc) }?;
+ if desc.ucrd_size < 0 {
+ return Err(io_err("negative report descriptor size"));
+ }
+ let size = desc.ucrd_size as usize;
+ let value = Vec::from(&desc.ucrd_data[..size]);
+ Ok(ReportDescriptor { value })
+}
+
+pub fn is_u2f_device(fd: &Fd) -> bool {
+ match read_report_descriptor(fd) {
+ Ok(desc) => has_fido_usage(desc),
+ Err(_) => false,
+ }
+}
+
+ioctl!(IOC_IN, usb_hid_set_raw_ioctl, b'h', 2, c_int);
+
+pub fn hid_set_raw(fd: &Fd, raw: bool) -> io::Result<()> {
+ let mut raw_int: c_int = if raw { 1 } else { 0 };
+ unsafe { usb_hid_set_raw_ioctl(fd.fileno, &mut raw_int) }?;
+ Ok(())
+}
diff --git a/third_party/rust/authenticator/src/transport/openbsd/device.rs b/third_party/rust/authenticator/src/transport/openbsd/device.rs
new file mode 100644
index 0000000000..f11ded7984
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/device.rs
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+use crate::consts::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::monitor::WrappedOpenDevice;
+use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret};
+use crate::u2ftypes::U2FDeviceInfo;
+use crate::util::{from_unix_result, io_err};
+use std::ffi::{CString, OsString};
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::mem;
+use std::os::unix::ffi::OsStrExt;
+
+#[derive(Debug)]
+pub struct Device {
+ path: OsString,
+ fd: libc::c_int,
+ in_rpt_size: usize,
+ out_rpt_size: usize,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<SharedSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+ protocol: FidoProtocol,
+}
+
+impl Device {
+ fn ping(&mut self) -> io::Result<()> {
+ let capacity = 256;
+
+ for _ in 0..10 {
+ let mut data = vec![0u8; capacity];
+
+ // Send 1 byte ping
+ // self.write_all requires Device to be mut. This can't be done at the moment,
+ // and this is a workaround anyways, so writing by hand instead.
+ self.write_all(&[0, 0xff, 0xff, 0xff, 0xff, 0x81, 0, 1])?;
+
+ // Wait for response
+ let mut pfd: libc::pollfd = unsafe { mem::zeroed() };
+ pfd.fd = self.fd;
+ pfd.events = libc::POLLIN;
+ if from_unix_result(unsafe { libc::poll(&mut pfd, 1, 100) })? == 0 {
+ debug!("device {:?} timeout", self.path);
+ continue;
+ }
+
+ // Read response
+ self.read(&mut data[..])?;
+
+ return Ok(());
+ }
+
+ Err(io_err("no response from device"))
+ }
+}
+
+impl Drop for Device {
+ fn drop(&mut self) {
+ // Close the fd, ignore any errors.
+ let _ = unsafe { libc::close(self.fd) };
+ debug!("device {:?} closed", self.path);
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let buf_ptr = buf.as_mut_ptr() as *mut libc::c_void;
+ let rv = unsafe { libc::read(self.fd, buf_ptr, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ // Always skip the first byte (report number)
+ let data = &buf[1..];
+ let data_ptr = data.as_ptr() as *const libc::c_void;
+ let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) };
+ Ok(from_unix_result(rv as usize)? + 1)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = WrappedOpenDevice;
+ type Id = OsString;
+
+ fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("device found: {:?}", fido);
+ let mut res = Self {
+ path: fido.os_path,
+ fd: fido.fd,
+ in_rpt_size: MAX_HID_RPT_SIZE,
+ out_rpt_size: MAX_HID_RPT_SIZE,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ protocol: FidoProtocol::CTAP2,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ self.in_rpt_size
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ self.out_rpt_size
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ HIDDevice::pre_init(self)
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ HIDDevice::get_device_info(self)
+ .cap_flags
+ .contains(Capability::CBOR)
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ debug!("device {:?} is U2F/FIDO", self.path);
+
+ // From OpenBSD's libfido2 in 6.6-current:
+ // "OpenBSD (as of 201910) has a bug that causes it to lose
+ // track of the DATA0/DATA1 sequence toggle across uhid device
+ // open and close. This is a terrible hack to work around it."
+ match self.ping() {
+ Ok(_) => true,
+ Err(err) => {
+ debug!("device {:?} is not responding: {}", self.path, err);
+ false
+ }
+ }
+ }
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: SharedSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1;
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/openbsd/mod.rs b/third_party/rust/authenticator/src/transport/openbsd/mod.rs
new file mode 100644
index 0000000000..fa02132e67
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/mod.rs
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod monitor;
diff --git a/third_party/rust/authenticator/src/transport/openbsd/monitor.rs b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs
new file mode 100644
index 0000000000..0ea5f3d0b8
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use crate::util::from_unix_result;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::ffi::{CString, OsString};
+use std::os::unix::ffi::OsStrExt;
+use std::os::unix::io::RawFd;
+use std::path::PathBuf;
+use std::sync::{mpsc::Sender, Arc};
+use std::thread;
+use std::time::Duration;
+
+const POLL_TIMEOUT: u64 = 500;
+
+#[derive(Debug)]
+pub struct WrappedOpenDevice {
+ pub fd: RawFd,
+ pub os_path: OsString,
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync,
+{
+ runloops: HashMap<OsString, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ // Loop until we're stopped by the controlling thread, or fail.
+ while alive() {
+ // Iterate the first 10 fido(4) devices.
+ for path in (0..10)
+ .map(|unit| PathBuf::from(&format!("/dev/fido/{}", unit)))
+ .filter(|path| path.exists())
+ {
+ let os_path = path.as_os_str().to_os_string();
+ let cstr = CString::new(os_path.as_bytes())?;
+
+ // Try to open the device.
+ let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) };
+ match from_unix_result(fd) {
+ Ok(fd) => {
+ // The device is available if it can be opened.
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![os_path.clone()]));
+ self.add_device(WrappedOpenDevice { fd, os_path });
+ }
+ Err(ref err) if err.raw_os_error() == Some(libc::EBUSY) => {
+ // The device is available but currently in use.
+ }
+ _ => {
+ // libc::ENODEV or any other error.
+ self.remove_device(os_path);
+ }
+ }
+ }
+
+ thread::sleep(Duration::from_millis(POLL_TIMEOUT));
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn add_device(&mut self, fido: WrappedOpenDevice) {
+ if !self.runloops.contains_key(&fido.os_path) {
+ let f = self.new_device_cb.clone();
+ let key = fido.os_path.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(fido, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+ }
+
+ fn remove_device(&mut self, path: OsString) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+ if let Some(runloop) = self.runloops.remove(&path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/openbsd/transaction.rs b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs
new file mode 100644
index 0000000000..6b15f6751a
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let device_selector = DeviceSelector::run();
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/stub/device.rs b/third_party/rust/authenticator/src/transport/stub/device.rs
new file mode 100644
index 0000000000..29d8a3ab9b
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/stub/device.rs
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::{FidoDevice, FidoProtocol};
+use crate::transport::{HIDError, SharedSecret};
+use crate::u2ftypes::U2FDeviceInfo;
+use std::hash::Hash;
+use std::io;
+use std::io::{Read, Write};
+use std::path::PathBuf;
+
+#[derive(Debug, Hash, PartialEq, Eq)]
+pub struct Device {}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ unimplemented!();
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ unimplemented!();
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ unimplemented!();
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = PathBuf;
+ type Id = PathBuf;
+
+ fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
+ unimplemented!();
+ }
+
+ fn id(&self) -> Self::Id {
+ unimplemented!()
+ }
+
+ fn get_cid(&self) -> &[u8; 4] {
+ unimplemented!();
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ unimplemented!();
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ unimplemented!();
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ unimplemented!();
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ unimplemented!();
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ unimplemented!();
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ unimplemented!();
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ unimplemented!();
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ unimplemented!();
+ }
+
+ fn initialized(&self) -> bool {
+ unimplemented!();
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ unimplemented!()
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ unimplemented!()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ unimplemented!()
+ }
+
+ fn set_shared_secret(&mut self, secret: SharedSecret) {
+ unimplemented!()
+ }
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ unimplemented!()
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ unimplemented!()
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ unimplemented!()
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/stub/mod.rs b/third_party/rust/authenticator/src/transport/stub/mod.rs
new file mode 100644
index 0000000000..0fab62d495
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/stub/mod.rs
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// No-op module to permit compiling token HID support for Android, where
+// no results are returned.
+
+#![allow(unused_variables)]
+
+pub mod device;
+pub mod transaction;
diff --git a/third_party/rust/authenticator/src/transport/stub/transaction.rs b/third_party/rust/authenticator/src/transport/stub/transaction.rs
new file mode 100644
index 0000000000..d471c94da8
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/stub/transaction.rs
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use std::path::PathBuf;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ _status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ // Just to silence "unused"-warnings
+ let mut device_selector = DeviceSelector::run();
+ let _ = DeviceSelectorEvent::DevicesAdded(vec![]);
+ let _ = DeviceSelectorEvent::DeviceRemoved(PathBuf::new());
+ let _ = device_selector.clone_sender();
+ device_selector.stop();
+
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotSupported,
+ )));
+
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotSupported,
+ ))
+ }
+
+ pub fn cancel(&mut self) {
+ /* No-op. */
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/windows/device.rs b/third_party/rust/authenticator/src/transport/windows/device.rs
new file mode 100644
index 0000000000..bda5d385cc
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/device.rs
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::winapi::DeviceCapabilities;
+use crate::consts::{
+ Capability, CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE,
+};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret};
+use crate::u2ftypes::U2FDeviceInfo;
+use std::fs::{File, OpenOptions};
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::os::windows::io::AsRawHandle;
+
+#[derive(Debug)]
+pub struct Device {
+ path: String,
+ file: File,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<SharedSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+ protocol: FidoProtocol,
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
+ // Windows always includes the report ID.
+ let mut input = [0u8; MAX_HID_RPT_SIZE + 1];
+ let _ = self.file.read(&mut input)?;
+ bytes.clone_from_slice(&input[1..]);
+ Ok(bytes.len())
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
+ self.file.write(bytes)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.file.flush()
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = String;
+ type Id = String;
+
+ fn new(path: String) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("Opening device {:?}", path);
+ let file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&path)
+ .map_err(|e| (HIDError::IO(Some(path.clone().into()), e), path.clone()))?;
+ let mut res = Self {
+ path,
+ file,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ protocol: FidoProtocol::CTAP2,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path))
+ }
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl FidoDevice for Device {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ HIDDevice::pre_init(self)
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ HIDDevice::get_device_info(self)
+ .cap_flags
+ .contains(Capability::CBOR)
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ match DeviceCapabilities::new(self.file.as_raw_handle()) {
+ Ok(caps) => caps.usage() == FIDO_USAGE_U2FHID && caps.usage_page() == FIDO_USAGE_PAGE,
+ _ => false,
+ }
+ }
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: SharedSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1;
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/windows/mod.rs b/third_party/rust/authenticator/src/transport/windows/mod.rs
new file mode 100644
index 0000000000..09135391dd
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod monitor;
+mod winapi;
diff --git a/third_party/rust/authenticator/src/transport/windows/monitor.rs b/third_party/rust/authenticator/src/transport/windows/monitor.rs
new file mode 100644
index 0000000000..c73c012b05
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/monitor.rs
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::{DeviceID, DeviceSelectorEvent};
+use crate::transport::platform::winapi::DeviceInfoSet;
+use runloop::RunLoop;
+use std::collections::{HashMap, HashSet};
+use std::error::Error;
+use std::iter::FromIterator;
+use std::sync::{mpsc::Sender, Arc};
+use std::thread;
+use std::time::Duration;
+
+pub struct Monitor<F>
+where
+ F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Sync,
+{
+ runloops: HashMap<String, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ let mut current = HashSet::new();
+ let mut previous;
+
+ while alive() {
+ let device_info_set = DeviceInfoSet::new()?;
+ previous = current;
+ current = HashSet::from_iter(device_info_set.devices());
+
+ // Remove devices that are gone.
+ for path in previous.difference(&current) {
+ self.remove_device(path);
+ }
+
+ let added: Vec<String> = current.difference(&previous).cloned().collect();
+
+ // We have to notify additions in batches to avoid
+ // arbitrarily selecting the first added device.
+ if !added.is_empty()
+ && self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(added.clone()))
+ .is_err()
+ {
+ // Send only fails if the receiver hung up. We should exit the loop.
+ break;
+ }
+
+ // Add devices that were plugged in.
+ for path in added {
+ self.add_device(&path);
+ }
+
+ // Wait a little before looking for devices again.
+ thread::sleep(Duration::from_millis(100));
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn add_device(&mut self, path: &DeviceID) {
+ let f = self.new_device_cb.clone();
+ let path = path.clone();
+ let key = path.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ debug!("Adding device {}", path);
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(path, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: &DeviceID) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path);
+ if let Some(runloop) = self.runloops.remove(path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(&path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/windows/transaction.rs b/third_party/rust/authenticator/src/transport/windows/transaction.rs
new file mode 100644
index 0000000000..6b15f6751a
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/transaction.rs
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let device_selector = DeviceSelector::run();
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/windows/winapi.rs b/third_party/rust/authenticator/src/transport/windows/winapi.rs
new file mode 100644
index 0000000000..44b4489811
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/winapi.rs
@@ -0,0 +1,263 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::io;
+use std::mem;
+use std::ptr;
+use std::slice;
+
+use std::ffi::OsString;
+use std::os::windows::ffi::OsStringExt;
+
+use crate::util::io_err;
+
+extern crate libc;
+extern crate winapi;
+
+use winapi::shared::{guiddef, minwindef, ntdef, windef};
+use winapi::shared::{hidclass, hidpi, hidusage};
+use winapi::um::{handleapi, setupapi};
+
+#[link(name = "setupapi")]
+extern "system" {
+ fn SetupDiGetClassDevsW(
+ ClassGuid: *const guiddef::GUID,
+ Enumerator: ntdef::PCSTR,
+ hwndParent: windef::HWND,
+ flags: minwindef::DWORD,
+ ) -> setupapi::HDEVINFO;
+
+ fn SetupDiDestroyDeviceInfoList(DeviceInfoSet: setupapi::HDEVINFO) -> minwindef::BOOL;
+
+ fn SetupDiEnumDeviceInterfaces(
+ DeviceInfoSet: setupapi::HDEVINFO,
+ DeviceInfoData: setupapi::PSP_DEVINFO_DATA,
+ InterfaceClassGuid: *const guiddef::GUID,
+ MemberIndex: minwindef::DWORD,
+ DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA,
+ ) -> minwindef::BOOL;
+
+ fn SetupDiGetDeviceInterfaceDetailW(
+ DeviceInfoSet: setupapi::HDEVINFO,
+ DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA,
+ DeviceInterfaceDetailData: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W,
+ DeviceInterfaceDetailDataSize: minwindef::DWORD,
+ RequiredSize: minwindef::PDWORD,
+ DeviceInfoData: setupapi::PSP_DEVINFO_DATA,
+ ) -> minwindef::BOOL;
+}
+
+#[link(name = "hid")]
+extern "system" {
+ fn HidD_GetPreparsedData(
+ HidDeviceObject: ntdef::HANDLE,
+ PreparsedData: *mut hidpi::PHIDP_PREPARSED_DATA,
+ ) -> ntdef::BOOLEAN;
+
+ fn HidD_FreePreparsedData(PreparsedData: hidpi::PHIDP_PREPARSED_DATA) -> ntdef::BOOLEAN;
+
+ fn HidP_GetCaps(
+ PreparsedData: hidpi::PHIDP_PREPARSED_DATA,
+ Capabilities: hidpi::PHIDP_CAPS,
+ ) -> ntdef::NTSTATUS;
+}
+
+fn from_wide_ptr(ptr: *const u16, len: usize) -> String {
+ assert!(!ptr.is_null() && len % 2 == 0);
+ let slice = unsafe { slice::from_raw_parts(ptr, len / 2) };
+ OsString::from_wide(slice).to_string_lossy().into_owned()
+}
+
+pub struct DeviceInfoSet {
+ set: setupapi::HDEVINFO,
+}
+
+impl DeviceInfoSet {
+ pub fn new() -> io::Result<Self> {
+ let flags = setupapi::DIGCF_PRESENT | setupapi::DIGCF_DEVICEINTERFACE;
+ let set = unsafe {
+ SetupDiGetClassDevsW(
+ &hidclass::GUID_DEVINTERFACE_HID,
+ ptr::null_mut(),
+ ptr::null_mut(),
+ flags,
+ )
+ };
+ if set == handleapi::INVALID_HANDLE_VALUE {
+ return Err(io_err("SetupDiGetClassDevsW failed!"));
+ }
+
+ Ok(Self { set })
+ }
+
+ pub fn get(&self) -> setupapi::HDEVINFO {
+ self.set
+ }
+
+ pub fn devices(&self) -> DeviceInfoSetIter {
+ DeviceInfoSetIter::new(self)
+ }
+}
+
+impl Drop for DeviceInfoSet {
+ fn drop(&mut self) {
+ let _ = unsafe { SetupDiDestroyDeviceInfoList(self.set) };
+ }
+}
+
+pub struct DeviceInfoSetIter<'a> {
+ set: &'a DeviceInfoSet,
+ index: minwindef::DWORD,
+}
+
+impl<'a> DeviceInfoSetIter<'a> {
+ fn new(set: &'a DeviceInfoSet) -> Self {
+ Self { set, index: 0 }
+ }
+}
+
+impl<'a> Iterator for DeviceInfoSetIter<'a> {
+ type Item = String;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let mut device_interface_data =
+ mem::MaybeUninit::<setupapi::SP_DEVICE_INTERFACE_DATA>::zeroed();
+ unsafe {
+ (*device_interface_data.as_mut_ptr()).cbSize =
+ mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DATA>() as minwindef::UINT;
+ }
+
+ let rv = unsafe {
+ SetupDiEnumDeviceInterfaces(
+ self.set.get(),
+ ptr::null_mut(),
+ &hidclass::GUID_DEVINTERFACE_HID,
+ self.index,
+ device_interface_data.as_mut_ptr(),
+ )
+ };
+ if rv == 0 {
+ return None; // We're past the last device index.
+ }
+
+ // Determine the size required to hold a detail struct.
+ let mut required_size = 0;
+ unsafe {
+ SetupDiGetDeviceInterfaceDetailW(
+ self.set.get(),
+ device_interface_data.as_mut_ptr(),
+ ptr::null_mut(),
+ required_size,
+ &mut required_size,
+ ptr::null_mut(),
+ )
+ };
+ if required_size == 0 {
+ return None; // An error occurred.
+ }
+
+ let detail = DeviceInterfaceDetailData::new(required_size as usize)?;
+ let rv = unsafe {
+ SetupDiGetDeviceInterfaceDetailW(
+ self.set.get(),
+ device_interface_data.as_mut_ptr(),
+ detail.get(),
+ required_size,
+ ptr::null_mut(),
+ ptr::null_mut(),
+ )
+ };
+ if rv == 0 {
+ return None; // An error occurred.
+ }
+
+ self.index += 1;
+ Some(detail.path())
+ }
+}
+
+struct DeviceInterfaceDetailData {
+ data: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W,
+ path_len: usize,
+}
+
+impl DeviceInterfaceDetailData {
+ fn new(size: usize) -> Option<Self> {
+ let mut cb_size = mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W>();
+ if cfg!(target_pointer_width = "32") {
+ cb_size = 4 + 2; // 4-byte uint + default TCHAR size. size_of is inaccurate.
+ }
+
+ if size < cb_size {
+ warn!("DeviceInterfaceDetailData is too small. {}", size);
+ return None;
+ }
+
+ let data = unsafe { libc::malloc(size) as setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W };
+ if data.is_null() {
+ return None;
+ }
+
+ // Set total size of the structure.
+ unsafe { (*data).cbSize = cb_size as minwindef::UINT };
+
+ // Compute offset of `SP_DEVICE_INTERFACE_DETAIL_DATA_W.DevicePath`.
+ let offset = memoffset::offset_of!(setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W, DevicePath);
+
+ Some(Self {
+ data,
+ path_len: size - offset,
+ })
+ }
+
+ fn get(&self) -> setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W {
+ self.data
+ }
+
+ fn path(&self) -> String {
+ unsafe { from_wide_ptr(ptr::addr_of!((*self.data).DevicePath[0]), self.path_len - 2) }
+ }
+}
+
+impl Drop for DeviceInterfaceDetailData {
+ fn drop(&mut self) {
+ unsafe { libc::free(self.data as *mut libc::c_void) };
+ }
+}
+
+pub struct DeviceCapabilities {
+ caps: hidpi::HIDP_CAPS,
+}
+
+impl DeviceCapabilities {
+ pub fn new(handle: ntdef::HANDLE) -> io::Result<Self> {
+ let mut preparsed_data = ptr::null_mut();
+ let rv = unsafe { HidD_GetPreparsedData(handle, &mut preparsed_data) };
+ if rv == 0 || preparsed_data.is_null() {
+ return Err(io_err("HidD_GetPreparsedData failed!"));
+ }
+
+ let mut caps = mem::MaybeUninit::<hidpi::HIDP_CAPS>::uninit();
+ unsafe {
+ let rv = HidP_GetCaps(preparsed_data, caps.as_mut_ptr());
+ HidD_FreePreparsedData(preparsed_data);
+
+ if rv != hidpi::HIDP_STATUS_SUCCESS {
+ return Err(io_err("HidP_GetCaps failed!"));
+ }
+
+ Ok(Self {
+ caps: caps.assume_init(),
+ })
+ }
+ }
+
+ pub fn usage(&self) -> hidusage::USAGE {
+ self.caps.Usage
+ }
+
+ pub fn usage_page(&self) -> hidusage::USAGE {
+ self.caps.UsagePage
+ }
+}
diff --git a/third_party/rust/authenticator/src/u2ftypes.rs b/third_party/rust/authenticator/src/u2ftypes.rs
new file mode 100644
index 0000000000..0237f3acb7
--- /dev/null
+++ b/third_party/rust/authenticator/src/u2ftypes.rs
@@ -0,0 +1,341 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::consts::*;
+use crate::transport::hid::HIDDevice;
+use crate::util::io_err;
+use serde::Serialize;
+use std::{cmp, fmt, io, str};
+
+pub fn to_hex(data: &[u8], joiner: &str) -> String {
+ let parts: Vec<String> = data.iter().map(|byte| format!("{byte:02x}")).collect();
+ parts.join(joiner)
+}
+
+pub fn trace_hex(data: &[u8]) {
+ if log_enabled!(log::Level::Trace) {
+ trace!("USB send: {}", to_hex(data, ""));
+ }
+}
+
+// Init structure for U2F Communications. Tells the receiver what channel
+// communication is happening on, what command is running, and how much data to
+// expect to receive over all.
+//
+// Spec at https://fidoalliance.org/specs/fido-u2f-v1.
+// 0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.html#message--and-packet-structure
+pub struct U2FHIDInit {}
+
+impl U2FHIDInit {
+ pub fn read<T: HIDDevice>(dev: &mut T) -> io::Result<(HIDCmd, Vec<u8>)> {
+ let mut frame = vec![0u8; dev.in_rpt_size()];
+ let mut count = dev.read(&mut frame)?;
+
+ while dev.get_cid() != &frame[..4] {
+ count = dev.read(&mut frame)?;
+ }
+
+ if count != dev.in_rpt_size() {
+ return Err(io_err("invalid init packet"));
+ }
+
+ let cmd = HIDCmd::from(frame[4] | TYPE_INIT);
+
+ let cap = (frame[5] as usize) << 8 | (frame[6] as usize);
+ let mut data = Vec::with_capacity(cap);
+
+ let len = if dev.in_rpt_size() >= INIT_HEADER_SIZE {
+ cmp::min(cap, dev.in_rpt_size() - INIT_HEADER_SIZE)
+ } else {
+ cap
+ };
+ data.extend_from_slice(&frame[7..7 + len]);
+
+ Ok((cmd, data))
+ }
+
+ pub fn write<T: HIDDevice>(dev: &mut T, cmd: u8, data: &[u8]) -> io::Result<usize> {
+ if data.len() > 0xffff {
+ return Err(io_err("payload length > 2^16"));
+ }
+
+ let mut frame = vec![0u8; dev.out_rpt_size() + 1];
+ frame[1..5].copy_from_slice(dev.get_cid());
+ frame[5] = cmd;
+ frame[6] = (data.len() >> 8) as u8;
+ frame[7] = data.len() as u8;
+
+ let count = if dev.out_rpt_size() >= INIT_HEADER_SIZE {
+ cmp::min(data.len(), dev.out_rpt_size() - INIT_HEADER_SIZE)
+ } else {
+ data.len()
+ };
+ frame[8..8 + count].copy_from_slice(&data[..count]);
+ trace_hex(&frame);
+
+ if dev.write(&frame)? != frame.len() {
+ return Err(io_err("device write failed"));
+ }
+
+ Ok(count)
+ }
+}
+
+// Continuation structure for U2F Communications. After an Init structure is
+// sent, continuation structures are used to transmit all extra data that
+// wouldn't fit in the initial packet. The sequence number increases with every
+// packet, until all data is received.
+//
+// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.
+// html#message--and-packet-structure
+pub struct U2FHIDCont {}
+
+impl U2FHIDCont {
+ pub fn read<T: HIDDevice>(dev: &mut T, seq: u8, max: usize) -> io::Result<Vec<u8>> {
+ let mut frame = vec![0u8; dev.in_rpt_size()];
+ let mut count = dev.read(&mut frame)?;
+
+ while dev.get_cid() != &frame[..4] {
+ count = dev.read(&mut frame)?;
+ }
+
+ if count != dev.in_rpt_size() {
+ return Err(io_err("invalid cont packet"));
+ }
+
+ if seq != frame[4] {
+ return Err(io_err("invalid sequence number"));
+ }
+
+ let max = if dev.in_rpt_size() >= CONT_HEADER_SIZE {
+ cmp::min(max, dev.in_rpt_size() - CONT_HEADER_SIZE)
+ } else {
+ max
+ };
+ Ok(frame[5..5 + max].to_vec())
+ }
+
+ pub fn write<T: HIDDevice>(dev: &mut T, seq: u8, data: &[u8]) -> io::Result<usize> {
+ let mut frame = vec![0u8; dev.out_rpt_size() + 1];
+ frame[1..5].copy_from_slice(dev.get_cid());
+ frame[5] = seq;
+
+ let count = if dev.out_rpt_size() >= CONT_HEADER_SIZE {
+ cmp::min(data.len(), dev.out_rpt_size() - CONT_HEADER_SIZE)
+ } else {
+ data.len()
+ };
+ frame[6..6 + count].copy_from_slice(&data[..count]);
+ trace_hex(&frame);
+
+ if dev.write(&frame)? != frame.len() {
+ return Err(io_err("device write failed"));
+ }
+
+ Ok(count)
+ }
+}
+
+// Reply sent after initialization command. Contains information about U2F USB
+// Key versioning, as well as the communication channel to be used for all
+// further requests.
+//
+// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.
+// html#u2fhid_init
+pub struct U2FHIDInitResp {
+ pub cid: [u8; 4],
+ pub version_interface: u8,
+ pub version_major: u8,
+ pub version_minor: u8,
+ pub version_build: u8,
+ pub cap_flags: Capability,
+}
+
+impl U2FHIDInitResp {
+ pub fn read(data: &[u8], nonce: &[u8]) -> io::Result<U2FHIDInitResp> {
+ assert_eq!(nonce.len(), INIT_NONCE_SIZE);
+
+ if data.len() < INIT_NONCE_SIZE + 9 {
+ return Err(io_err("invalid init response"));
+ }
+
+ if nonce != &data[..INIT_NONCE_SIZE] {
+ return Err(io_err("invalid nonce"));
+ }
+
+ let rsp = U2FHIDInitResp {
+ cid: [
+ data[INIT_NONCE_SIZE],
+ data[INIT_NONCE_SIZE + 1],
+ data[INIT_NONCE_SIZE + 2],
+ data[INIT_NONCE_SIZE + 3],
+ ],
+ version_interface: data[INIT_NONCE_SIZE + 4],
+ version_major: data[INIT_NONCE_SIZE + 5],
+ version_minor: data[INIT_NONCE_SIZE + 6],
+ version_build: data[INIT_NONCE_SIZE + 7],
+ cap_flags: Capability::from_bits_truncate(data[INIT_NONCE_SIZE + 8]),
+ };
+
+ Ok(rsp)
+ }
+}
+
+/// CTAP1 (FIDO v1.x / U2F / "APDU-like") request framing format, used for
+/// communication with authenticators over *all* transports.
+///
+/// This implementation follows the [FIDO v1.2 spec][fido12rawf], but only
+/// implements extended APDUs (supported by USB HID, NFC and BLE transports).
+///
+/// # Technical details
+///
+/// FIDO v1.0 U2F framing [claims][fido10rawf] to be based on
+/// [ISO/IEC 7816-4:2005][iso7816] (smart card) APDUs, but has several
+/// differences, errors and omissions which make it incompatible.
+///
+/// FIDO v1.1 and v1.2 fixed *most* of these issues, but as a result is *not*
+/// fully compatible with the FIDO v1.0 specification:
+///
+/// * FIDO v1.0 *only* defines extended APDUs, though
+/// [v1.0 NFC implementors][fido10nfc] need to also handle short APDUs.
+///
+/// FIDO v1.1 and later define *both* short and extended APDUs, but defers to
+/// transport-level guidance about which to use (where extended APDU support
+/// is mandatory for all transports, and short APDU support is only mandatory
+/// for NFC transports).
+///
+/// * FIDO v1.0 doesn't special-case N<sub>c</sub> (command data length) = 0
+/// (ie: L<sub>c</sub> is *always* present).
+///
+/// * FIDO v1.0 declares extended L<sub>c</sub> as a 24-bit integer, rather than
+/// 16-bit with padding byte.
+///
+/// * FIDO v1.0 omits L<sub>e</sub> bytes entirely,
+/// [except for short APDUs over NFC][fido10nfc].
+///
+/// Unfortunately, FIDO v2.x gives ambiguous compatibility guidance:
+///
+/// * [The FIDO v2.0 spec describes framing][fido20u2f] in
+/// [FIDO v1.0 U2F Raw Message Format][fido10rawf], [cites][fido20u2fcite] the
+/// FIDO v1.0 format by *name*, but actually links to the
+/// [FIDO v1.2 format][fido12rawf].
+///
+/// * [The FIDO v2.1 spec also describes framing][fido21u2f] in
+/// [FIDO v1.0 U2F Raw Message Format][fido10rawf], but [cites][fido21u2fcite]
+/// the [FIDO v1.2 U2F Raw Message Format][fido12rawf] as a reference by name
+/// and URL.
+///
+/// [fido10nfc]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-nfc-protocol.html#framing
+/// [fido10raw]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html
+/// [fido10rawf]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#u2f-message-framing
+/// [fido12rawf]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#u2f-message-framing
+/// [fido20u2f]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-framing
+/// [fido20u2fcite]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#biblio-u2frawmsgs
+/// [fido21u2f]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#u2f-framing
+/// [fido21u2fcite]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#biblio-u2frawmsgs
+/// [iso7816]: https://www.iso.org/standard/36134.html
+pub struct CTAP1RequestAPDU {}
+
+impl CTAP1RequestAPDU {
+ /// Serializes a CTAP command into
+ /// [FIDO v1.2 U2F Raw Message Format][fido12raw]. See
+ /// [the struct documentation][Self] for implementation notes.
+ ///
+ /// # Arguments
+ ///
+ /// * `ins`: U2F command code, as documented in
+ /// [FIDO v1.2 U2F Raw Format][fido12cmd].
+ /// * `p1`: Command parameter 1 / control byte.
+ /// * `data`: Request data, as documented in
+ /// [FIDO v1.2 Raw Message Formats][fido12raw], of up to 65535 bytes.
+ ///
+ /// [fido12cmd]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#command-and-parameter-values
+ /// [fido12raw]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html
+ pub fn serialize(ins: u8, p1: u8, data: &[u8]) -> io::Result<Vec<u8>> {
+ if data.len() > 0xffff {
+ return Err(io_err("payload length > 2^16"));
+ }
+ // Size of header + data.
+ let data_size = if data.is_empty() { 0 } else { 2 + data.len() };
+ let mut bytes = vec![0u8; U2FAPDUHEADER_SIZE + data_size];
+
+ // bytes[0] (CLA): Always 0 in FIDO v1.x.
+ bytes[1] = ins;
+ bytes[2] = p1;
+ // bytes[3] (P2): Always 0 in FIDO v1.x.
+
+ // bytes[4] (Lc1/Le1): Always 0 for extended APDUs.
+ if !data.is_empty() {
+ bytes[5] = (data.len() >> 8) as u8; // Lc2
+ bytes[6] = data.len() as u8; // Lc3
+
+ bytes[7..7 + data.len()].copy_from_slice(data);
+ }
+
+ // Last two bytes (Le): Always 0 for Ne = 65536
+ Ok(bytes)
+ }
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct U2FDeviceInfo {
+ pub vendor_name: Vec<u8>,
+ pub device_name: Vec<u8>,
+ pub version_interface: u8,
+ pub version_major: u8,
+ pub version_minor: u8,
+ pub version_build: u8,
+ pub cap_flags: Capability,
+}
+
+impl fmt::Display for U2FDeviceInfo {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "Vendor: {}, Device: {}, Interface: {}, Firmware: v{}.{}.{}, Capabilities: {}",
+ str::from_utf8(&self.vendor_name).unwrap(),
+ str::from_utf8(&self.device_name).unwrap(),
+ &self.version_interface,
+ &self.version_major,
+ &self.version_minor,
+ &self.version_build,
+ to_hex(&[self.cap_flags.bits()], ":"),
+ )
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+pub(crate) mod tests {
+ use super::CTAP1RequestAPDU;
+
+ #[test]
+ fn test_ctap1_serialize() {
+ // Command with no data, Lc should be omitted.
+ assert_eq!(
+ vec![0, 1, 2, 0, 0, 0, 0],
+ CTAP1RequestAPDU::serialize(1, 2, &[]).unwrap()
+ );
+
+ // Command with data, Lc should be included.
+ assert_eq!(
+ vec![0, 1, 2, 0, 0, 0, 1, 42, 0, 0],
+ CTAP1RequestAPDU::serialize(1, 2, &[42]).unwrap()
+ );
+
+ // Command with 300 bytes data, longer Lc.
+ let d = [0xFF; 300];
+ let mut expected = vec![0, 1, 2, 0, 0, 0x1, 0x2c];
+ expected.extend_from_slice(&d);
+ expected.extend_from_slice(&[0, 0]); // Lc
+ assert_eq!(expected, CTAP1RequestAPDU::serialize(1, 2, &d).unwrap());
+
+ // Command with 64k of data should error
+ let big = [0xFF; 65536];
+ assert!(CTAP1RequestAPDU::serialize(1, 2, &big).is_err());
+ }
+}
diff --git a/third_party/rust/authenticator/src/util.rs b/third_party/rust/authenticator/src/util.rs
new file mode 100644
index 0000000000..034259e85a
--- /dev/null
+++ b/third_party/rust/authenticator/src/util.rs
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::io;
+
+macro_rules! try_or {
+ ($val:expr, $or:expr) => {
+ match $val {
+ Ok(v) => v,
+ Err(e) => {
+ #[allow(clippy::redundant_closure_call)]
+ return $or(e);
+ }
+ }
+ };
+}
+
+pub trait Signed {
+ fn is_negative(&self) -> bool;
+}
+
+impl Signed for i32 {
+ fn is_negative(&self) -> bool {
+ *self < 0
+ }
+}
+
+impl Signed for usize {
+ fn is_negative(&self) -> bool {
+ (*self as isize) < 0
+ }
+}
+
+#[cfg(all(target_os = "linux", not(test)))]
+pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> {
+ if rv.is_negative() {
+ let errno = unsafe { *libc::__errno_location() };
+ Err(io::Error::from_raw_os_error(errno))
+ } else {
+ Ok(rv)
+ }
+}
+
+#[cfg(all(target_os = "freebsd", not(test)))]
+pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> {
+ if rv.is_negative() {
+ let errno = unsafe { *libc::__error() };
+ Err(io::Error::from_raw_os_error(errno))
+ } else {
+ Ok(rv)
+ }
+}
+
+#[cfg(all(target_os = "openbsd", not(test)))]
+pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> {
+ if rv.is_negative() {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(rv)
+ }
+}
+
+pub fn io_err(msg: &str) -> io::Error {
+ io::Error::new(io::ErrorKind::Other, msg)
+}
+
+#[cfg(all(test, not(feature = "crypto_dummy")))]
+pub fn decode_hex(s: &str) -> Vec<u8> {
+ (0..s.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
+ .collect()
+}
diff --git a/third_party/rust/authenticator/testing/cross/powerpc64le-unknown-linux-gnu.Dockerfile b/third_party/rust/authenticator/testing/cross/powerpc64le-unknown-linux-gnu.Dockerfile
new file mode 100644
index 0000000000..f24da41678
--- /dev/null
+++ b/third_party/rust/authenticator/testing/cross/powerpc64le-unknown-linux-gnu.Dockerfile
@@ -0,0 +1,8 @@
+FROM rustembedded/cross:powerpc64le-unknown-linux-gnu-0.2.1
+
+RUN dpkg --add-architecture powerpc64le && \
+ apt-get update && \
+ apt-get install --assume-yes libudev-dev
+
+RUN pkg-config --list-all && pkg-config --libs libudev && \
+ pkg-config --modversion libudev \ No newline at end of file
diff --git a/third_party/rust/authenticator/testing/cross/x86_64-unknown-linux-gnu.Dockerfile b/third_party/rust/authenticator/testing/cross/x86_64-unknown-linux-gnu.Dockerfile
new file mode 100644
index 0000000000..016ad4a362
--- /dev/null
+++ b/third_party/rust/authenticator/testing/cross/x86_64-unknown-linux-gnu.Dockerfile
@@ -0,0 +1,7 @@
+FROM rustembedded/cross:x86_64-unknown-linux-gnu-0.2.1
+
+RUN apt-get update && \
+ apt-get install --assume-yes libudev-dev
+
+RUN pkg-config --list-all && pkg-config --libs libudev && \
+ pkg-config --modversion libudev \ No newline at end of file
diff --git a/third_party/rust/authenticator/testing/run_cross.sh b/third_party/rust/authenticator/testing/run_cross.sh
new file mode 100755
index 0000000000..e62b8bd492
--- /dev/null
+++ b/third_party/rust/authenticator/testing/run_cross.sh
@@ -0,0 +1,11 @@
+#!/bin/bash -xe
+
+pushd testing/cross/
+docker build -t local_cross:x86_64-unknown-linux-gnu -f x86_64-unknown-linux-gnu.Dockerfile .
+docker build -t local_cross:powerpc64le-unknown-linux-gnu -f powerpc64le-unknown-linux-gnu.Dockerfile .
+popd
+
+cross test --target x86_64-unknown-linux-gnu
+cross build --target x86_64-unknown-netbsd
+cross build --target powerpc64le-unknown-linux-gnu
+cross build --target x86_64-pc-windows-gnu