summaryrefslogtreecommitdiffstats
path: root/third_party/rust/authenticator
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/rust/authenticator
parentInitial commit. (diff)
downloadthunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz
thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
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.lock1586
-rw-r--r--third_party/rust/authenticator/Cargo.toml174
-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.rs62
-rw-r--r--third_party/rust/authenticator/examples/ctap2.rs294
-rw-r--r--third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs366
-rw-r--r--third_party/rust/authenticator/examples/interactive_management.rs204
-rw-r--r--third_party/rust/authenticator/examples/reset.rs140
-rw-r--r--third_party/rust/authenticator/examples/set_pin.rs160
-rw-r--r--third_party/rust/authenticator/examples/test_exclude_list.rs315
-rw-r--r--third_party/rust/authenticator/rustfmt.toml3
-rw-r--r--third_party/rust/authenticator/src/authenticatorservice.rs687
-rw-r--r--third_party/rust/authenticator/src/consts.rs151
-rw-r--r--third_party/rust/authenticator/src/crypto/dummy.rs39
-rw-r--r--third_party/rust/authenticator/src/crypto/mod.rs1343
-rw-r--r--third_party/rust/authenticator/src/crypto/nss.rs397
-rw-r--r--third_party/rust/authenticator/src/crypto/openssl.rs165
-rw-r--r--third_party/rust/authenticator/src/ctap2/attestation.rs860
-rw-r--r--third_party/rust/authenticator/src/ctap2/client_data.rs338
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/client_pin.rs769
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs1504
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_info.rs983
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs50
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_version.rs110
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs1079
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/mod.rs480
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/reset.rs119
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/selection.rs119
-rw-r--r--third_party/rust/authenticator/src/ctap2/mod.rs10
-rw-r--r--third_party/rust/authenticator/src/ctap2/preflight.rs196
-rw-r--r--third_party/rust/authenticator/src/ctap2/server.rs532
-rw-r--r--third_party/rust/authenticator/src/ctap2/utils.rs14
-rw-r--r--third_party/rust/authenticator/src/errors.rs135
-rw-r--r--third_party/rust/authenticator/src/lib.rs111
-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.rs1521
-rw-r--r--third_party/rust/authenticator/src/status_update.rs89
-rw-r--r--third_party/rust/authenticator/src/transport/device_selector.rs475
-rw-r--r--third_party/rust/authenticator/src/transport/errors.rs98
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/device.rs217
-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.rs153
-rw-r--r--third_party/rust/authenticator/src/transport/hidproto.rs257
-rw-r--r--third_party/rust/authenticator/src/transport/linux/device.rs163
-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.rs51
-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_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.rs209
-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.rs181
-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.rs334
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/device.rs230
-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.rs206
-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.rs101
-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.rs154
-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/u2fprotocol.rs398
-rw-r--r--third_party/rust/authenticator/src/u2ftypes.rs363
-rw-r--r--third_party/rust/authenticator/src/util.rs75
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/mod.rs8
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/software_u2f.rs65
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs140
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs151
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs964
-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
-rw-r--r--third_party/rust/authenticator/webdriver-tools/requirements.txt2
-rw-r--r--third_party/rust/authenticator/webdriver-tools/webdriver-driver.py207
110 files changed, 23665 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..080c46c4c0
--- /dev/null
+++ b/third_party/rust/authenticator/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.lock":"803a1ca7735f93e1d952a07291a6976db787b6530bc67f9e3d2ae2dcaf8a90cc","Cargo.toml":"e8f07adde7f2c71a96cbe3809ab605a9082b8ccaf8d2a69aacb6d5db90fddcdc","Cross.toml":"8d132da818d48492aa9f4b78a348f0df3adfae45d988d42ebd6be8a5adadb6c3","LICENSE":"e866c8f5864d4cacfe403820e722e9dc03fe3c7565efa5e4dad9051d827bb92a","README.md":"c87d9c7cc44f1dd4ef861a3a9f8cd2eb68aedd3814768871f5fb63c2070806cd","build.rs":"01092254718e4cd5d6bffcd64d55cc3240dc00e79f3d7344a5dc4abf6c27bca6","examples/ctap2.rs":"51709e50dd23477f6f91225c09fca08824a00abdc851727b2f3bd9dcd746378e","examples/ctap2_discoverable_creds.rs":"952207c39bad1995998c686f99fbca39268e930099b0086a09adeb5d12931df6","examples/interactive_management.rs":"27d2578fca7672477584bb3a74db182295c85e4aa6ae2d8edfd849fc0018c413","examples/reset.rs":"b13d3a2ed3544018ede8660ec0cc79732139e792d4e55c2c6fb517ad376b36ad","examples/set_pin.rs":"991d9bd66fd6bdd9dd8627ed710fe100a3dfb65b968031f768ee9a28e1e995d7","examples/test_exclude_list.rs":"20577d6887b00c99d2ae404e1b1f64c746ecc774bd2f9f0f8d1c5bb6a6f30292","rustfmt.toml":"ceb6615363d6fff16426eb56f5727f98a7f7ed459ba9af735b1d8b672e2c3b9b","src/authenticatorservice.rs":"dc756ae9d420dac187b04afbb4831527c12fa307ef072f1c1cb4480df9cbda5f","src/consts.rs":"44fb7c396dc87d1657d1feed08e956fc70608c0b06a034716b626419b442bcfe","src/crypto/dummy.rs":"9cc6be0dc1e28c7328121e7a4bf435211ae8b1455784472b24993571c4009579","src/crypto/mod.rs":"e4342dd93fd41bf48fa26386188ed92db5f908ad4d69f32f080a65228c6d5390","src/crypto/nss.rs":"2bf33898728760f194f204876450d0906b47907d259270f6e3d43c62a709c99a","src/crypto/openssl.rs":"ef6e4dbcc7230137e505e3fc4ad37e102e6b26b37470afd0f4709a297b3aa546","src/ctap2/attestation.rs":"e3c581154fb6bd4e4d8bd2326515864849b21766f5344e2d955d607b360fc930","src/ctap2/client_data.rs":"04ee84b34e91c988183871b4975fc08e12234965187c793ad26d0d82ed44642f","src/ctap2/commands/client_pin.rs":"7f3a49b23592e985b8f32d43688593ff7411a05cb594444e24851c13f093cdef","src/ctap2/commands/get_assertion.rs":"e9cd68cff2ee54156af6e3e424691a06354aafffcc374a40ccc9622f030c4999","src/ctap2/commands/get_info.rs":"79117c39d280445fb17be057af2f45ec1d80651ea1c8b478e07118ade808291b","src/ctap2/commands/get_next_assertion.rs":"8a8fa69cb4079a21ff4734067e74784b2bfee3c20ddcc0b35675ce77a3d83ae9","src/ctap2/commands/get_version.rs":"958c273c6156af102bba515de42e4a5ae43f36b4d2d1814d922c269c500f6ce2","src/ctap2/commands/make_credentials.rs":"524cb3378fcc2b08696ab25bf5473e149af307d18ef503a4ee971b4b7e087ff3","src/ctap2/commands/mod.rs":"916eb63b3e46968a9e79d088dd217c2b80dc1c4d14beaf12803e91b7987b6c32","src/ctap2/commands/reset.rs":"45500500c900124f96269679862ceeb18e87111096d322c87c766f2694e576fc","src/ctap2/commands/selection.rs":"7832d62bf37ddbbaf996d84f905c2cdca7dceb529c8f9f1fe82eb288da886068","src/ctap2/mod.rs":"5953ee33ee5930437f9d91299f8a6fdbc21bc62297ae4194901893ef0a5ac82a","src/ctap2/preflight.rs":"1cd41e948955a8bcb22a2e55e254dad1be74590b6016437914e93a2639222aef","src/ctap2/server.rs":"61e2afa1bc3ce1d61743073f14c1a385d064e5deed2b8a194e32e0ccbd4243ad","src/ctap2/utils.rs":"ad0aa36a0dbeb510b7f37789329f1957eab206eb529dc083e6176b142984e26e","src/errors.rs":"a99e5fbdad315ba1589b116fc227310996ef900498b595545228be35744b2038","src/lib.rs":"d42fc78ab81b6fdd66ebe35951a4395a3656f557795cff4c8bfcc54199cabfcd","src/manager.rs":"d72f8523d0a549487504ef6d370aee9132ad7436aaae777e6d65a0a03f3c0c27","src/statecallback.rs":"6b16f97176db1ae3fc3851fe8394e4ffc324bc6fe59313845ac3a88132fd52f1","src/statemachine.rs":"3b1b08efda156bc8c00bad27096a95177217ad77cb041530a03b8903ba51d7e0","src/status_update.rs":"d032524f2c36c5a32db9dd424decf4577cea65adceca91bb1dfcdc07c58289cb","src/transport/device_selector.rs":"c703aa8e59b0b7ac9d11be0aac434dffda8b0c91e1a84298c48e598978e1576e","src/transport/errors.rs":"5af7cb8d22ffa63bf4264d182a0f54b9b3a2cc9d19d832b3495857229f9a2875","src/transport/freebsd/device.rs":"f41c7cf29c48bf2b403cf460e6387864372a134d6daeefc5c3afc3f40d0d4575","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":"033e0f1bf6428a1d4077e5abb53dbfa193ef72dd8a98b7666d7b5fb45a6570f0","src/transport/hidproto.rs":"9d490f161807b75f4d7d5096355006627c1f47c0d90fca53bade3692efc92a2d","src/transport/linux/device.rs":"e79bd06d98723a0d7e4f25b7cf2ac3e0260b10e52d2b0695909d2932288e10a4","src/transport/linux/hidraw.rs":"c7a0df9b4e51cb2736218ffffa02b2b2547b7c515d69f9bae2c9a8c8f1cb547b","src/transport/linux/hidwrapper.h":"72785db3a9b27ea72b6cf13a958fee032af54304522d002f56322473978a20f9","src/transport/linux/hidwrapper.rs":"753c7459dbb73befdd186b6269ac33f7a4537b4c935928f50f2b2131756e787d","src/transport/linux/ioctl_aarch64le.rs":"2d8b265cd39a9f46816f83d5a5df0701c13eb842bc609325bad42ce50add3bf0","src/transport/linux/ioctl_armle.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":"f508d0585079ecf87a73d6135c52e8b5a887fbf16e241676d51a8099a8001a81","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":"582b2b55f13d95dd9f1127e3dde49d2137a5ca020f9c1fa1ffa5c4083d05c0e7","src/transport/mock/mod.rs":"9c4c87efd19adddc1a91c699a6c328063cfbac5531b76346a5ff92e986aded8f","src/transport/mock/transaction.rs":"be3ed8c389dfa04122364b82515edd76fad6f5d5f72d15cacd45a84fb8397292","src/transport/mod.rs":"e28d72b6f3fdaff21f940c4db213067cd94f5832f864ecaad1c9901d5aea9b79","src/transport/netbsd/device.rs":"a7dec83b5040faf1a8ddb37e9fc2b45b9b12814be4802b3b351eff081d1b80c3","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":"47d8dfeb12c33e6cada2b2cd76476827059c797d8a16f2c4aea6e78d32ebab46","src/transport/openbsd/mod.rs":"514274d414042ff84b3667a41a736e78581e22fda87ccc97c2bc05617e381a30","src/transport/openbsd/monitor.rs":"2e0ba6ecc69b450be9cbfd21a7c65036ed2ce593b12363596d3eae0b5bfb79e8","src/transport/openbsd/transaction.rs":"ec28475a70dded260f9a7908c7f88dd3771f5d64b9a5dda835411d13b713c39a","src/transport/stub/device.rs":"aa21711d6690ed68bd878b28463172ba69c6324be7afabeccb1f07b4831cb020","src/transport/stub/mod.rs":"6a7fec504a52d403b0241b18cd8b95088a31807571f4c0a67e4055afc74f4453","src/transport/stub/transaction.rs":"c9a3ade9562468163f28fd51e7ff3e0bf5854b7edade9e987000d11c5d0e62d2","src/transport/windows/device.rs":"148b1572ed5fa8d476efbdb2a3a35608ec23012d6a805129f3c25c453bab4b7a","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/u2fprotocol.rs":"e61ac223aab79ae82383cd32a23213d18461e229c448373bf2483357a9eae69e","src/u2ftypes.rs":"8511c6f04f69670ddd403178a46060644a27128ca4077a9a3e00bc6671e3864b","src/util.rs":"cf37c4c3caf6dde4fc3cf6f5f297ed3c0f13bcb50fb0e8955899fc837483ef31","src/virtualdevices/mod.rs":"2c7df7691d5c150757304241351612aed4260d65b70ab0f483edbc1a5cfb5674","src/virtualdevices/software_u2f.rs":"83e63c0c4a597e71d87b5cd1f33a49646d00b3062edbdd05c51623b80fb60168","src/virtualdevices/webdriver/mod.rs":"4a36e6dfa9f45f941d863b4039bfbcfa8eaca660bd6ed78aeb1a2962db64be5a","src/virtualdevices/webdriver/testtoken.rs":"7146e02f1a5dad2c8827dd11c12ee408c0e42a0706ac65f139998feffd42570f","src/virtualdevices/webdriver/virtualmanager.rs":"7205a0397833628fc0847aa942a6a314dc1e23306858b546053e0de6a360ebe1","src/virtualdevices/webdriver/web_api.rs":"9032525af458b6fe9a3274c36b6ef8c791ecc4ec46d38ae36583fc9a4535b59d","testing/cross/powerpc64le-unknown-linux-gnu.Dockerfile":"d7463ff4376e3e0ca3fed879fab4aa975c4c0a3e7924c5b88aef9381a5d013de","testing/cross/x86_64-unknown-linux-gnu.Dockerfile":"11c79c04b07a171b0c9b63ef75fa75f33263ce76e3c1eda0879a3e723ebd0c24","testing/run_cross.sh":"cc2a7e0359f210eba2e7121f81eb8ab0125cea6e0d0f2698177b0fe2ad0c33d8","webdriver-tools/requirements.txt":"8236aa3dedad886f213c9b778fec80b037212d30e640b458984110211d546005","webdriver-tools/webdriver-driver.py":"82327c26ba271d1689acc87b612ab8436cb5475f0a3c0dba7baa06e7f6f5e19c"},"package":"aa0e182b77b6b19eaf9c7b69fddf3be970169ec6d34eca3f5d682ab948727e57"} \ 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..23f5c3159b
--- /dev/null
+++ b/third_party/rust/authenticator/Cargo.lock
@@ -0,0 +1,1586 @@
+# 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.15"
+dependencies = [
+ "assert_matches",
+ "base64",
+ "bindgen 0.58.1",
+ "bitflags",
+ "bytes 0.5.6",
+ "cfg-if",
+ "core-foundation",
+ "devd-rs",
+ "env_logger 0.6.2",
+ "getopts",
+ "libc",
+ "libudev",
+ "log",
+ "memoffset",
+ "nom 7.1.1",
+ "nss-gk-api",
+ "openssl",
+ "openssl-sys",
+ "pkcs11-bindings",
+ "rand",
+ "rpassword",
+ "runloop",
+ "serde",
+ "serde_bytes",
+ "serde_cbor",
+ "serde_json",
+ "sha2",
+ "tokio",
+ "warp",
+ "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.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[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 = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bytes"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+
+[[package]]
+name = "cc"
+version = "1.0.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f"
+
+[[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 = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[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 = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
+
+[[package]]
+name = "futures-task"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
+
+[[package]]
+name = "futures-util"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[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 = "h2"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
+dependencies = [
+ "bytes 1.2.1",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "headers"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
+dependencies = [
+ "base64",
+ "bitflags",
+ "bytes 1.2.1",
+ "headers-core",
+ "http",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+dependencies = [
+ "http",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes 1.2.1",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes 1.2.1",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[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 = "hyper"
+version = "0.14.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
+dependencies = [
+ "bytes 1.2.1",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[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.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55edcf6c0bb319052dea84732cf99db461780fd5e8d3eb46ab6ff312ab31f197"
+
+[[package]]
+name = "libloading"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
+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 = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "mio"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "mozbuild"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "903970ae2f248d7275214cf8f387f8ba0c4ea7e3d87a320e85493db60ce28616"
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand",
+ "safemem",
+ "tempfile",
+ "twoway",
+]
+
+[[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.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a689b62ba71fda80458a77b6ace9371d6e6a5473300901383ebd101659b3352"
+dependencies = [
+ "bindgen 0.61.0",
+ "mozbuild",
+ "once_cell",
+ "pkcs11-bindings",
+ "serde",
+ "serde_derive",
+ "toml",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[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 = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "pin-project"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[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.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+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.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+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 = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[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 = "rustls-pemfile"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
+
+[[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 = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[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 = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[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 = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[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 = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
+dependencies = [
+ "autocfg",
+ "bytes 1.2.1",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
+dependencies = [
+ "bytes 1.2.1",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "tungstenite"
+version = "0.17.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
+dependencies = [
+ "base64",
+ "byteorder",
+ "bytes 1.2.1",
+ "http",
+ "httparse",
+ "log",
+ "rand",
+ "sha-1",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "twoway"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[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 = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "warp"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed7b8be92646fc3d18b06147664ebc5f48d222686cb11a8755e561a735aacc6d"
+dependencies = [
+ "bytes 1.2.1",
+ "futures-channel",
+ "futures-util",
+ "headers",
+ "http",
+ "hyper",
+ "log",
+ "mime",
+ "mime_guess",
+ "multipart",
+ "percent-encoding",
+ "pin-project",
+ "rustls-pemfile",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-stream",
+ "tokio-tungstenite",
+ "tokio-util",
+ "tower-service",
+ "tracing",
+]
+
+[[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"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
diff --git a/third_party/rust/authenticator/Cargo.toml b/third_party/rust/authenticator/Cargo.toml
new file mode 100644
index 0000000000..25d2418807
--- /dev/null
+++ b/third_party/rust/authenticator/Cargo.toml
@@ -0,0 +1,174 @@
+# 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.15"
+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.13"
+
+[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.nom]
+version = "^7.1.1"
+features = ["std"]
+default-features = false
+
+[dependencies.nss-gk-api]
+version = "0.2.1"
+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"
+
+[dependencies.tokio]
+version = "1.17"
+features = [
+ "macros",
+ "rt-multi-thread",
+]
+optional = true
+
+[dependencies.warp]
+version = "0.3.2"
+optional = true
+
+[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"]
+webdriver = [
+ "bytes",
+ "warp",
+ "tokio",
+]
+
+[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..58f6cfa393
--- /dev/null
+++ b/third_party/rust/authenticator/build.rs
@@ -0,0 +1,62 @@
+#[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 {
+ 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..20154e9b1e
--- /dev/null
+++ b/third_party/rust/authenticator/examples/ctap2.rs
@@ -0,0 +1,294 @@
+/* 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, GetAssertionExtensions, HmacSecretExtension,
+ MakeCredentialsExtensions, RegisterArgs, SignArgs,
+ },
+ ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ ResidentKeyRequirement, Transport, User, UserVerificationRequirement,
+ },
+ statecallback::StateCallback,
+ COSEAlgorithm, Pin, RegisterResult, SignResult, 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("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
+ opts.optflag("h", "help", "print this help menu").optopt(
+ "t",
+ "timeout",
+ "timeout in seconds",
+ "SEC",
+ );
+ 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 mut manager =
+ AuthenticatorService::new().expect("The auth service should initialize safely");
+
+ if !matches.opt_present("no-u2f-usb-hid") {
+ 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 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: [u8; 32] = 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::DeviceAvailable { dev_info }) => {
+ println!("STATUS: device available: {dev_info}")
+ }
+ Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
+ println!("STATUS: device unavailable: {dev_info}")
+ }
+ Ok(StatusUpdate::Success { dev_info }) => {
+ println!("STATUS: success using device: {dev_info}");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::DeviceSelected(dev_info)) => {
+ println!("STATUS: Continuing with device: {dev_info}");
+ }
+ 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)
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let user = User {
+ id: "user_id".as_bytes().to_vec(),
+ icon: None,
+ name: Some("A. User".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,
+ icon: None,
+ },
+ origin: origin.clone(),
+ 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: MakeCredentialsExtensions {
+ hmac_secret: if matches.opt_present("hmac_secret") {
+ Some(true)
+ } else {
+ None
+ },
+ ..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(RegisterResult::CTAP1(_, _)) => panic!("Requested CTAP2, but got CTAP1 results!"),
+ Ok(RegisterResult::CTAP2(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.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,
+ relying_party_id: "example.com".to_string(),
+ allow_list,
+ user_verification_req: UserVerificationRequirement::Preferred,
+ user_presence_req: true,
+ extensions: GetAssertionExtensions {
+ hmac_secret: if matches.opt_present("hmac_secret") {
+ Some(HmacSecretExtension::new(
+ vec![
+ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0x10, 0x11, 0x12, 0x13,
+ 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25,
+ 0x26, 0x27, 0x28, 0x29, 0x30, 0x31, 0x32, 0x33, 0x34,
+ ],
+ None,
+ ))
+ } else {
+ None
+ },
+ },
+ pin: None,
+ alternate_rp_id: 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(SignResult::CTAP1(..)) => panic!("Requested CTAP2, but got CTAP1 sign results!"),
+ Ok(SignResult::CTAP2(assertion_object)) => {
+ println!("Assertion Object: {assertion_object:?}");
+ 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..ea5aa5bf3c
--- /dev/null
+++ b/third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs
@@ -0,0 +1,366 @@
+/* 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},
+ ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ ResidentKeyRequirement, Transport, User, UserVerificationRequirement,
+ },
+ statecallback::StateCallback,
+ COSEAlgorithm, Pin, RegisterResult, SignResult, 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) {
+ 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 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::DeviceAvailable { dev_info }) => {
+ println!("STATUS: device available: {dev_info}")
+ }
+ Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
+ println!("STATUS: device unavailable: {dev_info}")
+ }
+ Ok(StatusUpdate::Success { dev_info }) => {
+ println!("STATUS: success using device: {dev_info}");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::DeviceSelected(dev_info)) => {
+ println!("STATUS: Continuing with device: {dev_info}");
+ }
+ 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)
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let user = User {
+ id: username.as_bytes().to_vec(),
+ icon: None,
+ 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,
+ icon: 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: 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(RegisterResult::CTAP1(_, _)) => panic!("Requested CTAP2, but got CTAP1 results!"),
+ Ok(RegisterResult::CTAP2(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("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
+ 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");
+
+ if !matches.opt_present("no-u2f-usb-hid") {
+ 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;
+ }
+ };
+
+ 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::DeviceAvailable { dev_info }) => {
+ println!("STATUS: device available: {dev_info}")
+ }
+ Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
+ println!("STATUS: device unavailable: {dev_info}")
+ }
+ Ok(StatusUpdate::Success { dev_info }) => {
+ println!("STATUS: success using device: {dev_info}");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::DeviceSelected(dev_info)) => {
+ println!("STATUS: Continuing with device: {dev_info}");
+ }
+ 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)
+ }
+ 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,
+ alternate_rp_id: 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(SignResult::CTAP1(..)) => panic!("Requested CTAP2, but got CTAP1 sign results!"),
+ Ok(SignResult::CTAP2(assertion_object)) => {
+ println!("Assertion Object: {assertion_object:?}");
+ println!("-----------------------------------------------------------------");
+ println!("Found credentials:");
+ println!(
+ "{:?}",
+ assertion_object
+ .0
+ .iter()
+ .map(|x| x.user.clone().unwrap().name.unwrap()) // Unwrapping here, as these shouldn't fail
+ .collect::<Vec<_>>()
+ );
+ 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..714279fbbf
--- /dev/null
+++ b/third_party/rust/authenticator/examples/interactive_management.rs
@@ -0,0 +1,204 @@
+/* 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, errors::AuthenticatorError,
+ statecallback::StateCallback, InteractiveRequest, Pin, ResetResult, StatusUpdate,
+};
+use getopts::Options;
+use log::debug;
+use std::{env, io, thread};
+use std::{
+ io::Write,
+ sync::mpsc::{channel, Receiver, RecvError},
+};
+
+fn print_usage(program: &str, opts: Options) {
+ let brief = format!("Usage: {program} [options]");
+ print!("{}", opts.usage(&brief));
+}
+
+fn interactive_status_callback(status_rx: Receiver<StatusUpdate>) {
+ let mut num_of_devices = 0;
+ loop {
+ match status_rx.recv() {
+ Ok(StatusUpdate::InteractiveManagement((tx, dev_info, auth_info))) => {
+ debug!(
+ "STATUS: interactive management: {:#}, {:#?}",
+ dev_info, auth_info
+ );
+ println!("Device info {:#}", dev_info);
+ let mut change_pin = false;
+ if let Some(info) = auth_info {
+ println!("Authenticator Info {:#?}", info);
+ println!();
+ println!("What do you wish to do?");
+
+ let mut choices = vec!["0", "1"];
+ println!("(0) Quit");
+ println!("(1) Reset token");
+ match info.options.client_pin {
+ None => {}
+ Some(true) => {
+ println!("(2) Change PIN");
+ choices.push("2");
+ change_pin = true;
+ }
+ Some(false) => {
+ println!("(2) Set PIN");
+ choices.push("2");
+ }
+ }
+
+ let mut input = String::new();
+ while !choices.contains(&input.trim()) {
+ 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");
+ }
+ input = input.trim().to_string();
+
+ match input.as_str() {
+ "0" => {
+ return;
+ }
+ "1" => {
+ tx.send(InteractiveRequest::Reset)
+ .expect("Failed to send Reset request.");
+ }
+ "2" => {
+ 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 change_pin {
+ 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.send(InteractiveRequest::ChangePIN(curr_pin, new_pin))
+ .expect("Failed to send PIN-change request");
+ } else {
+ tx.send(InteractiveRequest::SetPIN(new_pin))
+ .expect("Failed to send PIN-set request");
+ }
+ return;
+ }
+ _ => {
+ panic!("Can't happen");
+ }
+ }
+ } else {
+ println!("Device only supports CTAP1 and can't be managed.");
+ }
+ }
+ Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
+ num_of_devices += 1;
+ debug!(
+ "STATUS: New device #{} available: {}",
+ num_of_devices, dev_info
+ );
+ }
+ Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
+ num_of_devices -= 1;
+ if num_of_devices <= 0 {
+ println!("No more devices left. Please plug in a device!");
+ }
+ debug!("STATUS: Device became unavailable: {}", dev_info)
+ }
+ Ok(StatusUpdate::Success { dev_info }) => {
+ println!("STATUS: success using device: {}", dev_info);
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::DeviceSelected(_dev_info)) => {}
+ Ok(StatusUpdate::PresenceRequired) => {
+ println!("STATUS: waiting for user presence");
+ }
+ Ok(StatusUpdate::PinUvError(..)) => {
+ println!("STATUS: Pin Error!");
+ }
+ 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("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
+ 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");
+
+ if !matches.opt_present("no-u2f-usb-hid") {
+ 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<ResetResult, 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)
+ }
+ }
+ let manage_result = manage_rx
+ .recv()
+ .expect("Problem receiving, unable to continue");
+ match manage_result {
+ Ok(_) => println!("Success!"),
+ 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..1194cb2b06
--- /dev/null
+++ b/third_party/rust/authenticator/examples/reset.rs
@@ -0,0 +1,140 @@
+/* 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("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
+ 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");
+
+ if !matches.opt_present("no-u2f-usb-hid") {
+ 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::DeviceSelected(dev_info)) => {
+ println!("STATUS: Continuing with device: {dev_info}");
+ }
+ 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..18304648b8
--- /dev/null
+++ b/third_party/rust/authenticator/examples/set_pin.rs
@@ -0,0 +1,160 @@
+/* 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("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
+ 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");
+
+ if !matches.opt_present("no-u2f-usb-hid") {
+ 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::DeviceAvailable { dev_info }) => {
+ println!("STATUS: device available: {dev_info}")
+ }
+ Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
+ println!("STATUS: device unavailable: {dev_info}")
+ }
+ Ok(StatusUpdate::Success { dev_info }) => {
+ println!("STATUS: success using device: {dev_info}");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::DeviceSelected(dev_info)) => {
+ println!("STATUS: Continuing with device: {dev_info}");
+ }
+ 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)
+ }
+ 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..ad2b33feba
--- /dev/null
+++ b/third_party/rust/authenticator/examples/test_exclude_list.rs
@@ -0,0 +1,315 @@
+/* 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, GetAssertionExtensions, RegisterArgs, SignArgs},
+ ctap2::commands::StatusCode,
+ ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ ResidentKeyRequirement, Transport, User, UserVerificationRequirement,
+ },
+ errors::{AuthenticatorError, CommandError, HIDError, UnsupportedOption},
+ statecallback::StateCallback,
+ COSEAlgorithm, Pin, RegisterResult, SignResult, 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::DeviceAvailable { dev_info }) => {
+ println!("STATUS: device available: {dev_info}")
+ }
+ Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
+ println!("STATUS: device unavailable: {dev_info}")
+ }
+ Ok(StatusUpdate::Success { dev_info }) => {
+ println!("STATUS: success using device: {dev_info}");
+ }
+ Ok(StatusUpdate::SelectDeviceNotice) => {
+ println!("STATUS: Please select a device by touching one of them.");
+ }
+ Ok(StatusUpdate::DeviceSelected(dev_info)) => {
+ println!("STATUS: Continuing with device: {dev_info}");
+ }
+ 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)
+ }
+ Err(RecvError) => {
+ println!("STATUS: end");
+ return;
+ }
+ }
+ });
+
+ let user = User {
+ id: "user_id".as_bytes().to_vec(),
+ icon: None,
+ 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,
+ icon: 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(RegisterResult::CTAP1(_, _)) => panic!("Requested CTAP2, but got CTAP1 results!"),
+ Ok(RegisterResult::CTAP2(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.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: GetAssertionExtensions::default(),
+ pin: None,
+ alternate_rp_id: 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(SignResult::CTAP1(..)) => panic!("Requested CTAP2, but got CTAP1 results!"),
+ Ok(SignResult::CTAP2(..)) => {
+ 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/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..4bfce3bf4c
--- /dev/null
+++ b/third_party/rust/authenticator/src/authenticatorservice.rs
@@ -0,0 +1,687 @@
+/* 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;
+pub use crate::ctap2::commands::get_assertion::{GetAssertionExtensions, HmacSecretExtension};
+pub use crate::ctap2::commands::make_credentials::MakeCredentialsExtensions;
+use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ ResidentKeyRequirement, User, 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: User,
+ pub pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ pub exclude_list: Vec<PublicKeyCredentialDescriptor>,
+ pub user_verification_req: UserVerificationRequirement,
+ pub resident_key_req: ResidentKeyRequirement,
+ pub extensions: MakeCredentialsExtensions,
+ 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: GetAssertionExtensions,
+ pub pin: Option<Pin>,
+ pub alternate_rp_id: Option<String>,
+ pub use_ctap1_fallback: bool,
+ // Todo: Extensions
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct AssertionExtensions {
+ pub hmac_secret: Option<HmacSecretExtension>,
+}
+
+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::ResetResult>>,
+ ) -> 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();
+ }
+
+ 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),
+ }
+ }
+
+ #[cfg(feature = "webdriver")]
+ pub fn add_webdriver_virtual_bus(&mut self) {
+ match crate::virtualdevices::webdriver::VirtualManager::new() {
+ Ok(token) => {
+ println!("WebDriver ready, listening at {}", &token.url());
+ self.add_transport(Box::new(token));
+ }
+ Err(e) => error!("Could not add WebDriver virtual bus: {}", 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::ResetResult>>,
+ ) -> 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::{Capability, PARAMETER_SIZE};
+ use crate::ctap2::server::{
+ RelyingParty, ResidentKeyRequirement, User, UserVerificationRequirement,
+ };
+ use crate::statecallback::StateCallback;
+ use crate::{RegisterResult, SignResult, 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 TestTransportDriver {
+ fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo {
+ crate::u2ftypes::U2FDeviceInfo {
+ vendor_name: String::from("Mozilla").into_bytes(),
+ device_name: String::from("Test Transport Token").into_bytes(),
+ version_interface: 0,
+ version_major: 1,
+ version_minor: 2,
+ version_build: 3,
+ cap_flags: Capability::empty(),
+ }
+ }
+ }
+
+ 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 {
+ let rv = Ok(RegisterResult::CTAP1(vec![0u8; 16], self.dev_info()));
+ 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 {
+ let rv = Ok(SignResult::CTAP1(
+ vec![0u8; 0],
+ vec![0u8; 0],
+ vec![0u8; 0],
+ self.dev_info(),
+ ));
+ 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::ResetResult>>,
+ ) -> 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,
+ icon: None,
+ },
+ origin: "example.com".to_string(),
+ user: User {
+ id: "user_id".as_bytes().to_vec(),
+ icon: None,
+ 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,
+ }
+ .into(),
+ 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,
+ alternate_rp_id: None,
+ use_ctap1_fallback: false,
+ }
+ .into(),
+ 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,
+ icon: None,
+ },
+ origin: "example.com".to_string(),
+ user: User {
+ id: "user_id".as_bytes().to_vec(),
+ icon: None,
+ 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,
+ }
+ .into(),
+ 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,
+ alternate_rp_id: None,
+ use_ctap1_fallback: false,
+ }
+ .into(),
+ 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,
+ icon: None,
+ },
+ origin: "example.com".to_string(),
+ user: User {
+ id: "user_id".as_bytes().to_vec(),
+ icon: None,
+ 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,
+ }
+ .into(),
+ 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/dummy.rs b/third_party/rust/authenticator/src/crypto/dummy.rs
new file mode 100644
index 0000000000..c06f92162f
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/dummy.rs
@@ -0,0 +1,39 @@
+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!()
+}
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..6d76664896
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/mod.rs
@@ -0,0 +1,1343 @@
+/* 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 serde_cbor::Value;
+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, hmac_sha256,
+ random_bytes, sha256,
+};
+
+// Object identifiers in DER tag-length-value form
+const DER_OID_EC_PUBLIC_KEY_BYTES: &[u8] = &[
+ 0x06, 0x07,
+ /* {iso(1) member-body(2) us(840) ansi-x962(10045) keyType(2) ecPublicKey(1)} */
+ 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
+];
+const DER_OID_P256_BYTES: &[u8] = &[
+ 0x06, 0x08,
+ /* {iso(1) member-body(2) us(840) ansi-x962(10045) curves(3) prime(1) prime256v1(7)} */
+ 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
+];
+
+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."
+ for proto_id in info.pin_protocols.iter() {
+ match proto_id {
+ 1 => return Ok(PinUvAuthProtocol(Box::new(PinUvAuth1 {}))),
+ 2 => return Ok(PinUvAuthProtocol(Box::new(PinUvAuth2 {}))),
+ _ => continue,
+ }
+ }
+ 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>,
+ #[allow(dead_code)] // Not yet used
+ 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, Serialize, Deserialize)]
+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 TryFrom<u64> for Curve {
+ type Error = CryptoError;
+ fn try_from(i: u64) -> Result<Self, Self::Error> {
+ match i {
+ 1 => Ok(Curve::SECP256R1),
+ 2 => Ok(Curve::SECP384R1),
+ 3 => Ok(Curve::SECP521R1),
+ 4 => Ok(Curve::X25519),
+ 5 => Ok(Curve::X448),
+ 6 => Ok(Curve::Ed25519),
+ 7 => 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,
+ {
+ match *self {
+ COSEAlgorithm::RS512 => serializer.serialize_i16(-259),
+ COSEAlgorithm::RS384 => serializer.serialize_i16(-258),
+ COSEAlgorithm::RS256 => serializer.serialize_i16(-257),
+ COSEAlgorithm::ES256K => serializer.serialize_i8(-47),
+ COSEAlgorithm::HSS_LMS => serializer.serialize_i8(-46),
+ COSEAlgorithm::SHAKE256 => serializer.serialize_i8(-45),
+ COSEAlgorithm::SHA512 => serializer.serialize_i8(-44),
+ COSEAlgorithm::SHA384 => serializer.serialize_i8(-43),
+ COSEAlgorithm::RSAES_OAEP_SHA_512 => serializer.serialize_i8(-42),
+ COSEAlgorithm::RSAES_OAEP_SHA_256 => serializer.serialize_i8(-41),
+ COSEAlgorithm::RSAES_OAEP_RFC_8017_default => serializer.serialize_i8(-40),
+ COSEAlgorithm::PS512 => serializer.serialize_i8(-39),
+ COSEAlgorithm::PS384 => serializer.serialize_i8(-38),
+ COSEAlgorithm::PS256 => serializer.serialize_i8(-37),
+ COSEAlgorithm::ES512 => serializer.serialize_i8(-36),
+ COSEAlgorithm::ES384 => serializer.serialize_i8(-35),
+ COSEAlgorithm::ECDH_SS_A256KW => serializer.serialize_i8(-34),
+ COSEAlgorithm::ECDH_SS_A192KW => serializer.serialize_i8(-33),
+ COSEAlgorithm::ECDH_SS_A128KW => serializer.serialize_i8(-32),
+ COSEAlgorithm::ECDH_ES_A256KW => serializer.serialize_i8(-31),
+ COSEAlgorithm::ECDH_ES_A192KW => serializer.serialize_i8(-30),
+ COSEAlgorithm::ECDH_ES_A128KW => serializer.serialize_i8(-29),
+ COSEAlgorithm::ECDH_SS_HKDF512 => serializer.serialize_i8(-28),
+ COSEAlgorithm::ECDH_SS_HKDF256 => serializer.serialize_i8(-27),
+ COSEAlgorithm::ECDH_ES_HKDF512 => serializer.serialize_i8(-26),
+ COSEAlgorithm::ECDH_ES_HKDF256 => serializer.serialize_i8(-25),
+ COSEAlgorithm::SHAKE128 => serializer.serialize_i8(-18),
+ COSEAlgorithm::SHA512_256 => serializer.serialize_i8(-17),
+ COSEAlgorithm::SHA256 => serializer.serialize_i8(-16),
+ COSEAlgorithm::SHA256_64 => serializer.serialize_i8(-15),
+ COSEAlgorithm::SHA1 => serializer.serialize_i8(-14),
+ COSEAlgorithm::Direct_HKDF_AES256 => serializer.serialize_i8(-13),
+ COSEAlgorithm::Direct_HKDF_AES128 => serializer.serialize_i8(-12),
+ COSEAlgorithm::Direct_HKDF_SHA512 => serializer.serialize_i8(-11),
+ COSEAlgorithm::Direct_HKDF_SHA256 => serializer.serialize_i8(-10),
+ COSEAlgorithm::EDDSA => serializer.serialize_i8(-8),
+ COSEAlgorithm::ES256 => serializer.serialize_i8(-7),
+ COSEAlgorithm::Direct => serializer.serialize_i8(-6),
+ COSEAlgorithm::A256KW => serializer.serialize_i8(-5),
+ COSEAlgorithm::A192KW => serializer.serialize_i8(-4),
+ COSEAlgorithm::A128KW => serializer.serialize_i8(-3),
+ COSEAlgorithm::A128GCM => serializer.serialize_i8(1),
+ COSEAlgorithm::A192GCM => serializer.serialize_i8(2),
+ COSEAlgorithm::A256GCM => serializer.serialize_i8(3),
+ COSEAlgorithm::HMAC256_64 => serializer.serialize_i8(4),
+ COSEAlgorithm::HMAC256_256 => serializer.serialize_i8(5),
+ COSEAlgorithm::HMAC384_384 => serializer.serialize_i8(6),
+ COSEAlgorithm::HMAC512_512 => serializer.serialize_i8(7),
+ COSEAlgorithm::AES_CCM_16_64_128 => serializer.serialize_i8(10),
+ COSEAlgorithm::AES_CCM_16_64_256 => serializer.serialize_i8(11),
+ COSEAlgorithm::AES_CCM_64_64_128 => serializer.serialize_i8(12),
+ COSEAlgorithm::AES_CCM_64_64_256 => serializer.serialize_i8(13),
+ COSEAlgorithm::AES_MAC_128_64 => serializer.serialize_i8(14),
+ COSEAlgorithm::AES_MAC_256_64 => serializer.serialize_i8(15),
+ COSEAlgorithm::ChaCha20_Poly1305 => serializer.serialize_i8(24),
+ COSEAlgorithm::AES_MAC_128_128 => serializer.serialize_i8(25),
+ COSEAlgorithm::AES_MAC_256_128 => serializer.serialize_i8(26),
+ COSEAlgorithm::AES_CCM_16_128_128 => serializer.serialize_i8(30),
+ COSEAlgorithm::AES_CCM_16_128_256 => serializer.serialize_i8(31),
+ COSEAlgorithm::AES_CCM_64_128_128 => serializer.serialize_i8(32),
+ COSEAlgorithm::AES_CCM_64_128_256 => serializer.serialize_i8(33),
+ COSEAlgorithm::IV_GENERATION => serializer.serialize_i8(34),
+ COSEAlgorithm::INSECURE_RS1 => serializer.serialize_i32(-65535),
+ }
+ }
+}
+
+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 {
+ -259 => Ok(COSEAlgorithm::RS512),
+ -258 => Ok(COSEAlgorithm::RS384),
+ -257 => Ok(COSEAlgorithm::RS256),
+ -47 => Ok(COSEAlgorithm::ES256K),
+ -46 => Ok(COSEAlgorithm::HSS_LMS),
+ -45 => Ok(COSEAlgorithm::SHAKE256),
+ -44 => Ok(COSEAlgorithm::SHA512),
+ -43 => Ok(COSEAlgorithm::SHA384),
+ -42 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_512),
+ -41 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_256),
+ -40 => Ok(COSEAlgorithm::RSAES_OAEP_RFC_8017_default),
+ -39 => Ok(COSEAlgorithm::PS512),
+ -38 => Ok(COSEAlgorithm::PS384),
+ -37 => Ok(COSEAlgorithm::PS256),
+ -36 => Ok(COSEAlgorithm::ES512),
+ -35 => Ok(COSEAlgorithm::ES384),
+ -34 => Ok(COSEAlgorithm::ECDH_SS_A256KW),
+ -33 => Ok(COSEAlgorithm::ECDH_SS_A192KW),
+ -32 => Ok(COSEAlgorithm::ECDH_SS_A128KW),
+ -31 => Ok(COSEAlgorithm::ECDH_ES_A256KW),
+ -30 => Ok(COSEAlgorithm::ECDH_ES_A192KW),
+ -29 => Ok(COSEAlgorithm::ECDH_ES_A128KW),
+ -28 => Ok(COSEAlgorithm::ECDH_SS_HKDF512),
+ -27 => Ok(COSEAlgorithm::ECDH_SS_HKDF256),
+ -26 => Ok(COSEAlgorithm::ECDH_ES_HKDF512),
+ -25 => Ok(COSEAlgorithm::ECDH_ES_HKDF256),
+ -18 => Ok(COSEAlgorithm::SHAKE128),
+ -17 => Ok(COSEAlgorithm::SHA512_256),
+ -16 => Ok(COSEAlgorithm::SHA256),
+ -15 => Ok(COSEAlgorithm::SHA256_64),
+ -14 => Ok(COSEAlgorithm::SHA1),
+ -13 => Ok(COSEAlgorithm::Direct_HKDF_AES256),
+ -12 => Ok(COSEAlgorithm::Direct_HKDF_AES128),
+ -11 => Ok(COSEAlgorithm::Direct_HKDF_SHA512),
+ -10 => Ok(COSEAlgorithm::Direct_HKDF_SHA256),
+ -8 => Ok(COSEAlgorithm::EDDSA),
+ -7 => Ok(COSEAlgorithm::ES256),
+ -6 => Ok(COSEAlgorithm::Direct),
+ -5 => Ok(COSEAlgorithm::A256KW),
+ -4 => Ok(COSEAlgorithm::A192KW),
+ -3 => Ok(COSEAlgorithm::A128KW),
+ 1 => Ok(COSEAlgorithm::A128GCM),
+ 2 => Ok(COSEAlgorithm::A192GCM),
+ 3 => Ok(COSEAlgorithm::A256GCM),
+ 4 => Ok(COSEAlgorithm::HMAC256_64),
+ 5 => Ok(COSEAlgorithm::HMAC256_256),
+ 6 => Ok(COSEAlgorithm::HMAC384_384),
+ 7 => Ok(COSEAlgorithm::HMAC512_512),
+ 10 => Ok(COSEAlgorithm::AES_CCM_16_64_128),
+ 11 => Ok(COSEAlgorithm::AES_CCM_16_64_256),
+ 12 => Ok(COSEAlgorithm::AES_CCM_64_64_128),
+ 13 => Ok(COSEAlgorithm::AES_CCM_64_64_256),
+ 14 => Ok(COSEAlgorithm::AES_MAC_128_64),
+ 15 => Ok(COSEAlgorithm::AES_MAC_256_64),
+ 24 => Ok(COSEAlgorithm::ChaCha20_Poly1305),
+ 25 => Ok(COSEAlgorithm::AES_MAC_128_128),
+ 26 => Ok(COSEAlgorithm::AES_MAC_256_128),
+ 30 => Ok(COSEAlgorithm::AES_CCM_16_128_128),
+ 31 => Ok(COSEAlgorithm::AES_CCM_16_128_256),
+ 32 => Ok(COSEAlgorithm::AES_CCM_64_128_128),
+ 33 => Ok(COSEAlgorithm::AES_CCM_64_128_256),
+ 34 => Ok(COSEAlgorithm::IV_GENERATION),
+ -65535 => 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(),
+ })
+ }
+
+ fn der_spki(&self) -> Result<Vec<u8>, CryptoError> {
+ let (curve_oid, seq_len, alg_len, spk_len) = match self.curve {
+ Curve::SECP256R1 => (
+ DER_OID_P256_BYTES,
+ [0x59].as_slice(),
+ [0x13].as_slice(),
+ [0x42].as_slice(),
+ ),
+ x => return Err(CryptoError::UnsupportedCurve(x)),
+ };
+
+ // [RFC 5280]
+ let mut spki: Vec<u8> = vec![];
+ // SubjectPublicKeyInfo
+ spki.push(0x30);
+ spki.extend_from_slice(seq_len);
+ // AlgorithmIdentifier
+ spki.push(0x30);
+ spki.extend_from_slice(alg_len);
+ // ObjectIdentifier
+ spki.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES);
+ // RFC 5480 ECParameters
+ spki.extend_from_slice(curve_oid);
+ // BIT STRING encoding uncompressed SEC1 public point
+ spki.push(0x03);
+ spki.extend_from_slice(spk_len);
+ spki.push(0x0); // no trailing zeros
+ spki.push(0x04); // SEC 1 encoded uncompressed point
+ spki.extend_from_slice(&self.x);
+ spki.extend_from_slice(&self.y);
+
+ Ok(spki)
+ }
+}
+
+/// 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>,
+}
+
+/// A COSE RSA PublicKey. This is a provided credential from a registered
+/// authenticator.
+/// You will likely never need to interact with this value, as it is part of the Credential
+/// API.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSERSAKey {
+ /// An RSA modulus
+ pub n: Vec<u8>,
+ /// An RSA exponent
+ pub e: Vec<u8>,
+}
+
+/// 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 COSESymmetricKey {
+ /// The key
+ pub key: Vec<u8>,
+}
+
+// https://tools.ietf.org/html/rfc8152#section-13
+#[allow(non_camel_case_types)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[repr(i64)]
+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,
+ /// Symmetric
+ Symmetric = 4,
+}
+
+impl TryFrom<u64> for COSEKeyTypeId {
+ type Error = CryptoError;
+ fn try_from(i: u64) -> Result<Self, Self::Error> {
+ match i {
+ 1 => Ok(COSEKeyTypeId::OKP),
+ 2 => Ok(COSEKeyTypeId::EC2),
+ 3 => Ok(COSEKeyTypeId::RSA),
+ 4 => Ok(COSEKeyTypeId::Symmetric),
+ _ => 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 {
+ // +-----------+-------+-----------------------------------------------+
+ // | Name | Value | Description |
+ // +-----------+-------+-----------------------------------------------+
+ // | OKP | 1 | Octet Key Pair |
+ // | EC2 | 2 | Elliptic Curve Keys w/ x- and y-coordinate |
+ // | | | pair |
+ // | Symmetric | 4 | Symmetric Keys |
+ // | Reserved | 0 | This value is reserved |
+ // +-----------+-------+-----------------------------------------------+
+ // Reserved, // should always be invalid.
+ /// Identifies this as an Elliptic Curve octet key pair
+ OKP(COSEOKPKey), // Not used here
+ /// Identifies this as an Elliptic Curve EC2 key
+ EC2(COSEEC2Key),
+ /// Identifies this as an RSA key
+ RSA(COSERSAKey), // Not used here
+ /// Identifies this as a Symmetric key
+ Symmetric(COSESymmetricKey), // Not used here
+}
+
+/// 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<'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 curve: Option<Curve> = None;
+ let mut key_type: Option<COSEKeyTypeId> = None;
+ let mut alg: Option<COSEAlgorithm> = None;
+ let mut x: Option<Vec<u8>> = None;
+ let mut y: Option<Vec<u8>> = None;
+
+ while let Some(key) = map.next_key()? {
+ trace!("cose key {:?}", key);
+ match key {
+ 1 => {
+ if key_type.is_some() {
+ return Err(SerdeError::duplicate_field("key_type"));
+ }
+ let value: u64 = map.next_value()?;
+ let val = COSEKeyTypeId::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported key_type {value}"))
+ })?;
+ key_type = Some(val);
+ // key_type = Some(map.next_value()?);
+ }
+ -1 => {
+ let key_type =
+ key_type.ok_or_else(|| SerdeError::missing_field("key_type"))?;
+ if key_type == COSEKeyTypeId::RSA {
+ if y.is_some() {
+ return Err(SerdeError::duplicate_field("y"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ y = Some(value.to_vec());
+ } else {
+ if curve.is_some() {
+ return Err(SerdeError::duplicate_field("curve"));
+ }
+ let value: u64 = map.next_value()?;
+ let val = Curve::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported curve {value}"))
+ })?;
+ curve = Some(val);
+ // curve = Some(map.next_value()?);
+ }
+ }
+ -2 => {
+ if x.is_some() {
+ return Err(SerdeError::duplicate_field("x"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ x = Some(value.to_vec());
+ }
+ -3 => {
+ if y.is_some() {
+ return Err(SerdeError::duplicate_field("y"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ y = Some(value.to_vec());
+ }
+ 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);
+ // alg = map.next_value()?;
+ }
+ _ => {
+ // This unknown field should raise an error, but
+ // there is a couple of field I(baloo) do not understand
+ // yet. I(baloo) chose to ignore silently the
+ // error instead because of that
+ let value: Value = map.next_value()?;
+ trace!("cose unknown value {:?}:{:?}", key, value);
+ }
+ };
+ }
+
+ let key_type = key_type.ok_or_else(|| SerdeError::missing_field("key_type"))?;
+ let x = x.ok_or_else(|| SerdeError::missing_field("x"))?;
+ let alg = alg.ok_or_else(|| SerdeError::missing_field("alg"))?;
+
+ let res = match key_type {
+ COSEKeyTypeId::OKP => {
+ let curve = curve.ok_or_else(|| SerdeError::missing_field("curve"))?;
+ COSEKeyType::OKP(COSEOKPKey { curve, x })
+ }
+ COSEKeyTypeId::EC2 => {
+ let curve = curve.ok_or_else(|| SerdeError::missing_field("curve"))?;
+ let y = y.ok_or_else(|| SerdeError::missing_field("y"))?;
+ COSEKeyType::EC2(COSEEC2Key { curve, x, y })
+ }
+ COSEKeyTypeId::RSA => {
+ let e = y.ok_or_else(|| SerdeError::missing_field("y"))?;
+ COSEKeyType::RSA(COSERSAKey { e, n: x })
+ }
+ COSEKeyTypeId::Symmetric => COSEKeyType::Symmetric(COSESymmetricKey { key: x }),
+ };
+ 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(_) => 3,
+ COSEKeyType::EC2(_) => 5,
+ COSEKeyType::RSA(_) => 4,
+ COSEKeyType::Symmetric(_) => 3,
+ };
+ 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, &key.x)?;
+ }
+ COSEKeyType::EC2(key) => {
+ map.serialize_entry(&1, &(COSEKeyTypeId::EC2 as u8))?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &(key.curve as u8))?;
+ 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, &key.n)?;
+ map.serialize_entry(&-2, &key.e)?;
+ }
+ COSEKeyType::Symmetric(key) => {
+ map.serialize_entry(&1, &COSEKeyTypeId::Symmetric)?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &key.key)?;
+ }
+ }
+
+ 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 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};
+ use crate::ctap2::commands::client_pin::Pin;
+ use crate::util::decode_hex;
+ use serde_cbor::de::from_slice;
+
+ #[test]
+ fn test_serialize_key() {
+ let x = [
+ 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,
+ ];
+ let y = [
+ 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 serialized_key = [
+ 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::from_sec1_uncompressed(Curve::SECP256R1, &serialized_key)
+ .expect("Failed to decode SEC 1 key");
+ assert_eq!(ec2_key.x, x);
+ assert_eq!(ec2_key.y, y);
+ }
+
+ #[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);
+ }
+}
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..890bb07954
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/nss.rs
@@ -0,0 +1,397 @@
+use super::{CryptoError, DER_OID_P256_BYTES};
+use nss_gk_api::p11::{
+ PK11Origin, PK11_CreateContextBySymKey, PK11_Decrypt, PK11_DigestFinal, PK11_DigestOp,
+ PK11_Encrypt, PK11_GenerateKeyPairWithOpFlags, PK11_GenerateRandom, PK11_HashBuf,
+ PK11_ImportSymKey, PK11_PubDeriveWithKDF, PrivateKey, PublicKey,
+ SECKEY_DecodeDERSubjectPublicKeyInfo, SECKEY_ExtractPublicKey, SECOidTag, Slot,
+ SubjectPublicKeyInfo, AES_BLOCK_SIZE, PK11_ATTR_SESSION, SHA256_LENGTH,
+};
+use nss_gk_api::{IntoResult, SECItem, SECItemBorrowed, PR_FALSE};
+use pkcs11_bindings::{
+ CKA_DERIVE, CKA_ENCRYPT, CKA_SIGN, CKD_NULL, CKF_DERIVE, CKM_AES_CBC, CKM_ECDH1_DERIVE,
+ 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;
+
+#[cfg(test)]
+use super::DER_OID_EC_PUBLIC_KEY_BYTES;
+
+#[cfg(test)]
+use nss_gk_api::p11::PK11_ImportDERPrivateKeyInfoAndReturnKey;
+
+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())
+}
+
+/// 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)?;
+
+ // 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 mut oid = SECItemBorrowed::wrap(DER_OID_P256_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
+ let (client_private, client_public) = 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_SESSION,
+ CKF_DERIVE,
+ CKF_DERIVE,
+ ptr::null_mut(),
+ )
+ .into_result()?;
+
+ let client_public = PublicKey::from_ptr(client_public_ptr)?;
+
+ (client_private, client_public)
+ };
+
+ 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>> {
+ 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 a key storage format such as PKCS#8. To avoid a dependency
+ * on an ASN.1 encoder for this test, we'll do it manually. */
+ let pkcs8_private_key_info_version = &[0x02, 0x01, 0x00];
+ let rfc5915_ec_private_key_version = &[0x02, 0x01, 0x01];
+
+ let (curve_oid, seq_len, alg_len, attr_len, ecpriv_len, param_len, spk_len) = (
+ DER_OID_P256_BYTES,
+ [0x81, 0x87].as_slice(),
+ [0x13].as_slice(),
+ [0x6d].as_slice(),
+ [0x6b].as_slice(),
+ [0x44].as_slice(),
+ [0x42].as_slice(),
+ );
+
+ let priv_len = client_private.len() as u8; // < 127
+
+ let mut pkcs8_priv: Vec<u8> = vec![];
+ // RFC 5208 PrivateKeyInfo
+ pkcs8_priv.push(0x30);
+ pkcs8_priv.extend_from_slice(seq_len);
+ // Integer (0)
+ pkcs8_priv.extend_from_slice(pkcs8_private_key_info_version);
+ // AlgorithmIdentifier
+ pkcs8_priv.push(0x30);
+ pkcs8_priv.extend_from_slice(alg_len);
+ // ObjectIdentifier
+ pkcs8_priv.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES);
+ // RFC 5480 ECParameters
+ pkcs8_priv.extend_from_slice(curve_oid);
+ // Attributes
+ pkcs8_priv.push(0x04);
+ pkcs8_priv.extend_from_slice(attr_len);
+ // RFC 5915 ECPrivateKey
+ pkcs8_priv.push(0x30);
+ pkcs8_priv.extend_from_slice(ecpriv_len);
+ pkcs8_priv.extend_from_slice(rfc5915_ec_private_key_version);
+ pkcs8_priv.push(0x04);
+ pkcs8_priv.push(priv_len);
+ pkcs8_priv.extend_from_slice(client_private);
+ pkcs8_priv.push(0xa1);
+ pkcs8_priv.extend_from_slice(param_len);
+ pkcs8_priv.push(0x03);
+ pkcs8_priv.extend_from_slice(spk_len);
+ pkcs8_priv.push(0x0);
+ pkcs8_priv.push(0x04); // SEC 1 encoded uncompressed point
+ pkcs8_priv.extend_from_slice(client_public_x);
+ pkcs8_priv.extend_from_slice(client_public_y);
+
+ // Now we can import the private key.
+ let slot = Slot::internal()?;
+ let mut pkcs8_priv_item = SECItemBorrowed::wrap(&pkcs8_priv);
+ let pkcs8_priv_item_ptr: *mut SECItem = pkcs8_priv_item.as_mut();
+ let mut client_private_ptr = ptr::null_mut();
+ unsafe {
+ PK11_ImportDERPrivateKeyInfoAndReturnKey(
+ *slot,
+ pkcs8_priv_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)
+}
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..84479bf49e
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/openssl.rs
@@ -0,0 +1,165 @@
+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)
+}
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..958bc01a7b
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/attestation.rs
@@ -0,0 +1,860 @@
+use super::utils::from_slice_stream;
+use crate::crypto::COSEAlgorithm;
+use crate::ctap2::commands::CommandError;
+use crate::ctap2::server::RpIdHash;
+use crate::{crypto::COSEKey, errors::AuthenticatorError};
+use nom::{
+ bytes::complete::take,
+ combinator::{cond, map},
+ error::Error as NomError,
+ number::complete::{be_u16, be_u32, be_u8},
+ Err as NomErr, IResult,
+};
+use serde::ser::{Error as SerError, SerializeMap, Serializer};
+use serde::{
+ de::{Error as SerdeError, MapAccess, Visitor},
+ Deserialize, Deserializer, Serialize,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor;
+use std::fmt;
+
+#[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 = "pinMinLength", skip_serializing_if = "Option::is_none")]
+ pub pin_min_length: Option<u64>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<HmacSecretResponse>,
+}
+
+impl Extension {
+ fn has_some(&self) -> bool {
+ self.pin_min_length.is_some() || self.hmac_secret.is_some()
+ }
+}
+
+fn parse_extensions(input: &[u8]) -> IResult<&[u8], Extension, NomError<&[u8]>> {
+ serde_to_nom(input)
+}
+
+#[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 serde_to_nom<'a, Output>(input: &'a [u8]) -> IResult<&'a [u8], Output>
+where
+ Output: Deserialize<'a>,
+{
+ from_slice_stream(input)
+ .map_err(|_e| nom::Err::Error(nom::error::make_error(input, nom::error::ErrorKind::NoneOf)))
+ // can't use custom errorkind because of error type mismatch in parse_attested_cred_data
+ //.map_err(|e| NomErr::Error(Context::Code(input, ErrorKind::Custom(e))))
+ // .map_err(|_| NomErr::Error(Context::Code(input, ErrorKind::Custom(42))))
+}
+
+fn parse_attested_cred_data(
+ input: &[u8],
+) -> IResult<&[u8], AttestedCredentialData, NomError<&[u8]>> {
+ let (rest, aaguid_res) = map(take(16u8), AAGuid::from)(input)?;
+ // // We can unwrap here, since we _know_ the input will be 16 bytes error out before calling from()
+ let aaguid = aaguid_res.unwrap();
+ let (rest, cred_len) = be_u16(rest)?;
+ let (rest, credential_id) = map(take(cred_len), Vec::from)(rest)?;
+ let (rest, credential_public_key) = serde_to_nom(rest)?;
+ Ok((
+ rest,
+ (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,
+}
+
+fn parse_ad(input: &[u8]) -> IResult<&[u8], AuthenticatorData, NomError<&[u8]>> {
+ let (rest, rp_id_hash_res) = map(take(32u8), RpIdHash::from)(input)?;
+ // We can unwrap here, since we _know_ the input to from() will be 32 bytes or error out before calling from()
+ let rp_id_hash = rp_id_hash_res.unwrap();
+ // preserve the flags, even if some reserved values are set.
+ let (rest, flags) = map(be_u8, AuthenticatorDataFlags::from_bits_truncate)(rest)?;
+ let (rest, counter) = be_u32(rest)?;
+ let (rest, credential_data) = cond(
+ flags.contains(AuthenticatorDataFlags::ATTESTED),
+ parse_attested_cred_data,
+ )(rest)?;
+ let (rest, extensions) = cond(
+ flags.contains(AuthenticatorDataFlags::EXTENSION_DATA),
+ parse_extensions,
+ )(rest)?;
+ // TODO(baloo): we should check for end of buffer and raise a parse
+ // parse error if data is still in the buffer
+ //eof!() >>
+ Ok((
+ rest,
+ AuthenticatorData {
+ rp_id_hash,
+ flags,
+ counter,
+ credential_data,
+ extensions: extensions.unwrap_or_default(),
+ },
+ ))
+}
+
+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, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ parse_ad(v)
+ .map(|(_input, value)| value)
+ .map_err(|e| match e {
+ NomErr::Incomplete(nom::Needed::Size(len)) => {
+ E::invalid_length(v.len(), &format!("{}", v.len() + len.get()).as_ref())
+ }
+ NomErr::Incomplete(nom::Needed::Unknown) => {
+ E::invalid_length(v.len(), &"unknown") // We don't know the expected value
+ }
+ // TODO(baloo): is that enough? should we be more
+ // specific on the error type?
+ e => E::custom(e.to_string()),
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(AuthenticatorDataVisitor)
+ }
+}
+
+impl 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)
+ pub fn to_vec(&self) -> Result<Vec<u8>, AuthenticatorError> {
+ 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.
+
+ // TODO(MS): Here flags=AT needs to be set, but this data comes from the security device
+ // and we (probably?) need to just trust the device to set the right flags
+ 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(CommandError::Serializing)?,
+ );
+ }
+ // TODO(MS): Here flags=ED needs to be set, but this data comes from the security device
+ // and we (probably?) need to just trust the device to set the right flags
+ if self.extensions.has_some() {
+ data.extend(
+ // (5) "extensions", len=variable
+ &serde_cbor::to_vec(&self.extensions).map_err(CommandError::Serializing)?,
+ );
+ }
+ Ok(data)
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+/// x509 encoded attestation certificate
+pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub(crate) 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(crate) ByteBuf);
+
+impl fmt::Debug for Signature {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD);
+ write!(f, "Signature({value})")
+ }
+}
+
+impl AsRef<[u8]> for Signature {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum AttestationStatement {
+ None,
+ Packed(AttestationStatementPacked),
+ // TODO(baloo): there is a couple other options than None and Packed:
+ // https://w3c.github.io/webauthn/#generating-an-attestation-object
+ // https://w3c.github.io/webauthn/#defined-attestation-formats
+ //TPM,
+ //AndroidKey,
+ //AndroidSafetyNet,
+ FidoU2F(AttestationStatementFidoU2F),
+}
+
+// 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(ByteBuf::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"
+}
+
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+enum AttestationFormat {
+ #[serde(rename = "fido-u2f")]
+ FidoU2F,
+ Packed,
+ None,
+ // TOOD(baloo): only packed is implemented for now, see spec:
+ // https://www.w3.org/TR/webauthn/#defined-attestation-formats
+ //TPM,
+ //AndroidKey,
+ //AndroidSafetyNet,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct AttestationObject {
+ pub auth_data: AuthenticatorData,
+ pub att_statement: AttestationStatement,
+}
+
+impl<'de> Deserialize<'de> for AttestationObject {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AttestationObjectVisitor;
+
+ impl<'de> Visitor<'de> for AttestationObjectVisitor {
+ type Value = AttestationObject;
+
+ 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<AttestationFormat> = None;
+ let mut auth_data = None;
+ let mut att_statement = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ // Spec for CTAP 2.0 is wrong and fmt should be numbered 1, and auth_data 2:
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
+ // Corrected in CTAP 2.1 and Webauthn spec
+ 1 => {
+ if format.is_some() {
+ return Err(SerdeError::duplicate_field("fmt"));
+ }
+ format = Some(map.next_value()?);
+ }
+ 2 => {
+ if auth_data.is_some() {
+ return Err(SerdeError::duplicate_field("auth_data"));
+ }
+ auth_data = Some(map.next_value()?);
+ }
+ 3 => {
+ let format = format
+ .as_ref()
+ .ok_or_else(|| SerdeError::missing_field("fmt"))?;
+ if att_statement.is_some() {
+ return Err(SerdeError::duplicate_field("att_statement"));
+ }
+ match format {
+ // This should not actually happen, but ...
+ AttestationFormat::None => {
+ att_statement = Some(AttestationStatement::None);
+ }
+ AttestationFormat::Packed => {
+ att_statement =
+ Some(AttestationStatement::Packed(map.next_value()?));
+ }
+ AttestationFormat::FidoU2F => {
+ att_statement =
+ Some(AttestationStatement::FidoU2F(map.next_value()?));
+ }
+ }
+ }
+ k => return Err(M::Error::custom(format!("unexpected key: {k:?}"))),
+ }
+ }
+
+ let auth_data =
+ auth_data.ok_or_else(|| M::Error::custom("found no auth_data".to_string()))?;
+ let att_statement = att_statement.unwrap_or(AttestationStatement::None);
+
+ Ok(AttestationObject {
+ auth_data,
+ att_statement,
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(AttestationObjectVisitor)
+ }
+}
+
+impl Serialize for AttestationObject {
+ /// Serialize can be used to repackage the CBOR answer we get from the token using CTAP-format
+ /// to webauthn-format (string-keys like "authData" instead of numbers). Yes, the specs are weird.
+ 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_statement {
+ AttestationStatement::None => {
+ // Even with Att None, an empty map is returned in the cbor!
+ map.serialize_entry(&"fmt", &"none")?; // (1) "fmt"
+ let v = serde_cbor::Value::Map(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"
+ }
+ }
+
+ let auth_data = self
+ .auth_data
+ .to_vec()
+ .map(serde_cbor::Value::Bytes)
+ .map_err(|_| SerError::custom("Failed to serialize auth_data"))?;
+ map.serialize_entry(&"authData", &auth_data)?; // (3) "authData"
+ map.end()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::super::utils::from_slice_stream;
+ use super::*;
+ use serde_cbor::from_slice;
+
+ const SAMPLE_ATTESTATION: [u8; 1006] = [
+ 0xa3, 0x1, 0x66, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2, 0x58, 0xc4, 0x49, 0x96, 0xd,
+ 0xe5, 0x88, 0xe, 0x8c, 0x68, 0x74, 0x34, 0x17, 0xf, 0x64, 0x76, 0x60, 0x5b, 0x8f, 0xe4,
+ 0xae, 0xb9, 0xa2, 0x86, 0x32, 0xc7, 0x99, 0x5c, 0xf3, 0xba, 0x83, 0x1d, 0x97, 0x63, 0x41,
+ 0x0, 0x0, 0x0, 0x7, 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27,
+ 0x29, 0xa1, 0x54, 0xa8, 0x0, 0x40, 0xc3, 0xcf, 0x1, 0x3b, 0xc6, 0x26, 0x93, 0x28, 0xfb,
+ 0x7f, 0xa9, 0x76, 0xef, 0xa8, 0x4b, 0x66, 0x71, 0xad, 0xa9, 0x64, 0xea, 0xcb, 0x58, 0x76,
+ 0x54, 0x51, 0xa, 0xc8, 0x86, 0x4f, 0xbb, 0x53, 0x2d, 0xfb, 0x2, 0xfc, 0xdc, 0xa9, 0x84,
+ 0xc2, 0x5c, 0x67, 0x8a, 0x3a, 0xab, 0x57, 0xf3, 0x71, 0x77, 0xd3, 0xd4, 0x41, 0x64, 0x1,
+ 0x50, 0xca, 0x6c, 0x42, 0x73, 0x1c, 0x42, 0xcb, 0x81, 0xba, 0xa5, 0x1, 0x2, 0x3, 0x26,
+ 0x20, 0x1, 0x21, 0x58, 0x20, 0x9, 0x2e, 0x34, 0xfe, 0xa7, 0xd7, 0x32, 0xc8, 0xae, 0x4c,
+ 0xf6, 0x96, 0xbe, 0x7a, 0x12, 0xdc, 0x29, 0xd5, 0xf1, 0xd3, 0xf1, 0x55, 0x4d, 0xdc, 0x87,
+ 0xc4, 0xc, 0x9b, 0xd0, 0x17, 0xba, 0xf, 0x22, 0x58, 0x20, 0xc9, 0xf0, 0x97, 0x33, 0x55,
+ 0x36, 0x58, 0xd9, 0xdb, 0x76, 0xf5, 0xef, 0x95, 0xcf, 0x8a, 0xc7, 0xfc, 0xc1, 0xb6, 0x81,
+ 0x25, 0x5f, 0x94, 0x6b, 0x62, 0x13, 0x7d, 0xd0, 0xc4, 0x86, 0x53, 0xdb, 0x3, 0xa3, 0x63,
+ 0x61, 0x6c, 0x67, 0x26, 0x63, 0x73, 0x69, 0x67, 0x58, 0x48, 0x30, 0x46, 0x2, 0x21, 0x0,
+ 0xac, 0x2a, 0x78, 0xa8, 0xaf, 0x18, 0x80, 0x39, 0x73, 0x8d, 0x3, 0x5e, 0x4, 0x4d, 0x94,
+ 0x4f, 0x3f, 0x57, 0xce, 0x88, 0x41, 0xfa, 0x81, 0x50, 0x40, 0xb6, 0xd1, 0x95, 0xb5, 0xeb,
+ 0xe4, 0x6f, 0x2, 0x21, 0x0, 0x8f, 0xf4, 0x15, 0xc9, 0xb3, 0x6d, 0x1c, 0xd, 0x4c, 0xa3,
+ 0xcf, 0x99, 0x8a, 0x46, 0xd4, 0x4c, 0x8b, 0x5c, 0x26, 0x3f, 0xdf, 0x22, 0x6c, 0x9b, 0x23,
+ 0x83, 0x8b, 0x69, 0x47, 0x67, 0x48, 0x45, 0x63, 0x78, 0x35, 0x63, 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_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,
+ ];
+
+ #[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_object() {
+ let value: AttestationObject = from_slice(&SAMPLE_ATTESTATION).unwrap();
+ println!("{value:?}");
+
+ //assert_eq!(true, false);
+ }
+
+ #[test]
+ fn parse_reader() {
+ let v: Vec<u8> = vec![
+ 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72,
+ ];
+ let (rest, value): (&[u8], String) = from_slice_stream(&v).unwrap();
+ assert_eq!(value, "foobar");
+ assert_eq!(rest, &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]);
+ let (rest, value): (&[u8], String) = from_slice_stream(rest).unwrap();
+ assert_eq!(value, "foobar");
+ assert!(rest.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,
+ ]))
+ );
+ }
+
+ /// 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..dc8fd1cb6f
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/client_data.rs
@@ -0,0 +1,338 @@
+use super::commands::CommandError;
+use crate::transport::errors::HIDError;
+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::encode_config(input, base64::URL_SAFE_NO_PAD);
+ 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/client_pin.rs b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs
new file mode 100644
index 0000000000..76a83babc7
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs
@@ -0,0 +1,769 @@
+#![allow(non_upper_case_globals)]
+// 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, PinUvAuthToken, SharedSecret};
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+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;
+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 {
+ pin_protocol: Option<PinUvAuthProtocol>,
+ subcommand: PINSubcommand,
+ key_agreement: Option<COSEKey>,
+ pin_auth: Option<ByteBuf>,
+ new_pin_enc: Option<ByteBuf>,
+ pin_hash_enc: Option<ByteBuf>,
+ permissions: Option<u8>,
+ 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, pin_auth)?;
+ }
+ if let Some(ref new_pin_enc) = self.new_pin_enc {
+ map.serialize_entry(&5, new_pin_enc)?;
+ }
+ if let Some(ref pin_hash_enc) = self.pin_hash_enc {
+ map.serialize_entry(&6, 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>;
+}
+
+struct ClientPinResponse {
+ key_agreement: Option<COSEKey>,
+ pin_token: Option<EncryptedPinToken>,
+ /// Number of PIN attempts remaining before lockout.
+ pin_retries: Option<u8>,
+ power_cycle_state: Option<bool>,
+ uv_retries: Option<u8>,
+}
+
+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"));
+ }
+ pin_token = map.next_value()?;
+ }
+ 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 = KeyAgreement;
+
+ 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 value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("GetKeyAgreement::parse_response_payload {:?}", value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ if let Some(key_agreement) = get_pin_response.key_agreement {
+ Ok(KeyAgreement {
+ pin_protocol: self.pin_protocol.clone(),
+ peer_key: key_agreement,
+ })
+ } else {
+ Err(CommandError::MissingRequiredField("key_agreement"))
+ }
+ }
+}
+
+#[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 = PinUvAuthToken;
+
+ 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(ByteBuf::from(pin_hash_enc)),
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("GetKeyAgreement::parse_response_payload {:?}", value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ match get_pin_response.pin_token {
+ Some(encrypted_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 = self
+ .shared_secret
+ .decrypt_pin_token(default_permissions, encrypted_pin_token.as_ref())?;
+ Ok(pin_token)
+ }
+ None => Err(CommandError::MissingRequiredField("key_agreement")),
+ }
+ }
+}
+
+#[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 = PinUvAuthToken;
+
+ 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(ByteBuf::from(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 value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!(
+ "GetPinUvAuthTokenUsingPinWithPermissions::parse_response_payload {:?}",
+ value
+ );
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ match get_pin_response.pin_token {
+ Some(encrypted_pin_token) => {
+ let pin_token = self
+ .shared_secret
+ .decrypt_pin_token(self.permissions, encrypted_pin_token.as_ref())?;
+ Ok(pin_token)
+ }
+ None => Err(CommandError::MissingRequiredField("key_agreement")),
+ }
+ }
+}
+
+macro_rules! implementRetries {
+ ($name:ident, $getter:ident) => {
+ #[derive(Debug)]
+ pub struct $name {}
+
+ impl $name {
+ pub fn new() -> Self {
+ Self {}
+ }
+ }
+
+ impl ClientPINSubCommand for $name {
+ type Output = u8;
+
+ 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 value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("{}::parse_response_payload {:?}", stringify!($name), value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ match get_pin_response.$getter {
+ Some($getter) => Ok($getter),
+ None => Err(CommandError::MissingRequiredField(stringify!($getter))),
+ }
+ }
+ }
+ };
+}
+
+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 = PinUvAuthToken;
+
+ 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 value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("GetKeyAgreement::parse_response_payload {:?}", value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ match get_pin_response.pin_token {
+ Some(encrypted_pin_token) => {
+ let pin_token = self
+ .shared_secret
+ .decrypt_pin_token(self.permissions, encrypted_pin_token.as_ref())?;
+ Ok(pin_token)
+ }
+ None => Err(CommandError::MissingRequiredField("key_agreement")),
+ }
+ }
+}
+
+#[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 = ();
+
+ 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(ByteBuf::from(new_pin_enc)),
+ pin_auth: Some(ByteBuf::from(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() {
+ Ok(())
+ } else {
+ let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ Ok(())
+ }
+ }
+}
+
+#[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 = ();
+
+ 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(ByteBuf::from(new_pin_enc)),
+ pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
+ pin_auth: Some(ByteBuf::from(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() {
+ Ok(())
+ } else {
+ let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ Ok(())
+ }
+ }
+}
+
+impl<T> RequestCtap2 for T
+where
+ T: ClientPINSubCommand,
+ T: fmt::Debug,
+{
+ type Output = <T as ClientPINSubCommand>::Output;
+
+ fn command() -> 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>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ 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())
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct KeyAgreement {
+ pin_protocol: PinUvAuthProtocol,
+ peer_key: COSEKey,
+}
+
+impl KeyAgreement {
+ pub fn shared_secret(&self) -> Result<SharedSecret, CommandError> {
+ Ok(self.pin_protocol.encapsulate(&self.peer_key)?)
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct EncryptedPinToken(ByteBuf);
+
+impl AsRef<[u8]> for EncryptedPinToken {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[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)
+ }
+}
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..4c57c00b4b
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs
@@ -0,0 +1,1504 @@
+use super::get_info::AuthenticatorInfo;
+use super::{
+ Command, CommandError, PinUvAuthCommand, Request, 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::client_pin::Pin;
+use crate::ctap2::commands::get_next_assertion::GetNextAssertion;
+use crate::ctap2::commands::make_credentials::UserVerification;
+use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, RelyingPartyWrapper, RpIdHash, User, UserVerificationRequirement,
+};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::FidoDevice;
+use crate::u2ftypes::CTAP1RequestAPDU;
+use nom::{
+ error::VerboseError,
+ number::complete::{be_u32, be_u8},
+ sequence::tuple,
+};
+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;
+
+#[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, Clone, Default)]
+pub struct GetAssertionExtensions {
+ pub hmac_secret: Option<HmacSecretExtension>,
+}
+
+impl Serialize for GetAssertionExtensions {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(1))?;
+ map.serialize_entry(&"hmac-secret", &self.hmac_secret)?;
+ map.end()
+ }
+}
+
+impl GetAssertionExtensions {
+ fn has_extensions(&self) -> bool {
+ self.hmac_secret.is_some()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct GetAssertion {
+ pub(crate) client_data_hash: ClientDataHash,
+ pub(crate) rp: RelyingPartyWrapper,
+ pub(crate) 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(crate) extensions: GetAssertionExtensions,
+ pub(crate) options: GetAssertionOptions,
+ pub(crate) pin: Option<Pin>,
+ pub(crate) pin_uv_auth_param: Option<PinUvAuthParam>,
+
+ // This is used to implement the FIDO AppID extension.
+ pub(crate) alternate_rp_id: Option<String>,
+}
+
+impl GetAssertion {
+ pub fn new(
+ client_data_hash: ClientDataHash,
+ rp: RelyingPartyWrapper,
+ allow_list: Vec<PublicKeyCredentialDescriptor>,
+ options: GetAssertionOptions,
+ extensions: GetAssertionExtensions,
+ pin: Option<Pin>,
+ alternate_rp_id: Option<String>,
+ ) -> Self {
+ Self {
+ client_data_hash,
+ rp,
+ allow_list,
+ extensions,
+ options,
+ pin,
+ pin_uv_auth_param: None,
+ alternate_rp_id,
+ }
+ }
+}
+
+impl PinUvAuthCommand for GetAssertion {
+ fn pin(&self) -> &Option<Pin> {
+ &self.pin
+ }
+
+ fn set_pin(&mut self, pin: Option<Pin>) {
+ self.pin = pin;
+ }
+
+ 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_uv_option(&mut self) -> Option<bool> {
+ self.options.user_verification
+ }
+
+ fn get_rp(&self) -> &RelyingPartyWrapper {
+ &self.rp
+ }
+
+ 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_extensions() {
+ 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))?;
+ match self.rp {
+ RelyingPartyWrapper::Data(ref d) => {
+ map.serialize_entry(&1, &d.id)?;
+ }
+ _ => {
+ return Err(S::Error::custom(
+ "Can't serialize a RelyingParty::Hash for CTAP2",
+ ));
+ }
+ }
+
+ 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_extensions() {
+ 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()
+ }
+}
+
+impl Request<GetAssertionResult> for GetAssertion {}
+
+impl RequestCtap1 for GetAssertion {
+ type Output = 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(
+ &self,
+ 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)));
+ }
+
+ GetAssertionResult::from_ctap1(input, &self.rp.hash(), add_info)
+ .map_err(HIDError::Command)
+ .map_err(Retryable::Error)
+ }
+}
+
+impl RequestCtap2 for GetAssertion {
+ type Output = GetAssertionResult;
+
+ fn command() -> 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>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: FidoDevice + io::Read + io::Write + fmt::Debug,
+ {
+ 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() {
+ let assertion: GetAssertionResponse =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ let number_of_credentials = assertion.number_of_credentials.unwrap_or(1);
+ let mut assertions = Vec::with_capacity(number_of_credentials);
+ assertions.push(assertion.into());
+
+ let msg = GetNextAssertion;
+ // We already have one, so skipping 0
+ for _ in 1..number_of_credentials {
+ let new_cred = dev.send_cbor(&msg)?;
+ assertions.push(new_cred.into());
+ }
+
+ Ok(GetAssertionResult(assertions))
+ } 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())
+ }
+ }
+}
+
+#[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<User>,
+}
+
+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 Vec<Assertion>);
+
+impl GetAssertionResult {
+ pub fn from_ctap1(
+ input: &[u8],
+ rp_id_hash: &RpIdHash,
+ key_handle: &PublicKeyCredentialDescriptor,
+ ) -> Result<GetAssertionResult, CommandError> {
+ let parse_authentication = |input| {
+ // Parsing an u8, then a u32, and the rest is the signature
+ let (rest, (user_presence, counter)) = tuple((be_u8, be_u32))(input)?;
+ let signature = Vec::from(rest);
+ Ok((user_presence, counter, signature))
+ };
+ let (user_presence, counter, signature) =
+ parse_authentication(input).map_err(|e: nom::Err<VerboseError<_>>| {
+ error!("error while parsing authentication: {:?}", e);
+ CommandError::Deserializing(DesError::custom("unable to parse authentication"))
+ })?;
+
+ // 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(vec![assertion]))
+ }
+
+ pub fn u2f_sign_data(&self) -> Vec<u8> {
+ if let Some(first) = self.0.first() {
+ let mut res = Vec::new();
+ res.push(first.auth_data.flags.bits());
+ res.extend(first.auth_data.counter.to_be_bytes());
+ res.extend(&first.signature);
+ res
+ // first.signature.clone()
+ } else {
+ Vec::new()
+ }
+ }
+}
+
+pub(crate) struct GetAssertionResponse {
+ credentials: Option<PublicKeyCredentialDescriptor>,
+ auth_data: AuthenticatorData,
+ signature: Vec<u8>,
+ user: Option<User>,
+ number_of_credentials: Option<usize>,
+}
+
+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::{
+ PublicKeyCredentialDescriptor, RelyingParty, RelyingPartyWrapper, RpIdHash, Transport, User,
+ };
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use crate::transport::FidoDevice;
+ use crate::u2ftypes::{U2FDevice, 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"),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ 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(),
+ None,
+ None,
+ );
+ let mut device = Device::new("commands/get_assertion").unwrap();
+ 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, 0x5c]); // 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(User {
+ 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,
+ ],
+ icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()),
+ name: Some("johnpsmith@example.com".to_string()),
+ display_name: Some("John P. Smith".to_string()),
+ }),
+ auth_data: expected_auth_data,
+ };
+
+ let expected = GetAssertionResult(vec![expected_assertion]);
+ 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"),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ vec![allowed_key.clone()],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ None,
+ );
+ let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
+ // channel id
+ 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 = GetAssertionResult(vec![expected_assertion]);
+
+ 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"),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ vec![too_long_key_handle.clone()],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ None,
+ );
+
+ let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
+ // channel id
+ 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 = GetAssertionResult(vec![expected_assertion]);
+
+ 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"),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ 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(),
+ None,
+ None,
+ );
+ let mut device = Device::new("commands/get_assertion").unwrap();
+ 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: 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, 0x94]);
+ 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
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x75, 0x76, // uv
+ 0xf4, // false
+ 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, 0x94]);
+ 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
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x75, 0x76, // uv
+ 0xf4, // false
+ 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, 0x5c]); // 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.0[0].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; 348] = [
+ 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)
+ 0xA4, // map(4)
+ 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)
+ 0x69, 0x63, 0x6F, 0x6E, // "icon"
+ 0x78, 0x2B, // text(0x43, )
+ 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x70, 0x69, 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, // "https://pics.example.com/0x00, /p/aBjjjpqPb.png"
+ 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..d2aea1908b
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs
@@ -0,0 +1,983 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::ctap2::attestation::AAGuid;
+use crate::ctap2::server::PublicKeyCredentialParameters;
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+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() -> Command {
+ Command::GetInfo
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ 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 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: 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_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
+ }
+}
+
+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 = Vec::new();
+ 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 => {
+ if !pin_protocols.is_empty() {
+ return Err(serde::de::Error::duplicate_field("pin_protocols"));
+ }
+ pin_protocols = map.next_value()?;
+ }
+ 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(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, Nonce};
+ use crate::u2ftypes::U2FDevice;
+ 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: 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: 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();
+ 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(Nonce::Use(nonce))
+ .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: 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
+ );
+ }
+}
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..6b3d7b3612
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs
@@ -0,0 +1,50 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::ctap2::commands::get_assertion::GetAssertionResponse;
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug)]
+pub(crate) struct GetNextAssertion;
+
+impl RequestCtap2 for GetNextAssertion {
+ type Output = GetAssertionResponse;
+
+ fn command() -> Command {
+ Command::GetNextAssertion
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ 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())
+ }
+ }
+}
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..95a3bccf3c
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs
@@ -0,0 +1,110 @@
+use super::{CommandError, RequestCtap1, Retryable};
+use crate::consts::U2F_VERSION;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::u2ftypes::CTAP1RequestAPDU;
+
+#[allow(non_camel_case_types)]
+pub enum U2FInfo {
+ U2F_V2,
+}
+
+#[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(
+ &self,
+ _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, ()))
+ }
+}
+
+#[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, Nonce};
+ use crate::u2ftypes::U2FDevice;
+ use rand::{thread_rng, RngCore};
+
+ #[test]
+ fn test_get_version_ctap1_only() {
+ let mut device = Device::new("commands/get_version").unwrap();
+ 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(Nonce::Use(nonce))
+ .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..b401f22d93
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs
@@ -0,0 +1,1079 @@
+use super::get_info::{AuthenticatorInfo, AuthenticatorVersion};
+use super::{
+ Command, CommandError, PinUvAuthCommand, Request, 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,
+};
+use crate::ctap2::client_data::ClientDataHash;
+use crate::ctap2::commands::client_pin::Pin;
+use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ RelyingPartyWrapper, RpIdHash, User, UserVerificationRequirement,
+};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::u2ftypes::{CTAP1RequestAPDU, U2FDevice};
+use nom::{
+ bytes::complete::{tag, take},
+ error::VerboseError,
+ number::complete::be_u8,
+};
+#[cfg(test)]
+use serde::Deserialize;
+use serde::{
+ de::Error as DesError,
+ ser::{Error as SerError, SerializeMap},
+ Serialize, Serializer,
+};
+use serde_cbor::{self, de::from_slice, ser, Value};
+use std::fmt;
+use std::io;
+
+#[derive(Debug)]
+pub struct MakeCredentialsResult(pub AttestationObject);
+
+impl MakeCredentialsResult {
+ pub fn from_ctap1(
+ input: &[u8],
+ rp_id_hash: &RpIdHash,
+ ) -> Result<MakeCredentialsResult, CommandError> {
+ let parse_register = |input| {
+ let (rest, _) = tag(&[0x05])(input)?;
+ let (rest, public_key) = take(65u8)(rest)?;
+ let (rest, key_handle_len) = be_u8(rest)?;
+ let (rest, key_handle) = take(key_handle_len)(rest)?;
+ Ok((rest, public_key, key_handle))
+ };
+
+ let (rest, public_key, key_handle) =
+ parse_register(input).map_err(|e: nom::Err<VerboseError<_>>| {
+ error!("error while parsing registration: {:?}", e);
+ CommandError::Deserializing(DesError::custom("unable to parse registration"))
+ })?;
+
+ let cert_and_sig = parse_u2f_der_certificate(rest).map_err(|e| {
+ error!("error while parsing registration: {:?}", e);
+ CommandError::Deserializing(DesError::custom("unable to parse registration"))
+ })?;
+
+ let credential_ec2_key = COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, public_key)
+ .map_err(|e| {
+ error!("error while parsing registration: {:?}", e);
+ CommandError::Deserializing(DesError::custom("unable to parse registration"))
+ })?;
+
+ 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: Vec::from(key_handle),
+ credential_public_key,
+ }),
+ extensions: Default::default(),
+ };
+
+ let att_statement = AttestationStatement::FidoU2F(AttestationStatementFidoU2F::new(
+ cert_and_sig.certificate,
+ cert_and_sig.signature,
+ ));
+
+ let attestation_object = AttestationObject {
+ auth_data,
+ att_statement,
+ };
+
+ Ok(MakeCredentialsResult(attestation_object))
+ }
+}
+
+#[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, Clone, Serialize, Default)]
+pub struct MakeCredentialsExtensions {
+ #[serde(rename = "pinMinLength", skip_serializing_if = "Option::is_none")]
+ pub pin_min_length: Option<bool>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<bool>,
+}
+
+impl MakeCredentialsExtensions {
+ fn has_extensions(&self) -> bool {
+ self.pin_min_length.or(self.hmac_secret).is_some()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct MakeCredentials {
+ pub(crate) client_data_hash: ClientDataHash,
+ pub(crate) rp: RelyingPartyWrapper,
+ // Note(baloo): If none -> ctap1
+ pub(crate) user: Option<User>,
+ pub(crate) pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ pub(crate) 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(crate) extensions: MakeCredentialsExtensions,
+ pub(crate) options: MakeCredentialsOptions,
+ pub(crate) pin: Option<Pin>,
+ pub(crate) pin_uv_auth_param: Option<PinUvAuthParam>,
+ pub(crate) enterprise_attestation: Option<u64>,
+}
+
+impl MakeCredentials {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ client_data_hash: ClientDataHash,
+ rp: RelyingPartyWrapper,
+ user: Option<User>,
+ pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ exclude_list: Vec<PublicKeyCredentialDescriptor>,
+ options: MakeCredentialsOptions,
+ extensions: MakeCredentialsExtensions,
+ pin: Option<Pin>,
+ ) -> Self {
+ Self {
+ client_data_hash,
+ rp,
+ user,
+ pub_cred_params,
+ exclude_list,
+ extensions,
+ options,
+ pin,
+ pin_uv_auth_param: None,
+ enterprise_attestation: None,
+ }
+ }
+}
+
+impl PinUvAuthCommand for MakeCredentials {
+ fn pin(&self) -> &Option<Pin> {
+ &self.pin
+ }
+
+ fn set_pin(&mut self, pin: Option<Pin>) {
+ self.pin = pin;
+ }
+
+ 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_uv_option(&mut self) -> Option<bool> {
+ self.options.user_verification
+ }
+
+ fn get_rp(&self) -> &RelyingPartyWrapper {
+ &self.rp
+ }
+
+ 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);
+ let device_protected = supports_uv || pin_configured;
+ // make_cred_uv_not_rqd is only relevant for rk = false
+ let make_cred_uv_not_required = info.options.make_cred_uv_not_rqd == Some(true)
+ && self.options.resident_key != Some(true);
+ // For CTAP2.0, UV is always required when doing MakeCredential
+ let always_uv = info.options.always_uv == Some(true)
+ || info.max_supported_version() == AuthenticatorVersion::FIDO_2_0;
+ let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged;
+
+ // CTAP 2.1 authenticators can allow MakeCredential without PinUvAuth,
+ // but that is only relevant, if RP also discourages UV.
+ let can_make_cred_without_uv = make_cred_uv_not_required && uv_discouraged;
+
+ !always_uv && (!device_protected || can_make_cred_without_uv)
+ }
+
+ 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_extensions() {
+ 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)?;
+ match self.rp {
+ RelyingPartyWrapper::Data(ref d) => {
+ map.serialize_entry(&0x02, &d)?;
+ }
+ _ => {
+ return Err(S::Error::custom(
+ "Can't serialize a RelyingParty::Hash for CTAP2",
+ ));
+ }
+ }
+ 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_extensions() {
+ 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 Request<MakeCredentialsResult> for MakeCredentials {}
+
+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(
+ &self,
+ 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)));
+ }
+
+ MakeCredentialsResult::from_ctap1(input, &self.rp.hash())
+ .map_err(HIDError::Command)
+ .map_err(Retryable::Error)
+ }
+}
+
+impl RequestCtap2 for MakeCredentials {
+ type Output = MakeCredentialsResult;
+
+ fn command() -> 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>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice + io::Read + io::Write + fmt::Debug,
+ {
+ 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() {
+ let attestation = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Ok(MakeCredentialsResult(attestation))
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(HIDError::Command(CommandError::StatusCode(
+ status,
+ Some(data),
+ )))
+ }
+ } else if status.is_ok() {
+ Err(HIDError::Command(CommandError::InputTooSmall))
+ } else {
+ Err(HIDError::Command(CommandError::StatusCode(status, None)))
+ }
+ }
+}
+
+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,
+ ]),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("make.me.blink"),
+ ..Default::default()
+ }),
+ Some(User {
+ id: vec![0],
+ name: Some(String::from("make.me.blink")),
+ ..Default::default()
+ }),
+ vec![PublicKeyCredentialParameters {
+ alg: crate::COSEAlgorithm::ES256,
+ }],
+ vec![],
+ MakeCredentialsOptions::default(),
+ MakeCredentialsExtensions::default(),
+ None,
+ );
+ // 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};
+ use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve};
+ use crate::ctap2::attestation::{
+ AAGuid, AttestationCertificate, AttestationObject, AttestationStatement,
+ AttestationStatementFidoU2F, AttestationStatementPacked, 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::{
+ PublicKeyCredentialParameters, RelyingParty, RelyingPartyWrapper, User,
+ };
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use serde_bytes::ByteBuf;
+
+ 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_statement: AttestationStatement::Packed(AttestationStatementPacked {
+ alg: COSEAlgorithm::ES256,
+ sig: Signature(ByteBuf::from([
+ 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 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"),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ Some(User {
+ id: base64::decode_config(
+ "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=",
+ base64::URL_SAFE_NO_PAD,
+ )
+ .unwrap(),
+ icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()),
+ 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(),
+ None,
+ );
+
+ let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it)
+ let req_serialized = req
+ .wire_format()
+ .expect("Failed to serialize MakeCredentials request");
+ assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2);
+ let attestation_object = req
+ .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2)
+ .expect("Failed to handle CTAP2 response")
+ .0;
+ let expected = create_attestation_obj();
+
+ assert_eq!(attestation_object, 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"),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ Some(User {
+ id: base64::decode_config(
+ "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=",
+ base64::URL_SAFE_NO_PAD,
+ )
+ .unwrap(),
+ icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()),
+ 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(),
+ None,
+ );
+
+ 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 attestation_object = req
+ .handle_response_ctap1(Ok(()), &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1, &())
+ .expect("Failed to handle CTAP1 response")
+ .0;
+
+ let expected = 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_statement: AttestationStatement::FidoU2F(AttestationStatementFidoU2F {
+ sig: Signature(ByteBuf::from([
+ 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,
+ ])],
+ }),
+ };
+
+ assert_eq!(attestation_object, expected);
+ }
+
+ #[test]
+ fn serialize_attestation_object() {
+ let att_obj = create_attestation_obj();
+ let serialized_obj =
+ serde_cbor::to_vec(&att_obj).expect("Failed to serialize attestation object");
+ assert_eq!(serialized_obj, SERIALIZED_ATTESTATION_OBJECT);
+ }
+
+ #[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; 260] = [
+ // 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
+ 0xa4, // map(4)
+ 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)
+ 0x69, 0x63, 0x6f, 0x6e, // "icon"
+ 0x78, 0x2b, // text(43)
+ 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, // "https://pics.example.com/00/p/aBjjjpqPb.png"
+ 0x2f, 0x70, 0x69, 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, // "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, // ...
+ ];
+
+ #[rustfmt::skip]
+ pub const SERIALIZED_ATTESTATION_OBJECT: [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,
+ ];
+}
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..8629c511fd
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/mod.rs
@@ -0,0 +1,480 @@
+use super::server::RelyingPartyWrapper;
+use crate::crypto::{CryptoError, PinUvAuthParam, PinUvAuthToken};
+use crate::ctap2::commands::client_pin::{GetPinRetries, GetUvRetries, Pin, 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;
+use serde_cbor::{error::Error as CborError, Value};
+use serde_json as json;
+use std::error::Error as StdErrorT;
+use std::fmt;
+use std::io::{Read, Write};
+
+pub(crate) mod client_pin;
+pub(crate) mod get_assertion;
+pub(crate) mod get_info;
+pub(crate) mod get_next_assertion;
+pub(crate) mod get_version;
+pub(crate) mod make_credentials;
+pub(crate) mod reset;
+pub(crate) mod selection;
+
+pub trait Request<T>
+where
+ Self: fmt::Debug,
+ Self: RequestCtap1<Output = T>,
+ Self: RequestCtap2<Output = T>,
+{
+}
+
+/// 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;
+ // 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(
+ &self,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ add_info: &Self::AdditionalInfo,
+ ) -> Result<Self::Output, Retryable<HIDError>>;
+}
+
+pub trait RequestCtap2: fmt::Debug {
+ type Output;
+
+ fn command() -> Command;
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError>;
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: FidoDevice + Read + Write + fmt::Debug;
+}
+
+#[derive(Debug, Clone)]
+pub(crate) 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 pin(&self) -> &Option<Pin>;
+ fn set_pin(&mut self, pin: Option<Pin>);
+ 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_uv_option(&mut self) -> Option<bool>;
+ fn get_rp(&self) -> &RelyingPartyWrapper;
+ 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();
+ let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err
+ AuthenticatorError::PinError(PinError::InvalidPin(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();
+ let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err
+ AuthenticatorError::PinError(PinError::InvalidUv(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,
+ Selection = 0x0B,
+}
+
+impl Command {
+ #[cfg(test)]
+ pub fn from_u8(v: u8) -> Option<Command> {
+ match v {
+ 0x01 => Some(Command::MakeCredentials),
+ 0x02 => Some(Command::GetAssertion),
+ 0x04 => Some(Command::GetInfo),
+ 0x06 => Some(Command::ClientPin),
+ 0x07 => Some(Command::Reset),
+ 0x08 => Some(Command::GetNextAssertion),
+ _ => None,
+ }
+ }
+}
+
+#[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..d06015af24
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/reset.rs
@@ -0,0 +1,119 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug, Default)]
+pub struct Reset {}
+
+impl RequestCtap2 for Reset {
+ type Output = ();
+
+ fn command() -> Command {
+ Command::Reset
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ 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())
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::HIDCmd;
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice};
+ use crate::u2ftypes::U2FDevice;
+ 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();
+ // 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..63cec47b6a
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/selection.rs
@@ -0,0 +1,119 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug, Default)]
+pub struct Selection {}
+
+impl RequestCtap2 for Selection {
+ type Output = ();
+
+ fn command() -> Command {
+ Command::Selection
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ 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())
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::HIDCmd;
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice};
+ use crate::u2ftypes::U2FDevice;
+ 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();
+ // 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..33ab859452
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/mod.rs
@@ -0,0 +1,10 @@
+#[allow(dead_code)] // TODO(MS): Remove me asap
+pub mod commands;
+pub use commands::get_assertion::GetAssertionResult;
+
+pub mod attestation;
+
+pub mod client_data;
+pub(crate) mod preflight;
+pub mod server;
+pub(crate) mod utils;
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..3adf44176e
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/preflight.rs
@@ -0,0 +1,196 @@
+use super::client_data::ClientDataHash;
+use super::commands::get_assertion::{GetAssertion, GetAssertionOptions};
+use super::commands::{CommandError, PinUvAuthCommand, RequestCtap1, Retryable, StatusCode};
+use crate::authenticatorservice::GetAssertionExtensions;
+use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED};
+use crate::crypto::PinUvAuthToken;
+use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingPartyWrapper};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::FidoDevice;
+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(crate) struct CheckKeyHandle<'assertion> {
+ pub(crate) key_handle: &'assertion [u8],
+ pub(crate) client_data_hash: &'assertion [u8],
+ pub(crate) rp: &'assertion RelyingPartyWrapper,
+}
+
+impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> {
+ type Output = ();
+ 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(
+ &self,
+ 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))),
+ }
+ }
+}
+
+/// "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: &RelyingPartyWrapper,
+ 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: &RelyingPartyWrapper,
+ 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);
+ }
+ }
+
+ // Step 1.2: Return early, if we only have one chunk anyways
+ if cred_list.len() <= chunk_size {
+ return Ok(cred_list);
+ }
+
+ 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: if pin_uv_auth_token.is_some() {
+ None
+ } else {
+ Some(false)
+ },
+ user_presence: Some(false),
+ },
+ GetAssertionExtensions::default(),
+ None,
+ None,
+ );
+ silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?;
+ let res = dev.send_msg(&silent_assert);
+ match res {
+ Ok(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
+ .0
+ .iter()
+ .filter_map(|a| a.credentials.clone())
+ .collect();
+ // Replace credential_id_list with the valid credentials
+ final_list = credential_ids;
+ break;
+ }
+ Err(HIDError::Command(CommandError::StatusCode(StatusCode::NoCredentials, _))) => {
+ // No-op: Go to next chunk.
+ }
+ Err(e) => {
+ // Some unexpected error
+ return Err(e.into());
+ }
+ }
+ }
+
+ // 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)
+}
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..163c2a6f1e
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/server.rs
@@ -0,0 +1,532 @@
+use crate::crypto::COSEAlgorithm;
+use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle};
+use serde::de::MapAccess;
+use serde::{
+ de::{Error as SerdeError, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+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::encode_config(self.0, base64::URL_SAFE_NO_PAD);
+ 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))
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Clone, Default)]
+#[cfg_attr(test, derive(Deserialize))]
+pub struct RelyingParty {
+ // TODO(baloo): spec is wrong !!!!111
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#commands
+ // in the example "A PublicKeyCredentialRpEntity DOM object defined as follows:"
+ // inconsistent with https://w3c.github.io/webauthn/#sctn-rp-credential-params
+ pub id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub icon: Option<String>,
+}
+
+// Note: This enum is provided to make old CTAP1/U2F API work. This should be deprecated at some point
+#[derive(Debug, Clone)]
+pub enum RelyingPartyWrapper {
+ Data(RelyingParty),
+ // CTAP1 hash can be derived from full object, see RelyingParty::hash below,
+ // but very old backends might still provide application IDs.
+ Hash(RpIdHash),
+}
+
+impl RelyingPartyWrapper {
+ pub fn hash(&self) -> RpIdHash {
+ match *self {
+ RelyingPartyWrapper::Data(ref d) => {
+ let mut hasher = Sha256::new();
+ hasher.update(&d.id);
+
+ let mut output = [0u8; 32];
+ output.copy_from_slice(hasher.finalize().as_slice());
+
+ RpIdHash(output)
+ }
+ RelyingPartyWrapper::Hash(ref d) => d.clone(),
+ }
+ }
+
+ pub fn id(&self) -> Option<&String> {
+ match self {
+ // CTAP1 case: We only have the hash, not the entire RpID
+ RelyingPartyWrapper::Hash(..) => None,
+ RelyingPartyWrapper::Data(r) => Some(&r.id),
+ }
+ }
+}
+
+// TODO(baloo): should we rename this PublicKeyCredentialUserEntity ?
+#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize, Default)]
+pub struct User {
+ #[serde(with = "serde_bytes")]
+ pub id: Vec<u8>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub icon: Option<String>, // This has been removed from Webauthn-2
+ 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
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublicKeyCredentialDescriptor {
+ pub id: Vec<u8>,
+ 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", &ByteBuf::from(self.id.clone()))?;
+ 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_bytes(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,
+}
+
+#[cfg(test)]
+mod test {
+ use super::{
+ COSEAlgorithm, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ Transport, User,
+ };
+
+ #[test]
+ fn serialize_rp() {
+ let rp = RelyingParty {
+ id: String::from("Acme"),
+ name: None,
+ icon: 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 serialize_user() {
+ let user = User {
+ 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,
+ ],
+ icon: Some(String::from("https://pics.example.com/00/p/aBjjjpqPb.png")),
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: Some(String::from("John P. Smith")),
+ };
+
+ let payload = ser::to_vec(&user).unwrap();
+ println!("payload = {payload:?}");
+ assert_eq!(
+ payload,
+ 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, // ...
+ ]
+ );
+ }
+
+ #[test]
+ fn serialize_user_noicon_nodisplayname() {
+ let user = User {
+ 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,
+ ],
+ icon: None,
+ 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..ba9c7db3b4
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/utils.rs
@@ -0,0 +1,14 @@
+use serde::de;
+use serde_cbor::error::Result;
+use serde_cbor::Deserializer;
+
+pub fn from_slice_stream<'a, T>(slice: &'a [u8]) -> Result<(&'a [u8], T)>
+where
+ T: de::Deserialize<'a>,
+{
+ let mut deserializer = Deserializer::from_slice(slice);
+ let value = de::Deserialize::deserialize(&mut deserializer)?;
+ let rest = &slice[deserializer.byte_offset()..];
+
+ Ok((rest, value))
+}
diff --git a/third_party/rust/authenticator/src/errors.rs b/third_party/rust/authenticator/src/errors.rs
new file mode 100644
index 0000000000..25694bd72d
--- /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 {
+ EmptyAllowList,
+ HmacSecret,
+ 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..91f9b59431
--- /dev/null
+++ b/third_party/rust/authenticator/src/lib.rs
@@ -0,0 +1,111 @@
+/* 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(any(target_os = "linux"))]
+extern crate libudev;
+
+#[cfg(any(target_os = "freebsd"))]
+extern crate devd_rs;
+
+#[cfg(any(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;
+
+pub mod authenticatorservice;
+mod consts;
+mod statemachine;
+mod u2fprotocol;
+mod u2ftypes;
+
+mod manager;
+
+pub mod ctap2;
+pub use ctap2::attestation::AttestationObject;
+pub use ctap2::client_data::CollectedClientData;
+pub use ctap2::commands::client_pin::{Pin, PinError};
+pub use ctap2::commands::get_assertion::Assertion;
+pub use ctap2::commands::get_info::AuthenticatorInfo;
+pub use ctap2::GetAssertionResult;
+
+pub mod errors;
+pub mod statecallback;
+mod transport;
+mod virtualdevices;
+
+mod status_update;
+pub use status_update::*;
+
+mod crypto;
+pub use crypto::COSEAlgorithm;
+
+// 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>;
+
+#[derive(Debug)]
+pub enum RegisterResult {
+ CTAP1(Vec<u8>, u2ftypes::U2FDeviceInfo),
+ CTAP2(AttestationObject),
+}
+
+#[derive(Debug)]
+pub enum SignResult {
+ CTAP1(AppId, Vec<u8>, Vec<u8>, u2ftypes::U2FDeviceInfo),
+ CTAP2(GetAssertionResult),
+}
+
+pub type ResetResult = ();
+
+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 u2fprotocol::*;
+#[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..3a62a92252
--- /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::ResetResult>>,
+ },
+}
+
+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::ResetResult>>,
+ ) -> 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..ce1caf3e7c
--- /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 Fn(T) + Send>>>>,
+ observer: Arc<Mutex<Option<Box<dyn Fn() + 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 Fn(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 Fn() + 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..a1a1c5372f
--- /dev/null
+++ b/third_party/rust/authenticator/src/statemachine.rs
@@ -0,0 +1,1521 @@
+/* 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::consts::PARAMETER_SIZE;
+use crate::crypto::COSEAlgorithm;
+use crate::ctap2::client_data::ClientDataHash;
+use crate::ctap2::commands::client_pin::{
+ ChangeExistingPin, Pin, PinError, PinUvAuthTokenPermission, SetNewPin,
+};
+use crate::ctap2::commands::get_assertion::{
+ GetAssertion, GetAssertionOptions, GetAssertionResult,
+};
+use crate::ctap2::commands::make_credentials::{
+ dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsOptions, MakeCredentialsResult,
+};
+use crate::ctap2::commands::reset::Reset;
+use crate::ctap2::commands::{
+ repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, Request, StatusCode,
+};
+use crate::ctap2::preflight::{
+ do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2,
+};
+use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, RelyingParty, RelyingPartyWrapper, ResidentKeyRequirement,
+ RpIdHash, UserVerificationRequirement,
+};
+use crate::errors::{self, AuthenticatorError, UnsupportedOption};
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ BlinkResult, Device, DeviceBuildParameters, DeviceCommand, DeviceSelectorEvent,
+};
+use crate::transport::platform::transaction::Transaction;
+use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, Nonce};
+use crate::u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign};
+use crate::u2ftypes::U2FDevice;
+use crate::{
+ send_status, AuthenticatorTransports, InteractiveRequest, KeyHandle, RegisterFlags,
+ RegisterResult, SignFlags, SignResult, StatusPinUv, StatusUpdate,
+};
+use std::sync::mpsc::{channel, RecvError, RecvTimeoutError, Sender};
+use std::thread;
+use std::time::Duration;
+
+fn is_valid_transport(transports: crate::AuthenticatorTransports) -> bool {
+ transports.is_empty() || transports.contains(crate::AuthenticatorTransports::USB)
+}
+
+fn find_valid_key_handles<'a, F>(
+ app_ids: &'a [crate::AppId],
+ key_handles: &'a [crate::KeyHandle],
+ mut is_valid: F,
+) -> (&'a crate::AppId, Vec<&'a crate::KeyHandle>)
+where
+ F: FnMut(&Vec<u8>, &crate::KeyHandle) -> bool,
+{
+ // Try all given app_ids in order.
+ for app_id in app_ids {
+ // Find all valid key handles for the current app_id.
+ let valid_handles = key_handles
+ .iter()
+ .filter(|key_handle| is_valid(app_id, key_handle))
+ .collect::<Vec<_>>();
+
+ // If there's at least one, stop.
+ if !valid_handles.is_empty() {
+ return (app_id, valid_handles);
+ }
+ }
+
+ (&app_ids[0], vec![])
+}
+
+macro_rules! unwrap_result {
+ ($item: expr, $callback: expr) => {
+ match $item {
+ Ok(r) => r,
+ Err(e) => {
+ $callback.call(Err(e.into()));
+ return;
+ }
+ }
+ };
+}
+
+#[derive(Default)]
+pub struct StateMachine {
+ transaction: Option<Transaction>,
+}
+
+impl StateMachine {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ fn init_and_select(
+ info: DeviceBuildParameters,
+ selector: &Sender<DeviceSelectorEvent>,
+ status: &Sender<crate::StatusUpdate>,
+ ctap2_only: bool,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> 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);
+ selector.send(DeviceSelectorEvent::NotAToken(id)).ok()?;
+ return None;
+ }
+ };
+
+ // Try initializing it.
+ if let Err(e) = dev.init(Nonce::CreateRandom) {
+ warn!("error while initializing device: {}", e);
+ selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok();
+ return None;
+ }
+
+ if ctap2_only && dev.get_authenticator_info().is_none() {
+ info!("Device does not support CTAP2");
+ selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok();
+ return None;
+ }
+
+ let (tx, rx) = channel();
+ selector
+ .send(DeviceSelectorEvent::ImAToken((dev.id(), tx)))
+ .ok()?;
+ send_status(
+ status,
+ crate::StatusUpdate::DeviceAvailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+
+ // 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) => {
+ // Inform the user that there are multiple devices available.
+ // NOTE: We'll send this once per device, so the recipient should be prepared
+ // to receive this 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.
+ selector
+ .send(DeviceSelectorEvent::SelectedToken(dev.id()))
+ .ok()?;
+
+ send_status(
+ status,
+ crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
+ );
+ }
+ BlinkResult::Cancelled => {
+ info!("Device {:?} was not selected", dev.id());
+ return None;
+ }
+ }
+ }
+ Ok(DeviceCommand::Cancel) => {
+ info!("Device {:?} was not selected", dev.id());
+ return None;
+ }
+ Ok(DeviceCommand::Removed) => {
+ info!("Device {:?} was removed", dev.id());
+ send_status(
+ status,
+ crate::StatusUpdate::DeviceUnavailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ return None;
+ }
+ Ok(DeviceCommand::Continue) => {
+ // Just continue
+ send_status(
+ status,
+ crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
+ );
+ }
+ Err(_) => {
+ warn!("Error when trying to receive messages from DeviceSelector! Exiting.");
+ return None;
+ }
+ }
+ Some(dev)
+ }
+
+ fn ask_user_for_pin<U>(
+ was_invalid: bool,
+ retries: Option<u8>,
+ status: &Sender<StatusUpdate>,
+ callback: &StateCallback<crate::Result<U>>,
+ ) -> Result<Pin, ()> {
+ 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.");
+ callback.call(Err(AuthenticatorError::CancelledByUser));
+ Err(())
+ }
+ }
+ }
+
+ /// 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<T: PinUvAuthCommand + Request<V>, V>(
+ cmd: &mut T,
+ dev: &mut Device,
+ permission: PinUvAuthTokenPermission,
+ skip_uv: bool,
+ uv_req: UserVerificationRequirement,
+ ) -> 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);
+
+ // CTAP1/U2F-only devices do not support user verification, so we skip it
+ let info = match dev.get_authenticator_info() {
+ Some(info) => info,
+ None => 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())
+ .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(
+ cmd.pin(),
+ permission,
+ cmd.get_rp().id(),
+ )
+ .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 && cmd.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()?;
+ }
+ // 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(cmd.pin())
+ .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.
+ fn determine_puap_if_needed<T: PinUvAuthCommand + Request<V>, U, V>(
+ cmd: &mut T,
+ dev: &mut Device,
+ mut skip_uv: bool,
+ permission: PinUvAuthTokenPermission,
+ uv_req: UserVerificationRequirement,
+ status: &Sender<StatusUpdate>,
+ callback: &StateCallback<crate::Result<U>>,
+ alive: &dyn Fn() -> bool,
+ ) -> Result<PinUvAuthResult, ()> {
+ while alive() {
+ debug!("-----------------------------------------------------------------");
+ debug!("Getting pinUvAuthParam");
+ match Self::get_pin_uv_auth_param(cmd, dev, permission, skip_uv, uv_req) {
+ Ok(r) => {
+ return Ok(r);
+ }
+
+ Err(AuthenticatorError::PinError(PinError::PinRequired)) => {
+ if let Ok(pin) = Self::ask_user_for_pin(false, None, status, callback) {
+ cmd.set_pin(Some(pin));
+ skip_uv = true;
+ continue;
+ } else {
+ return Err(());
+ }
+ }
+ Err(AuthenticatorError::PinError(PinError::InvalidPin(retries))) => {
+ if let Ok(pin) = Self::ask_user_for_pin(true, retries, status, callback) {
+ cmd.set_pin(Some(pin));
+ continue;
+ } else {
+ return Err(());
+ }
+ }
+ 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);
+ callback.call(Err(e));
+ return Err(());
+ }
+ Err(e @ AuthenticatorError::PinError(PinError::PinBlocked)) => {
+ send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinBlocked));
+ error!("Error when determining pinAuth: {:?}", e);
+ callback.call(Err(e));
+ return Err(());
+ }
+ Err(e @ AuthenticatorError::PinError(PinError::PinNotSet)) => {
+ send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinNotSet));
+ error!("Error when determining pinAuth: {:?}", e);
+ callback.call(Err(e));
+ return Err(());
+ }
+ 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);
+ callback.call(Err(e));
+ return Err(());
+ }
+ }
+ }
+ Err(())
+ }
+
+ pub fn register(
+ &mut self,
+ timeout: u64,
+ args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) {
+ if args.use_ctap1_fallback {
+ /* Firefox uses this when security.webauthn.ctap2 is false. */
+ let mut flags = RegisterFlags::empty();
+ if args.resident_key_req == ResidentKeyRequirement::Required {
+ flags |= RegisterFlags::REQUIRE_RESIDENT_KEY;
+ }
+ if args.user_verification_req == UserVerificationRequirement::Required {
+ flags |= RegisterFlags::REQUIRE_USER_VERIFICATION;
+ }
+
+ let rp = RelyingPartyWrapper::Data(args.relying_party);
+ let application = rp.hash().as_ref().to_vec();
+ let key_handles = args
+ .exclude_list
+ .iter()
+ .map(|cred_desc| KeyHandle {
+ credential: cred_desc.id.clone(),
+ transports: AuthenticatorTransports::empty(),
+ })
+ .collect();
+ let challenge = ClientDataHash(args.client_data_hash);
+
+ self.legacy_register(
+ flags,
+ timeout,
+ challenge,
+ application,
+ key_handles,
+ status,
+ callback,
+ );
+ return;
+ }
+
+ // 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_and_select(info, &selector, &status, false, alive) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ info!("Device {:?} continues with the register process", dev.id());
+
+ // We need a copy of the arguments for this device
+ let args = args.clone();
+
+ let mut options = MakeCredentialsOptions::default();
+
+ if let Some(info) = dev.get_authenticator_info() {
+ // Check if extensions have been requested that are not supported by the device
+ if let Some(true) = args.extensions.hmac_secret {
+ if !info.supports_hmac_secret() {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::HmacSecret,
+ )));
+ return;
+ }
+ }
+
+ // 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;
+ }
+ if args.user_verification_req == UserVerificationRequirement::Required {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::UserVerification,
+ )));
+ return;
+ }
+ if !args
+ .pub_cred_params
+ .iter()
+ .any(|x| x.alg == COSEAlgorithm::ES256)
+ {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::PubCredParams,
+ )));
+ return;
+ }
+ }
+
+ let mut makecred = MakeCredentials::new(
+ ClientDataHash(args.client_data_hash),
+ RelyingPartyWrapper::Data(args.relying_party),
+ Some(args.user),
+ args.pub_cred_params,
+ args.exclude_list,
+ options,
+ args.extensions,
+ args.pin,
+ );
+
+ let mut skip_uv = false;
+ 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 = match Self::determine_puap_if_needed(
+ &mut makecred,
+ &mut dev,
+ skip_uv,
+ permissions,
+ args.user_verification_req,
+ &status,
+ &callback,
+ alive,
+ ) {
+ Ok(r) => r,
+ Err(()) => {
+ break;
+ }
+ };
+
+ // Do "pre-flight": Filter the exclude-list
+ if dev.get_authenticator_info().is_some() {
+ makecred.exclude_list = unwrap_result!(
+ do_credential_list_filtering_ctap2(
+ &mut 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(
+ &mut 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(HIDError::Command(CommandError::StatusCode(
+ StatusCode::CredentialExcluded,
+ None,
+ ))
+ .into()));
+ return;
+ }
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{makecred:?} using {pin_uv_auth_result:?}");
+ debug!("------------------------------------------------------------------");
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+ let resp = dev.send_msg_cancellable(&makecred, alive);
+ if resp.is_ok() {
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ // 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(MakeCredentialsResult(attestation)) => {
+ callback.call(Ok(RegisterResult::CTAP2(attestation)));
+ break;
+ }
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::ChannelBusy,
+ _,
+ ))) => {
+ // Channel busy. Client SHOULD retry the request after a short delay.
+ thread::sleep(Duration::from_millis(100));
+ continue;
+ }
+ Err(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 MakeCredentials
+ send_status(
+ &status,
+ StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)),
+ );
+ continue;
+ }
+ Err(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;
+ continue;
+ }
+ Err(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;
+ continue;
+ }
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::CredentialExcluded,
+ _,
+ ))) => {
+ callback.call(Err(AuthenticatorError::CredentialExcluded));
+ break;
+ }
+ Err(e) => {
+ warn!("error happened: {e}");
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ break;
+ }
+ }
+ }
+ },
+ );
+
+ 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>>,
+ ) {
+ if args.use_ctap1_fallback {
+ /* Firefox uses this when security.webauthn.ctap2 is false. */
+ let mut flags = SignFlags::empty();
+ if args.user_verification_req == UserVerificationRequirement::Required {
+ flags |= SignFlags::REQUIRE_USER_VERIFICATION;
+ }
+ let mut app_ids = vec![];
+ let rp_id = RelyingPartyWrapper::Data(RelyingParty {
+ id: args.relying_party_id,
+ ..Default::default()
+ });
+ app_ids.push(rp_id.hash().as_ref().to_vec());
+ if let Some(app_id) = args.alternate_rp_id {
+ let app_id = RelyingPartyWrapper::Data(RelyingParty {
+ id: app_id,
+ ..Default::default()
+ });
+ app_ids.push(app_id.hash().as_ref().to_vec());
+ }
+ let key_handles = args
+ .allow_list
+ .iter()
+ .map(|cred_desc| KeyHandle {
+ credential: cred_desc.id.clone(),
+ transports: AuthenticatorTransports::empty(),
+ })
+ .collect();
+ let challenge = ClientDataHash(args.client_data_hash);
+
+ self.legacy_sign(
+ flags,
+ timeout,
+ challenge,
+ app_ids,
+ key_handles,
+ status,
+ callback,
+ );
+ return;
+ }
+
+ // 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_and_select(info, &selector, &status, false, alive) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ info!("Device {:?} continues with the signing process", dev.id());
+
+ // We need a copy of the arguments for this device
+ let args = args.clone();
+
+ if let Some(info) = dev.get_authenticator_info() {
+ // Check if extensions have been requested that are not supported by the device
+ if args.extensions.hmac_secret.is_some() && !info.supports_hmac_secret() {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::HmacSecret,
+ )));
+ return;
+ }
+ } else {
+ // 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;
+ }
+ if args.allow_list.is_empty() {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::EmptyAllowList,
+ )));
+ return;
+ }
+ }
+
+ let mut get_assertion = GetAssertion::new(
+ ClientDataHash(args.client_data_hash),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: args.relying_party_id,
+ name: None,
+ icon: None,
+ }),
+ args.allow_list,
+ GetAssertionOptions {
+ user_presence: Some(args.user_presence_req),
+ user_verification: None,
+ },
+ args.extensions,
+ args.pin,
+ args.alternate_rp_id,
+ );
+
+ let mut skip_uv = false;
+ while alive() {
+ let pin_uv_auth_result = match Self::determine_puap_if_needed(
+ &mut get_assertion,
+ &mut dev,
+ skip_uv,
+ PinUvAuthTokenPermission::GetAssertion,
+ args.user_verification_req,
+ &status,
+ &callback,
+ alive,
+ ) {
+ Ok(r) => r,
+ Err(()) => {
+ return;
+ }
+ };
+
+ // 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;
+ }
+ }
+ }
+ }
+
+ // Do "pre-flight": Filter the allow-list
+ let original_allow_list_was_empty = get_assertion.allow_list.is_empty();
+ if dev.get_authenticator_info().is_some() {
+ get_assertion.allow_list = unwrap_result!(
+ do_credential_list_filtering_ctap2(
+ &mut 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(
+ &mut 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;
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{get_assertion:?} using {pin_uv_auth_result:?}");
+ debug!("------------------------------------------------------------------");
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+ let mut resp = dev.send_msg_cancellable(&get_assertion, alive);
+ if resp.is_err() {
+ // Retry with a different RP ID if one was supplied. This is intended to be
+ // used with the AppID provided in the WebAuthn FIDO AppID extension.
+ if let Some(alternate_rp_id) = get_assertion.alternate_rp_id {
+ get_assertion.rp = RelyingPartyWrapper::Data(RelyingParty {
+ id: alternate_rp_id,
+ ..Default::default()
+ });
+ get_assertion.alternate_rp_id = None;
+ resp = dev.send_msg_cancellable(&get_assertion, alive);
+ }
+ }
+ if resp.is_ok() {
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ // 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(assertions) => {
+ callback.call(Ok(SignResult::CTAP2(assertions)));
+ break;
+ }
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::ChannelBusy,
+ _,
+ ))) => {
+ // Channel busy. Client SHOULD retry the request after a short delay.
+ thread::sleep(Duration::from_millis(100));
+ continue;
+ }
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::OperationDenied,
+ _,
+ ))) 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
+ // Yes, this is a different error code than for MakeCredential.
+ send_status(
+ &status,
+ StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)),
+ );
+ continue;
+ }
+ Err(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;
+ continue;
+ }
+ Err(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;
+ continue;
+ }
+ Err(e) => {
+ warn!("error happened: {e}");
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ break;
+ }
+ }
+ }
+ },
+ );
+
+ 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_helper(
+ dev: &mut Device,
+ selector: Sender<DeviceSelectorEvent>,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ 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() {
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ // 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(())),
+ 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 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_and_select(info, &selector, &status, true, alive) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+ Self::reset_helper(&mut dev, selector, status, callback.clone(), alive);
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+
+ pub fn set_or_change_pin_helper(
+ dev: &mut Device,
+ mut current_pin: Option<Pin>,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ alive: &dyn Fn() -> bool,
+ ) {
+ let mut shared_secret = match dev.establish_shared_secret() {
+ 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 Self::ask_user_for_pin(was_invalid, retries, &status, &callback) {
+ Ok(pin) => pin,
+ _ => {
+ 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() {
+ 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)
+ };
+
+ callback.call(res);
+ }
+
+ 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_and_select(info, &selector, &status, true, alive) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ Self::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))));
+ }
+
+ pub fn legacy_register(
+ &mut self,
+ flags: crate::RegisterFlags,
+ timeout: u64,
+ challenge: ClientDataHash,
+ application: crate::AppId,
+ key_handles: Vec<crate::KeyHandle>,
+ 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, _, status, alive| {
+ // Create a new device.
+ let dev = &mut match Device::new(info) {
+ Ok(dev) => dev,
+ _ => return,
+ };
+
+ // Try initializing it.
+ if !dev.is_u2f() || !u2f_init_device(dev) {
+ return;
+ }
+
+ // We currently support none of the authenticator selection
+ // criteria because we can't ask tokens whether they do support
+ // those features. If flags are set, ignore all tokens for now.
+ //
+ // Technically, this is a ConstraintError because we shouldn't talk
+ // to this authenticator in the first place. But the result is the
+ // same anyway.
+ if !flags.is_empty() {
+ return;
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceAvailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+
+ // Iterate the exclude list and see if there are any matches.
+ // If so, we'll keep polling the device anyway to test for user
+ // consent, to be consistent with CTAP2 device behavior.
+ let excluded = key_handles.iter().any(|key_handle| {
+ is_valid_transport(key_handle.transports)
+ && u2f_is_keyhandle_valid(
+ dev,
+ challenge.as_ref(),
+ &application,
+ &key_handle.credential,
+ )
+ .unwrap_or(false) /* no match on failure */
+ });
+
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+
+ while alive() {
+ if excluded {
+ let blank = vec![0u8; PARAMETER_SIZE];
+ if u2f_register(dev, &blank, &blank).is_ok() {
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::InvalidState,
+ )));
+ break;
+ }
+ } else if let Ok(bytes) = u2f_register(dev, challenge.as_ref(), &application) {
+ let mut rp_id_hash: RpIdHash = RpIdHash([0u8; 32]);
+ rp_id_hash.0.copy_from_slice(&application);
+ let result = match MakeCredentialsResult::from_ctap1(&bytes, &rp_id_hash) {
+ Ok(MakeCredentialsResult(att_obj)) => att_obj,
+ Err(_) => {
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ )));
+ break;
+ }
+ };
+ let dev_info = dev.get_device_info();
+ send_status(&status, crate::StatusUpdate::Success { dev_info });
+ callback.call(Ok(RegisterResult::CTAP2(result)));
+ break;
+ }
+
+ // Sleep a bit before trying again.
+ thread::sleep(Duration::from_millis(100));
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceUnavailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
+ }
+
+ pub fn legacy_sign(
+ &mut self,
+ flags: crate::SignFlags,
+ timeout: u64,
+ challenge: ClientDataHash,
+ app_ids: Vec<crate::AppId>,
+ key_handles: Vec<crate::KeyHandle>,
+ 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,
+ cbc.clone(),
+ status,
+ move |info, _, status, alive| {
+ // Create a new device.
+ let dev = &mut match Device::new(info) {
+ Ok(dev) => dev,
+ _ => return,
+ };
+
+ // Try initializing it.
+ if !dev.is_u2f() || !u2f_init_device(dev) {
+ return;
+ }
+
+ // We currently don't support user verification because we can't
+ // ask tokens whether they do support that. If the flag is set,
+ // ignore all tokens for now.
+ //
+ // Technically, this is a ConstraintError because we shouldn't talk
+ // to this authenticator in the first place. But the result is the
+ // same anyway.
+ if !flags.is_empty() {
+ return;
+ }
+
+ // For each appId, try all key handles. If there's at least one
+ // valid key handle for an appId, we'll use that appId below.
+ let (app_id, valid_handles) =
+ find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| {
+ u2f_is_keyhandle_valid(
+ dev,
+ challenge.as_ref(),
+ app_id,
+ &key_handle.credential,
+ )
+ .unwrap_or(false) /* no match on failure */
+ });
+
+ // Aggregate distinct transports from all given credentials.
+ let transports = key_handles
+ .iter()
+ .fold(crate::AuthenticatorTransports::empty(), |t, k| {
+ t | k.transports
+ });
+
+ // We currently only support USB. If the RP specifies transports
+ // and doesn't include USB it's probably lying.
+ if !is_valid_transport(transports) {
+ return;
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceAvailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+
+ send_status(&status, crate::StatusUpdate::PresenceRequired);
+
+ 'outer: while alive() {
+ // If the device matches none of the given key handles
+ // then just make it blink with bogus data.
+ if valid_handles.is_empty() {
+ let blank = vec![0u8; PARAMETER_SIZE];
+ if u2f_register(dev, &blank, &blank).is_ok() {
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::InvalidState,
+ )));
+ break;
+ }
+ } else {
+ // Otherwise, try to sign.
+ for key_handle in &valid_handles {
+ if let Ok(bytes) =
+ u2f_sign(dev, challenge.as_ref(), app_id, &key_handle.credential)
+ {
+ let pkcd = PublicKeyCredentialDescriptor {
+ id: key_handle.credential.clone(),
+ transports: vec![],
+ };
+ let mut rp_id_hash: RpIdHash = RpIdHash([0u8; 32]);
+ rp_id_hash.0.copy_from_slice(app_id);
+ let result = match GetAssertionResult::from_ctap1(
+ &bytes,
+ &rp_id_hash,
+ &pkcd,
+ ) {
+ Ok(assertions) => assertions,
+ Err(_) => {
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ )));
+ break 'outer;
+ }
+ };
+ let dev_info = dev.get_device_info();
+ send_status(&status, crate::StatusUpdate::Success { dev_info });
+ callback.call(Ok(SignResult::CTAP2(result)));
+ break 'outer;
+ }
+ }
+ }
+
+ // Sleep a bit before trying again.
+ thread::sleep(Duration::from_millis(100));
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceUnavailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, |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::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_and_select(info, &selector, &status, true, alive) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ 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((
+ tx,
+ dev.get_device_info(),
+ dev.get_authenticator_info().cloned(),
+ )),
+ );
+ while alive() {
+ match rx.recv_timeout(Duration::from_millis(400)) {
+ Ok(InteractiveRequest::Reset) => {
+ Self::reset_helper(&mut dev, selector, status, callback.clone(), alive);
+ }
+ Ok(InteractiveRequest::ChangePIN(curr_pin, new_pin)) => {
+ Self::set_or_change_pin_helper(
+ &mut dev,
+ Some(curr_pin),
+ new_pin,
+ status,
+ callback.clone(),
+ alive,
+ );
+ }
+ Ok(InteractiveRequest::SetPIN(pin)) => {
+ Self::set_or_change_pin_helper(
+ &mut dev,
+ None,
+ pin,
+ status,
+ callback.clone(),
+ alive,
+ );
+ }
+ 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..f01cbd0cea
--- /dev/null
+++ b/third_party/rust/authenticator/src/status_update.rs
@@ -0,0 +1,89 @@
+use super::{u2ftypes, Pin};
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use serde::{Deserialize, Serialize as DeriveSer, Serializer};
+use std::sync::mpsc::Sender;
+
+#[derive(Debug, Deserialize, DeriveSer)]
+pub enum InteractiveRequest {
+ Reset,
+ ChangePIN(Pin, Pin),
+ SetPIN(Pin),
+}
+
+// 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 StatusUpdate {
+ /// Device found
+ DeviceAvailable { dev_info: u2ftypes::U2FDeviceInfo },
+ /// Device got removed
+ DeviceUnavailable { dev_info: u2ftypes::U2FDeviceInfo },
+ /// We're waiting for the user to touch their token
+ PresenceRequired,
+ /// We successfully finished the register or sign request
+ Success { dev_info: u2ftypes::U2FDeviceInfo },
+ /// 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, once a device was selected (either automatically or by user-interaction)
+ /// and the register or signing process continues with this device
+ DeviceSelected(u2ftypes::U2FDeviceInfo),
+ /// Sent when a token was selected for interactive management
+ InteractiveManagement(
+ (
+ Sender<InteractiveRequest>,
+ u2ftypes::U2FDeviceInfo,
+ Option<AuthenticatorInfo>,
+ ),
+ ),
+}
+
+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..a0ce4ccb57
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/device_selector.rs
@@ -0,0 +1,475 @@
+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;
+
+// This import is used, but Rust 1.68 gives a warning
+#[allow(unused_imports)]
+use crate::u2ftypes::U2FDevice;
+
+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},
+ u2ftypes::U2FDeviceInfo,
+ };
+
+ 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,
+ }
+ }
+
+ fn make_device_simple_u2f(dev: &mut Device) {
+ dev.set_device_info(gen_info(dev.id()));
+ dev.create_channel();
+ }
+
+ fn make_device_with_pin(dev: &mut Device) {
+ dev.set_device_info(gen_info(dev.id()));
+ 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..7a350c067e
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/device.rs
@@ -0,0 +1,217 @@
+/* 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::{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, HIDError, SharedSecret};
+use crate::u2ftypes::{U2FDevice, 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>,
+}
+
+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 U2FDevice for Device {
+ 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 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,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ 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);
+ }
+}
+
+impl FidoDevice for Device {}
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..a789344754
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/hid.rs
@@ -0,0 +1,153 @@
+use crate::consts::{HIDCmd, CID_BROADCAST};
+use crate::crypto::SharedSecret;
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::{errors::HIDError, Nonce};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp};
+use rand::{thread_rng, RngCore};
+use std::cmp::Eq;
+use std::fmt;
+use std::hash::Hash;
+use std::io;
+
+pub trait HIDDevice
+where
+ Self: io::Read,
+ Self: io::Write,
+ Self: U2FDevice,
+ Self: Sized,
+ Self: fmt::Debug,
+{
+ 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 initialized(&self) -> bool;
+ // Check if the device is actually a token
+ fn is_u2f(&mut self) -> bool;
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>;
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo);
+ fn set_shared_secret(&mut self, secret: SharedSecret);
+ fn get_shared_secret(&self) -> Option<&SharedSecret>;
+
+ // Initialize on a protocol-level
+ fn initialize(&mut self, noncecmd: Nonce) -> Result<(), HIDError> {
+ if self.initialized() {
+ return Ok(());
+ }
+
+ let nonce = match noncecmd {
+ Nonce::Use(x) => x,
+ Nonce::CreateRandom => {
+ let mut nonce = [0u8; 8];
+ thread_rng().fill_bytes(&mut nonce);
+ nonce
+ }
+ };
+
+ // Send Init to broadcast address to create a new channel
+ self.set_cid(CID_BROADCAST);
+ let (cmd, raw) = self.sendrecv(HIDCmd::Init, &nonce, &|| true)?;
+ if cmd != HIDCmd::Init {
+ return Err(HIDError::DeviceError);
+ }
+
+ let rsp = U2FHIDInitResp::read(&raw, &nonce)?;
+ // Get 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>)> {
+ let cmd: u8 = cmd.into();
+ self.u2f_write(cmd, send)?;
+ loop {
+ let (cmd, data) = self.u2f_read()?;
+ if cmd != HIDCmd::Keepalive {
+ 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_authenticator_info().is_some() {
+ 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))
+ }
+}
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..2438eb730d
--- /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(any(target_os = "linux"))]
+use std::io;
+use std::mem;
+
+use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID};
+#[cfg(any(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(any(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..bad487c53c
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/device.rs
@@ -0,0 +1,163 @@
+/* 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::CID_BROADCAST;
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::{hidraw, monitor};
+use crate::transport::{FidoDevice, HIDError, SharedSecret};
+use crate::u2ftypes::{U2FDevice, 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>,
+}
+
+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 U2FDevice for Device {
+ 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 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,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ 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);
+ }
+}
+
+impl FidoDevice for Device {}
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..82aabc6301
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs
@@ -0,0 +1,51 @@
+#![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");
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_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..0e55b92e96
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/device.rs
@@ -0,0 +1,209 @@
+/* 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::{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, HIDError, SharedSecret};
+use crate::u2ftypes::{U2FDevice, 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>,
+}
+
+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 U2FDevice for Device {
+ 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 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,
+ })
+ }
+
+ fn initialized(&self) -> bool {
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.device_ref
+ }
+
+ 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);
+ }
+}
+
+impl FidoDevice for Device {}
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..c22e53b3bd
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mock/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/. */
+use crate::consts::CID_BROADCAST;
+use crate::crypto::SharedSecret;
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::device_selector::DeviceCommand;
+use crate::transport::{hid::HIDDevice, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+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>>,
+}
+
+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 create_channel(&mut self) {
+ let (tx, rx) = channel();
+ self.sender = Some(tx);
+ self.receiver = Some(rx);
+ }
+}
+
+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 U2FDevice for Device {
+ 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);
+ }
+}
+
+impl HIDDevice for Device {
+ type Id = String;
+ type BuildParameters = &'static str; // None used
+
+ 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 set_shared_secret(&mut self, _: SharedSecret) {
+ // Nothing
+ }
+ fn get_shared_secret(&self) -> std::option::Option<&SharedSecret> {
+ None
+ }
+
+ 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,
+ })
+ }
+
+ fn initialized(&self) -> bool {
+ self.get_cid() != &CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.id.clone()
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ self.sender.is_some()
+ }
+}
+
+impl FidoDevice for Device {}
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..91ec7fe1d7
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mod.rs
@@ -0,0 +1,334 @@
+use crate::consts::{Capability, HIDCmd};
+use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret};
+use crate::ctap2::commands::client_pin::{
+ GetKeyAgreement, GetPinToken, GetPinUvAuthTokenUsingPinWithPermissions,
+ GetPinUvAuthTokenUsingUvWithPermissions, PinUvAuthTokenPermission,
+};
+use crate::ctap2::commands::get_info::{AuthenticatorVersion, GetInfo};
+use crate::ctap2::commands::get_version::GetVersion;
+use crate::ctap2::commands::make_credentials::dummy_make_credentials_cmd;
+use crate::ctap2::commands::selection::Selection;
+use crate::ctap2::commands::{
+ CommandError, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode,
+};
+use crate::transport::device_selector::BlinkResult;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::hid::HIDDevice;
+use crate::util::io_err;
+use crate::Pin;
+use std::convert::TryFrom;
+use std::thread;
+use std::time::Duration;
+
+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)]
+pub enum Nonce {
+ CreateRandom,
+ Use([u8; 8]),
+}
+
+// TODO(MS): This is the lazy way: FidoDevice currently only extends HIDDevice by more functions,
+// but the goal is to remove U2FDevice entirely and copy over the trait-definition here
+pub trait FidoDevice: HIDDevice {
+ fn send_msg<Out, Req: Request<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: Request<Out>>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Out, HIDError> {
+ if !self.initialized() {
+ return Err(HIDError::DeviceNotInitialized);
+ }
+
+ if self.get_authenticator_info().is_some() {
+ self.send_cbor_cancellable(msg, keep_alive)
+ } else {
+ self.send_ctap1_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);
+
+ let mut data = msg.wire_format()?;
+ let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1);
+ // CTAP2 command
+ buf.push(Req::command() as u8);
+ // payload
+ buf.append(&mut data);
+ let buf = buf;
+
+ let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf, keep_alive)?;
+ debug!(
+ "got from Device {:?} status={:?}: {:?}",
+ self.id(),
+ cmd,
+ resp
+ );
+ 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);
+ 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)?;
+ debug!(
+ "got from Device {:?} status={:?}: {:?}",
+ self.id(),
+ cmd,
+ data
+ );
+ 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(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,
+ )))
+ }
+
+ // This is ugly as we have 2 init-functions now, but the fastest way currently.
+ fn init(&mut self, nonce: Nonce) -> Result<(), HIDError> {
+ <Self as HIDDevice>::initialize(self, nonce)?;
+
+ // If the device has the CBOR capability flag, then we'll check
+ // for CTAP2 support by sending an authenticatorGetInfo command.
+ // We're not aware of any CTAP2 devices that fail to set the CBOR
+ // capability flag, but we may need to rework this in the future.
+ if self.get_device_info().cap_flags.contains(Capability::CBOR) {
+ let command = GetInfo::default();
+ if let Ok(info) = self.send_cbor(&command) {
+ debug!("{:?}: {:?}", self.id(), info);
+ if info.max_supported_version() != AuthenticatorVersion::U2F_V2 {
+ // Device supports CTAP2
+ self.set_authenticator_info(info);
+ return Ok(());
+ }
+ }
+ // An error from GetInfo might indicate that we're talking
+ // to a CTAP1 device that mistakenly claimed the CBOR capability,
+ // so we fallthrough here.
+ }
+ // 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_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) -> Result<SharedSecret, HIDError> {
+ // CTAP1 devices don't support establishing a shared secret
+ let info = match self.get_authenticator_info() {
+ Some(info) => info,
+ None => 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);
+ let device_key_agreement = self.send_cbor(&pin_command)?;
+ let shared_secret = device_key_agreement.shared_secret()?;
+ self.set_shared_secret(shared_secret.clone());
+ Ok(shared_secret)
+ }
+
+ /// CTAP 2.0-only version:
+ /// "Getting pinUvAuthToken using getPinToken (superseded)"
+ fn get_pin_token(&mut self, pin: &Option<Pin>) -> 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()?;
+
+ let pin_command = GetPinToken::new(&shared_secret, pin);
+ let pin_token = self.send_cbor(&pin_command)?;
+
+ Ok(pin_token)
+ }
+
+ fn get_pin_uv_auth_token_using_uv_with_permissions(
+ &mut self,
+ permission: PinUvAuthTokenPermission,
+ rp_id: Option<&String>,
+ ) -> Result<PinUvAuthToken, HIDError> {
+ // Explicitly not reusing the shared secret here
+ let shared_secret = self.establish_shared_secret()?;
+ let pin_command = GetPinUvAuthTokenUsingUvWithPermissions::new(
+ &shared_secret,
+ permission,
+ rp_id.cloned(),
+ );
+ let pin_auth_token = self.send_cbor(&pin_command)?;
+
+ Ok(pin_auth_token)
+ }
+
+ fn get_pin_uv_auth_token_using_pin_with_permissions(
+ &mut self,
+ pin: &Option<Pin>,
+ permission: PinUvAuthTokenPermission,
+ rp_id: Option<&String>,
+ ) -> 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()?;
+ let pin_command = GetPinUvAuthTokenUsingPinWithPermissions::new(
+ &shared_secret,
+ pin,
+ permission,
+ rp_id.cloned(),
+ );
+ let pin_auth_token = self.send_cbor(&pin_command)?;
+
+ Ok(pin_auth_token)
+ }
+}
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..c93aee8d6a
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/device.rs
@@ -0,0 +1,230 @@
+/* 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::{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, HIDError, SharedSecret};
+use crate::u2ftypes::{U2FDevice, 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>,
+}
+
+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 U2FDevice for Device {
+ 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 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,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ 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);
+ }
+}
+
+impl FidoDevice for Device {}
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..fe4d6a642e
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/device.rs
@@ -0,0 +1,206 @@
+/* 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::{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, HIDError, SharedSecret};
+use crate::u2ftypes::{U2FDevice, 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>,
+}
+
+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 U2FDevice for Device {
+ 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 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,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ 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);
+ }
+}
+
+impl FidoDevice for Device {}
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..9c5a412a95
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/stub/device.rs
@@ -0,0 +1,101 @@
+/* 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;
+use crate::transport::{HIDError, SharedSecret};
+use crate::u2ftypes::{U2FDevice, 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> {
+ panic!("not implemented");
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ panic!("not implemented");
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ panic!("not implemented");
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid(&self) -> &[u8; 4] {
+ panic!("not implemented");
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ panic!("not implemented");
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ panic!("not implemented");
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ panic!("not implemented");
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ panic!("not implemented")
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ panic!("not implemented")
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ panic!("not implemented")
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = PathBuf;
+ type Id = PathBuf;
+
+ fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
+ unimplemented!();
+ }
+
+ fn initialized(&self) -> bool {
+ unimplemented!();
+ }
+
+ fn id(&self) -> Self::Id {
+ 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!()
+ }
+}
+
+impl FidoDevice for Device {}
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..1037c25a20
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/device.rs
@@ -0,0 +1,154 @@
+/* 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::{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, HIDError, SharedSecret};
+use crate::u2ftypes::{U2FDevice, 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>,
+}
+
+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 U2FDevice for Device {
+ 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 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,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ 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);
+ }
+}
+
+impl FidoDevice for Device {}
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/u2fprotocol.rs b/third_party/rust/authenticator/src/u2fprotocol.rs
new file mode 100644
index 0000000000..53405f0021
--- /dev/null
+++ b/third_party/rust/authenticator/src/u2fprotocol.rs
@@ -0,0 +1,398 @@
+/* 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::needless_lifetimes))]
+
+extern crate std;
+
+use rand::{thread_rng, RngCore};
+use std::ffi::CString;
+use std::io;
+use std::io::{Read, Write};
+
+use crate::consts::*;
+use crate::u2ftypes::*;
+use crate::util::io_err;
+
+////////////////////////////////////////////////////////////////////////
+// Device Commands
+////////////////////////////////////////////////////////////////////////
+
+pub fn u2f_init_device<T>(dev: &mut T) -> bool
+where
+ T: U2FDevice + Read + Write,
+{
+ let mut nonce = [0u8; 8];
+ thread_rng().fill_bytes(&mut nonce);
+
+ // Initialize the device and check its version.
+ init_device(dev, &nonce).is_ok() && is_v2_device(dev).unwrap_or(false)
+}
+
+pub fn u2f_register<T>(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result<Vec<u8>>
+where
+ T: U2FDevice + Read + Write,
+{
+ if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Invalid parameter sizes",
+ ));
+ }
+
+ let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE);
+ register_data.extend(challenge);
+ register_data.extend(application);
+
+ let flags = U2F_REQUEST_USER_PRESENCE;
+ let (resp, status) = send_ctap1(dev, U2F_REGISTER, flags, &register_data)?;
+ status_word_to_result(status, resp)
+}
+
+pub fn u2f_sign<T>(
+ dev: &mut T,
+ challenge: &[u8],
+ application: &[u8],
+ key_handle: &[u8],
+) -> io::Result<Vec<u8>>
+where
+ T: U2FDevice + Read + Write,
+{
+ if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Invalid parameter sizes",
+ ));
+ }
+
+ if key_handle.len() > 256 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Key handle too large",
+ ));
+ }
+
+ let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len());
+ sign_data.extend(challenge);
+ sign_data.extend(application);
+ sign_data.push(key_handle.len() as u8);
+ sign_data.extend(key_handle);
+
+ let flags = U2F_REQUEST_USER_PRESENCE;
+ let (resp, status) = send_ctap1(dev, U2F_AUTHENTICATE, flags, &sign_data)?;
+ status_word_to_result(status, resp)
+}
+
+pub fn u2f_is_keyhandle_valid<T>(
+ dev: &mut T,
+ challenge: &[u8],
+ application: &[u8],
+ key_handle: &[u8],
+) -> io::Result<bool>
+where
+ T: U2FDevice + Read + Write,
+{
+ if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Invalid parameter sizes",
+ ));
+ }
+
+ if key_handle.len() >= 256 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Key handle too large",
+ ));
+ }
+
+ let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len());
+ sign_data.extend(challenge);
+ sign_data.extend(application);
+ sign_data.push(key_handle.len() as u8);
+ sign_data.extend(key_handle);
+
+ let flags = U2F_CHECK_IS_REGISTERED;
+ let (_, status) = send_ctap1(dev, U2F_AUTHENTICATE, flags, &sign_data)?;
+ Ok(status == SW_CONDITIONS_NOT_SATISFIED)
+}
+
+////////////////////////////////////////////////////////////////////////
+// Internal Device Commands
+////////////////////////////////////////////////////////////////////////
+
+fn init_device<T>(dev: &mut T, nonce: &[u8]) -> io::Result<()>
+where
+ T: U2FDevice + Read + Write,
+{
+ assert_eq!(nonce.len(), INIT_NONCE_SIZE);
+ // Send Init to broadcast address to create a new channel
+ let raw = sendrecv(dev, HIDCmd::Init, nonce)?;
+ let rsp = U2FHIDInitResp::read(&raw, nonce)?;
+ // Get the new Channel ID
+ dev.set_cid(rsp.cid);
+
+ let vendor = dev
+ .get_property("Manufacturer")
+ .unwrap_or_else(|_| String::from("Unknown Vendor"));
+ let product = dev
+ .get_property("Product")
+ .unwrap_or_else(|_| String::from("Unknown Device"));
+
+ dev.set_device_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,
+ });
+
+ Ok(())
+}
+
+fn is_v2_device<T>(dev: &mut T) -> io::Result<bool>
+where
+ T: U2FDevice + Read + Write,
+{
+ let (data, status) = send_ctap1(dev, U2F_VERSION, 0x00, &[])?;
+ let actual = CString::new(data)?;
+ let expected = CString::new("U2F_V2")?;
+ status_word_to_result(status, actual == expected)
+}
+
+////////////////////////////////////////////////////////////////////////
+// Error Handling
+////////////////////////////////////////////////////////////////////////
+
+fn status_word_to_result<T>(status: [u8; 2], val: T) -> io::Result<T> {
+ use self::io::ErrorKind::{InvalidData, InvalidInput};
+
+ match status {
+ SW_NO_ERROR => Ok(val),
+ SW_WRONG_DATA => Err(io::Error::new(InvalidData, "wrong data")),
+ SW_WRONG_LENGTH => Err(io::Error::new(InvalidInput, "wrong length")),
+ SW_CONDITIONS_NOT_SATISFIED => Err(io_err("conditions not satisfied")),
+ _ => Err(io_err(&format!("failed with status {status:?}"))),
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Device Communication Functions
+////////////////////////////////////////////////////////////////////////
+
+pub fn sendrecv<T>(dev: &mut T, cmd: HIDCmd, send: &[u8]) -> io::Result<Vec<u8>>
+where
+ T: U2FDevice + Read + Write,
+{
+ // Send initialization packet.
+ let mut count = U2FHIDInit::write(dev, cmd.into(), send)?;
+
+ // Send continuation packets.
+ let mut sequence = 0u8;
+ while count < send.len() {
+ count += U2FHIDCont::write(dev, sequence, &send[count..])?;
+ sequence += 1;
+ }
+
+ // 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 (_, mut data) = U2FHIDInit::read(dev)?;
+
+ let mut sequence = 0u8;
+ while data.len() < data.capacity() {
+ let max = data.capacity() - data.len();
+ data.extend_from_slice(&U2FHIDCont::read(dev, sequence, max)?);
+ sequence += 1;
+ }
+
+ Ok(data)
+}
+
+fn send_ctap1<T>(dev: &mut T, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec<u8>, [u8; 2])>
+where
+ T: U2FDevice + Read + Write,
+{
+ let apdu = CTAP1RequestAPDU::serialize(cmd, p1, send)?;
+ let mut data = sendrecv(dev, HIDCmd::Msg, &apdu)?;
+
+ if data.len() < 2 {
+ return Err(io_err("unexpected response"));
+ }
+
+ let split_at = data.len() - 2;
+ let status = data.split_off(split_at);
+ Ok((data, [status[0], status[1]]))
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+pub(crate) mod tests {
+ use super::{init_device, is_v2_device, send_ctap1, sendrecv, U2FDevice};
+ use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR};
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use crate::u2ftypes::U2FDeviceInfo;
+ use rand::{thread_rng, RngCore};
+
+ #[test]
+ fn test_init_device() {
+ let mut device = Device::new("u2fprotocol").unwrap();
+ let nonce = vec![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![HIDCmd::Init.into(), 0x00, 0x11]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ msg.extend_from_slice(&cid); // new channel id
+ msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags
+ device.add_read(&msg, 0);
+
+ init_device(&mut device, &nonce).unwrap();
+ assert_eq!(device.get_cid(), &cid);
+
+ let dev_info = device.get_device_info();
+ assert_eq!(dev_info.version_interface, 0x02);
+ assert_eq!(dev_info.version_major, 0x04);
+ assert_eq!(dev_info.version_minor, 0x01);
+ assert_eq!(dev_info.version_build, 0x08);
+ assert_eq!(dev_info.cap_flags, Capability::WINK); // 0x01
+ }
+
+ #[test]
+ fn test_get_version() {
+ let mut device = Device::new("u2fprotocol").unwrap();
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ device.set_cid(cid);
+
+ let 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,
+ };
+ device.set_device_info(info);
+
+ // ctap1.0 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);
+
+ let res = is_v2_device(&mut device).expect("Failed to get version");
+ assert!(res);
+ }
+
+ #[test]
+ fn test_sendrecv_multiple() {
+ let mut device = Device::new("u2fprotocol").unwrap();
+ let cid = [0x01, 0x02, 0x03, 0x04];
+ device.set_cid(cid);
+
+ // init packet
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Ping.into(), 0x00, 0xe4]); // cmd + length = 228
+ // write msg, append [1u8; 57], 171 bytes remain
+ device.add_write(&msg, 1);
+ device.add_read(&msg, 1);
+
+ // cont packet
+ let mut msg = cid.to_vec();
+ msg.push(0x00); // seq = 0
+ // write msg, append [1u8; 59], 112 bytes remaining
+ device.add_write(&msg, 1);
+ device.add_read(&msg, 1);
+
+ // cont packet
+ let mut msg = cid.to_vec();
+ msg.push(0x01); // seq = 1
+ // write msg, append [1u8; 59], 53 bytes remaining
+ device.add_write(&msg, 1);
+ device.add_read(&msg, 1);
+
+ // cont packet
+ let mut msg = cid.to_vec();
+ msg.push(0x02); // seq = 2
+ msg.extend_from_slice(&[1u8; 53]);
+ // write msg, append remaining 53 bytes.
+ device.add_write(&msg, 0);
+ device.add_read(&msg, 0);
+
+ let data = [1u8; 228];
+ let d = sendrecv(&mut device, HIDCmd::Ping, &data).unwrap();
+ assert_eq!(d.len(), 228);
+ assert_eq!(d, &data[..]);
+ }
+
+ #[test]
+ fn test_sendapdu() {
+ let cid = [0x01, 0x02, 0x03, 0x04];
+ let data = [0x01, 0x02, 0x03, 0x04, 0x05];
+ let mut device = Device::new("u2fprotocol").unwrap();
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ // sendrecv header
+ msg.extend(vec![HIDCmd::Msg.into(), 0x00, 0x0e]); // len = 14
+ // apdu header
+ msg.extend(vec![
+ 0x00,
+ HIDCmd::Ping.into(),
+ 0xaa,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x05,
+ ]);
+ // apdu data
+ msg.extend_from_slice(&data);
+ device.add_write(&msg, 0);
+
+ // Send data back
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Msg.into(), 0x00, 0x07]);
+ msg.extend_from_slice(&data);
+ msg.extend_from_slice(&SW_NO_ERROR);
+ device.add_read(&msg, 0);
+
+ let (result, status) = send_ctap1(&mut device, HIDCmd::Ping.into(), 0xaa, &data).unwrap();
+ assert_eq!(result, &data);
+ assert_eq!(status, SW_NO_ERROR);
+ }
+
+ #[test]
+ fn test_get_property() {
+ let device = Device::new("u2fprotocol").unwrap();
+
+ assert_eq!(device.get_property("a").unwrap(), "a not implemented");
+ }
+}
diff --git a/third_party/rust/authenticator/src/u2ftypes.rs b/third_party/rust/authenticator/src/u2ftypes.rs
new file mode 100644
index 0000000000..4a2584f9ce
--- /dev/null
+++ b/third_party/rust/authenticator/src/u2ftypes.rs
@@ -0,0 +1,363 @@
+/* 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::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, ""));
+ }
+}
+
+// Trait for representing U2F HID Devices. Requires getters/setters for the
+// channel ID, created during device initialization.
+pub trait U2FDevice {
+ fn get_cid(&self) -> &[u8; 4];
+ fn set_cid(&mut self, cid: [u8; 4]);
+
+ fn in_rpt_size(&self) -> usize;
+ fn in_init_data_size(&self) -> usize {
+ self.in_rpt_size() - INIT_HEADER_SIZE
+ }
+ fn in_cont_data_size(&self) -> usize {
+ self.in_rpt_size() - CONT_HEADER_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize;
+ fn out_init_data_size(&self) -> usize {
+ self.out_rpt_size() - INIT_HEADER_SIZE
+ }
+ fn out_cont_data_size(&self) -> usize {
+ self.out_rpt_size() - CONT_HEADER_SIZE
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String>;
+ fn get_device_info(&self) -> U2FDeviceInfo;
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo);
+}
+
+// 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>(dev: &mut T) -> io::Result<(HIDCmd, Vec<u8>)>
+ where
+ T: U2FDevice + io::Read,
+ {
+ 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 = cmp::min(cap, dev.in_init_data_size());
+ data.extend_from_slice(&frame[7..7 + len]);
+
+ Ok((cmd, data))
+ }
+
+ pub fn write<T>(dev: &mut T, cmd: u8, data: &[u8]) -> io::Result<usize>
+ where
+ T: U2FDevice + io::Write,
+ {
+ 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 = cmp::min(data.len(), dev.out_init_data_size());
+ 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>(dev: &mut T, seq: u8, max: usize) -> io::Result<Vec<u8>>
+ where
+ T: U2FDevice + io::Read,
+ {
+ 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 = cmp::min(max, dev.in_cont_data_size());
+ Ok(frame[5..5 + max].to_vec())
+ }
+
+ pub fn write<T>(dev: &mut T, seq: u8, data: &[u8]) -> io::Result<usize>
+ where
+ T: U2FDevice + io::Write,
+ {
+ let mut frame = vec![0u8; dev.out_rpt_size() + 1];
+ frame[1..5].copy_from_slice(dev.get_cid());
+ frame[5] = seq;
+
+ let count = cmp::min(data.len(), dev.out_cont_data_size());
+ 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..b73f9de38b
--- /dev/null
+++ b/third_party/rust/authenticator/src/util.rs
@@ -0,0 +1,75 @@
+/* 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) => {
+ 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/src/virtualdevices/mod.rs b/third_party/rust/authenticator/src/virtualdevices/mod.rs
new file mode 100644
index 0000000000..5c0a9d39fc
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/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/. */
+
+#[cfg(feature = "webdriver")]
+pub mod webdriver;
+
+pub mod software_u2f;
diff --git a/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs
new file mode 100644
index 0000000000..cda4ca82fc
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs
@@ -0,0 +1,65 @@
+/* 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;
+use crate::{RegisterResult, SignResult};
+
+pub struct SoftwareU2FToken {}
+
+// This is simply for platforms that aren't using the U2F Token, usually for builds
+// without --feature webdriver
+#[allow(dead_code)]
+
+impl SoftwareU2FToken {
+ pub fn new() -> SoftwareU2FToken {
+ Self {}
+ }
+
+ pub fn register(
+ &self,
+ _flags: crate::RegisterFlags,
+ _timeout: u64,
+ _challenge: Vec<u8>,
+ _application: crate::AppId,
+ _key_handles: Vec<crate::KeyHandle>,
+ ) -> crate::Result<crate::RegisterResult> {
+ Ok(RegisterResult::CTAP1(vec![0u8; 16], self.dev_info()))
+ }
+
+ /// The implementation of this method must return quickly and should
+ /// report its status via the status and callback methods
+ pub fn sign(
+ &self,
+ _flags: crate::SignFlags,
+ _timeout: u64,
+ _challenge: Vec<u8>,
+ _app_ids: Vec<crate::AppId>,
+ _key_handles: Vec<crate::KeyHandle>,
+ ) -> crate::Result<crate::SignResult> {
+ Ok(SignResult::CTAP1(
+ vec![0u8; 0],
+ vec![0u8; 0],
+ vec![0u8; 0],
+ self.dev_info(),
+ ))
+ }
+
+ pub fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo {
+ crate::u2ftypes::U2FDeviceInfo {
+ vendor_name: b"Mozilla".to_vec(),
+ device_name: b"Authenticator Webdriver Token".to_vec(),
+ version_interface: 0,
+ version_major: 1,
+ version_minor: 2,
+ version_build: 3,
+ cap_flags: Capability::empty(),
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs
new file mode 100644
index 0000000000..b1ef27d813
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/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/. */
+
+mod testtoken;
+mod virtualmanager;
+mod web_api;
+
+pub use virtualmanager::VirtualManager;
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
new file mode 100644
index 0000000000..9bf60bbaf5
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
@@ -0,0 +1,140 @@
+/* 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::virtualdevices::software_u2f::SoftwareU2FToken;
+use crate::{RegisterFlags, RegisterResult, SignFlags, SignResult};
+
+pub enum TestWireProtocol {
+ CTAP1,
+ CTAP2,
+}
+
+impl TestWireProtocol {
+ pub fn to_webdriver_string(&self) -> String {
+ match self {
+ TestWireProtocol::CTAP1 => "ctap1/u2f".to_string(),
+ TestWireProtocol::CTAP2 => "ctap2".to_string(),
+ }
+ }
+}
+
+pub struct TestTokenCredential {
+ pub credential: Vec<u8>,
+ pub privkey: Vec<u8>,
+ pub user_handle: Vec<u8>,
+ pub sign_count: u64,
+ pub is_resident_credential: bool,
+ pub rp_id: String,
+}
+
+pub struct TestToken {
+ pub id: u64,
+ pub protocol: TestWireProtocol,
+ pub transport: String,
+ pub is_user_consenting: bool,
+ pub has_user_verification: bool,
+ pub is_user_verified: bool,
+ pub has_resident_key: bool,
+ pub u2f_impl: Option<SoftwareU2FToken>,
+ pub credentials: Vec<TestTokenCredential>,
+}
+
+impl TestToken {
+ pub fn new(
+ id: u64,
+ protocol: TestWireProtocol,
+ transport: String,
+ is_user_consenting: bool,
+ has_user_verification: bool,
+ is_user_verified: bool,
+ has_resident_key: bool,
+ ) -> TestToken {
+ match protocol {
+ TestWireProtocol::CTAP1 => Self {
+ id,
+ protocol,
+ transport,
+ is_user_consenting,
+ has_user_verification,
+ is_user_verified,
+ has_resident_key,
+ u2f_impl: Some(SoftwareU2FToken::new()),
+ credentials: Vec::new(),
+ },
+ _ => unreachable!(),
+ }
+ }
+
+ pub fn insert_credential(
+ &mut self,
+ credential: &[u8],
+ privkey: &[u8],
+ rp_id: String,
+ is_resident_credential: bool,
+ user_handle: &[u8],
+ sign_count: u64,
+ ) {
+ let c = TestTokenCredential {
+ credential: credential.to_vec(),
+ privkey: privkey.to_vec(),
+ rp_id,
+ is_resident_credential,
+ user_handle: user_handle.to_vec(),
+ sign_count,
+ };
+
+ match self
+ .credentials
+ .binary_search_by_key(&credential, |probe| &probe.credential)
+ {
+ Ok(_) => {}
+ Err(idx) => self.credentials.insert(idx, c),
+ }
+ }
+
+ pub fn delete_credential(&mut self, credential: &[u8]) -> bool {
+ debug!("Asking to delete credential",);
+ if let Ok(idx) = self
+ .credentials
+ .binary_search_by_key(&credential, |probe| &probe.credential)
+ {
+ debug!("Asking to delete credential from idx {}", idx);
+ self.credentials.remove(idx);
+ return true;
+ }
+
+ false
+ }
+
+ pub fn register(&self) -> crate::Result<RegisterResult> {
+ if self.u2f_impl.is_some() {
+ return self.u2f_impl.as_ref().unwrap().register(
+ RegisterFlags::empty(),
+ 10_000,
+ vec![0; 32],
+ vec![0; 32],
+ vec![],
+ );
+ }
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ))
+ }
+
+ pub fn sign(&self) -> crate::Result<SignResult> {
+ if self.u2f_impl.is_some() {
+ return self.u2f_impl.as_ref().unwrap().sign(
+ SignFlags::empty(),
+ 10_000,
+ vec![0; 32],
+ vec![vec![0; 32]],
+ vec![],
+ );
+ }
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ))
+ }
+}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs
new file mode 100644
index 0000000000..31e5d09e3a
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.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/. */
+
+use runloop::RunLoop;
+use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+use std::ops::Deref;
+use std::sync::mpsc::Sender;
+use std::sync::{Arc, Mutex};
+use std::vec;
+use std::{io, string, thread};
+
+use crate::authenticatorservice::{AuthenticatorTransport, RegisterArgs, SignArgs};
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::virtualdevices::webdriver::{testtoken, web_api};
+
+pub struct VirtualManagerState {
+ pub authenticator_counter: u64,
+ pub tokens: vec::Vec<testtoken::TestToken>,
+}
+
+impl VirtualManagerState {
+ pub fn new() -> Arc<Mutex<VirtualManagerState>> {
+ Arc::new(Mutex::new(VirtualManagerState {
+ authenticator_counter: 0,
+ tokens: vec![],
+ }))
+ }
+}
+
+pub struct VirtualManager {
+ addr: SocketAddr,
+ state: Arc<Mutex<VirtualManagerState>>,
+ rloop: Option<RunLoop>,
+}
+
+impl VirtualManager {
+ pub fn new() -> io::Result<Self> {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080);
+ let state = VirtualManagerState::new();
+ let stateclone = state.clone();
+
+ let builder = thread::Builder::new().name("WebDriver Command Server".into());
+ builder.spawn(move || {
+ web_api::serve(stateclone, addr);
+ })?;
+
+ Ok(Self {
+ addr,
+ state,
+ rloop: None,
+ })
+ }
+
+ pub fn url(&self) -> string::String {
+ format!("http://{}/webauthn/authenticator", &self.addr)
+ }
+}
+
+impl AuthenticatorTransport for VirtualManager {
+ fn register(
+ &mut self,
+ timeout: u64,
+ _ctap_args: RegisterArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ if self.rloop.is_some() {
+ error!("WebDriver state error, prior operation never cancelled.");
+ return Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ));
+ }
+
+ let state = self.state.clone();
+ let rloop = try_or!(
+ RunLoop::new_with_timeout(
+ move |alive| {
+ while alive() {
+ let state_obj = state.lock().unwrap();
+
+ for token in state_obj.tokens.deref() {
+ if token.is_user_consenting {
+ let register_result = token.register();
+ thread::spawn(move || {
+ callback.call(register_result);
+ });
+ return;
+ }
+ }
+ }
+ },
+ timeout
+ ),
+ |_| Err(errors::AuthenticatorError::Platform)
+ );
+
+ self.rloop = Some(rloop);
+ Ok(())
+ }
+
+ fn sign(
+ &mut self,
+ timeout: u64,
+ _ctap_args: SignArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ if self.rloop.is_some() {
+ error!("WebDriver state error, prior operation never cancelled.");
+ return Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ));
+ }
+
+ let state = self.state.clone();
+ let rloop = try_or!(
+ RunLoop::new_with_timeout(
+ move |alive| {
+ while alive() {
+ let state_obj = state.lock().unwrap();
+
+ for token in state_obj.tokens.deref() {
+ if token.is_user_consenting {
+ let sign_result = token.sign();
+ thread::spawn(move || {
+ callback.call(sign_result);
+ });
+ return;
+ }
+ }
+ }
+ },
+ timeout
+ ),
+ |_| Err(errors::AuthenticatorError::Platform)
+ );
+
+ self.rloop = Some(rloop);
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> crate::Result<()> {
+ if let Some(r) = self.rloop.take() {
+ debug!("WebDriver operation cancelled.");
+ r.cancel();
+ }
+ Ok(())
+ }
+}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs
new file mode 100644
index 0000000000..07bfc9f612
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs
@@ -0,0 +1,964 @@
+/* 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 serde::{Deserialize, Serialize};
+use std::net::SocketAddr;
+use std::string;
+use std::sync::{Arc, Mutex};
+use warp::Filter;
+
+use crate::virtualdevices::webdriver::{testtoken, virtualmanager::VirtualManagerState};
+
+fn default_as_false() -> bool {
+ false
+}
+fn default_as_true() -> bool {
+ false
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct AuthenticatorConfiguration {
+ protocol: string::String,
+ transport: string::String,
+ #[serde(rename = "hasResidentKey")]
+ #[serde(default = "default_as_false")]
+ has_resident_key: bool,
+ #[serde(rename = "hasUserVerification")]
+ #[serde(default = "default_as_false")]
+ has_user_verification: bool,
+ #[serde(rename = "isUserConsenting")]
+ #[serde(default = "default_as_true")]
+ is_user_consenting: bool,
+ #[serde(rename = "isUserVerified")]
+ #[serde(default = "default_as_false")]
+ is_user_verified: bool,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct CredentialParameters {
+ #[serde(rename = "credentialId")]
+ credential_id: String,
+ #[serde(rename = "isResidentCredential")]
+ is_resident_credential: bool,
+ #[serde(rename = "rpId")]
+ rp_id: String,
+ #[serde(rename = "privateKey")]
+ private_key: String,
+ #[serde(rename = "userHandle")]
+ #[serde(default)]
+ user_handle: String,
+ #[serde(rename = "signCount")]
+ sign_count: u64,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct UserVerificationParameters {
+ #[serde(rename = "isUserVerified")]
+ is_user_verified: bool,
+}
+
+impl CredentialParameters {
+ fn new_from_test_token_credential(tc: &testtoken::TestTokenCredential) -> CredentialParameters {
+ let credential_id = base64::encode_config(&tc.credential, base64::URL_SAFE);
+
+ let private_key = base64::encode_config(&tc.privkey, base64::URL_SAFE);
+
+ let user_handle = base64::encode_config(&tc.user_handle, base64::URL_SAFE);
+
+ CredentialParameters {
+ credential_id,
+ is_resident_credential: tc.is_resident_credential,
+ rp_id: tc.rp_id.clone(),
+ private_key,
+ user_handle,
+ sign_count: tc.sign_count,
+ }
+ }
+}
+
+fn with_state(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = (Arc<Mutex<VirtualManagerState>>,), Error = std::convert::Infallible> + Clone
+{
+ warp::any().map(move || state.clone())
+}
+
+fn authenticator_add(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_add)
+}
+
+fn authenticator_delete(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64)
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_delete)
+}
+
+fn authenticator_set_uv(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "uv")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_set_uv)
+}
+
+// This is not part of the specification, but it's useful for debugging
+fn authenticator_get(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64)
+ .and(warp::get())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_get)
+}
+
+fn authenticator_credential_add(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credential")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credential_add)
+}
+
+fn authenticator_credential_delete(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials" / String)
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credential_delete)
+}
+
+fn authenticator_credentials_get(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials")
+ .and(warp::get())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credentials_get)
+}
+
+fn authenticator_credentials_clear(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials")
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credentials_clear)
+}
+
+mod handlers {
+ use super::{CredentialParameters, UserVerificationParameters};
+ use crate::virtualdevices::webdriver::{
+ testtoken, virtualmanager::VirtualManagerState, web_api::AuthenticatorConfiguration,
+ };
+ use serde::Serialize;
+ use std::convert::Infallible;
+ use std::ops::DerefMut;
+ use std::sync::{Arc, Mutex};
+ use std::vec;
+ use warp::http::{uri, StatusCode};
+
+ #[derive(Serialize)]
+ struct JsonSuccess {}
+
+ impl JsonSuccess {
+ pub fn blank() -> JsonSuccess {
+ JsonSuccess {}
+ }
+ }
+
+ #[derive(Serialize)]
+ struct JsonError {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ line: Option<u32>,
+ error: String,
+ details: String,
+ }
+
+ impl JsonError {
+ pub fn new(error: &str, line: u32, details: &str) -> JsonError {
+ JsonError {
+ details: details.to_string(),
+ error: error.to_string(),
+ line: Some(line),
+ }
+ }
+ pub fn from_status_code(code: StatusCode) -> JsonError {
+ JsonError {
+ details: code.canonical_reason().unwrap().to_string(),
+ line: None,
+ error: "".to_string(),
+ }
+ }
+ pub fn from_error(error: &str) -> JsonError {
+ JsonError {
+ details: "".to_string(),
+ error: error.to_string(),
+ line: None,
+ }
+ }
+ }
+
+ macro_rules! reply_error {
+ ($status:expr) => {
+ warp::reply::with_status(
+ warp::reply::json(&JsonError::from_status_code($status)),
+ $status,
+ )
+ };
+ }
+
+ macro_rules! try_json {
+ ($val:expr, $status:expr) => {
+ match $val {
+ Ok(v) => v,
+ Err(e) => {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonError::new(
+ $status.canonical_reason().unwrap(),
+ line!(),
+ &e.to_string(),
+ )),
+ $status,
+ ));
+ }
+ }
+ };
+ }
+
+ pub fn validate_rp_id(rp_id: &str) -> crate::Result<()> {
+ if let Ok(uri) = rp_id.parse::<uri::Uri>().map_err(|_| {
+ crate::errors::AuthenticatorError::U2FToken(crate::errors::U2FTokenError::Unknown)
+ }) {
+ if uri.scheme().is_none()
+ && uri.path_and_query().is_none()
+ && uri.port().is_none()
+ && uri.host().is_some()
+ && uri.authority().unwrap() == uri.host().unwrap()
+ // Don't try too hard to ensure it's a valid domain, just
+ // ensure there's a label delim in there somewhere
+ && uri.host().unwrap().find('.').is_some()
+ {
+ return Ok(());
+ }
+ }
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ }
+
+ pub async fn authenticator_add(
+ auth: AuthenticatorConfiguration,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let protocol = match auth.protocol.as_str() {
+ "ctap1/u2f" => testtoken::TestWireProtocol::CTAP1,
+ "ctap2" => testtoken::TestWireProtocol::CTAP2,
+ _ => {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonError::from_error(
+ format!("unknown protocol: {}", auth.protocol).as_str(),
+ )),
+ StatusCode::BAD_REQUEST,
+ ))
+ }
+ };
+
+ let mut state_lock = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ let mut state_obj = state_lock.deref_mut();
+ state_obj.authenticator_counter += 1;
+
+ let tt = testtoken::TestToken::new(
+ state_obj.authenticator_counter,
+ protocol,
+ auth.transport,
+ auth.is_user_consenting,
+ auth.has_user_verification,
+ auth.is_user_verified,
+ auth.has_resident_key,
+ );
+
+ match state_obj
+ .tokens
+ .binary_search_by_key(&state_obj.authenticator_counter, |probe| probe.id)
+ {
+ Ok(_) => panic!("unexpected repeat of authenticator_id"),
+ Err(idx) => state_obj.tokens.insert(idx, tt),
+ }
+
+ #[derive(Serialize)]
+ struct AddResult {
+ #[serde(rename = "authenticatorId")]
+ authenticator_id: u64,
+ }
+
+ Ok(warp::reply::with_status(
+ warp::reply::json(&AddResult {
+ authenticator_id: state_obj.authenticator_counter,
+ }),
+ StatusCode::CREATED,
+ ))
+ }
+
+ pub async fn authenticator_delete(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ match state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ Ok(idx) => state_obj.tokens.remove(idx),
+ Err(_) => {
+ return Ok(reply_error!(StatusCode::NOT_FOUND));
+ }
+ };
+
+ Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ))
+ }
+
+ pub async fn authenticator_get(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ let data = AuthenticatorConfiguration {
+ protocol: tt.protocol.to_webdriver_string(),
+ transport: tt.transport.clone(),
+ has_resident_key: tt.has_resident_key,
+ has_user_verification: tt.has_user_verification,
+ is_user_consenting: tt.is_user_consenting,
+ is_user_verified: tt.is_user_verified,
+ };
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&data),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_set_uv(
+ id: u64,
+ uv: UserVerificationParameters,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ tt.is_user_verified = uv.is_user_verified;
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credential_add(
+ id: u64,
+ auth: CredentialParameters,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let credential = try_json!(
+ base64::decode_config(&auth.credential_id, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let privkey = try_json!(
+ base64::decode_config(&auth.private_key, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let userhandle = try_json!(
+ base64::decode_config(&auth.user_handle, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ try_json!(validate_rp_id(&auth.rp_id), StatusCode::BAD_REQUEST);
+
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ tt.insert_credential(
+ &credential,
+ &privkey,
+ auth.rp_id,
+ auth.is_resident_credential,
+ &userhandle,
+ auth.sign_count,
+ );
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::CREATED,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credential_delete(
+ id: u64,
+ credential_id: String,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let credential = try_json!(
+ base64::decode_config(&credential_id, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ debug!("Asking to delete {}", &credential_id);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ debug!("Asking to delete from token {}", tt.id);
+ if tt.delete_credential(&credential) {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credentials_get(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ let mut creds: vec::Vec<CredentialParameters> = vec![];
+ for ttc in &tt.credentials {
+ creds.push(CredentialParameters::new_from_test_token_credential(ttc));
+ }
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&creds),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credentials_clear(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ tt.credentials.clear();
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+}
+
+#[tokio::main]
+pub async fn serve(state: Arc<Mutex<VirtualManagerState>>, addr: SocketAddr) {
+ let routes = authenticator_add(state.clone())
+ .or(authenticator_delete(state.clone()))
+ .or(authenticator_get(state.clone()))
+ .or(authenticator_set_uv(state.clone()))
+ .or(authenticator_credential_add(state.clone()))
+ .or(authenticator_credential_delete(state.clone()))
+ .or(authenticator_credentials_get(state.clone()))
+ .or(authenticator_credentials_clear(state.clone()));
+
+ warp::serve(routes).run(addr).await;
+}
+
+#[cfg(test)]
+mod tests {
+ use super::handlers::validate_rp_id;
+ use super::testtoken::*;
+ use super::*;
+ use crate::virtualdevices::webdriver::virtualmanager::VirtualManagerState;
+ use std::sync::{Arc, Mutex};
+ use warp::http::StatusCode;
+
+ fn init() {
+ let _ = env_logger::builder().is_test(true).try_init();
+ }
+
+ #[test]
+ fn test_validate_rp_id() {
+ init();
+
+ assert_matches!(
+ validate_rp_id(&String::from("http://example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("https://example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com:443")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com/path")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com:443/path")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("user:pass@example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(validate_rp_id(&String::from("example.com")), Ok(()));
+ }
+
+ fn mk_state_with_token_list(ids: &[u64]) -> Arc<Mutex<VirtualManagerState>> {
+ let state = VirtualManagerState::new();
+
+ {
+ let mut state_obj = state.lock().unwrap();
+ for id in ids {
+ state_obj.tokens.push(TestToken::new(
+ *id,
+ TestWireProtocol::CTAP1,
+ "internal".to_string(),
+ true,
+ true,
+ true,
+ true,
+ ));
+ }
+
+ state_obj.tokens.sort_by_key(|probe| probe.id)
+ }
+
+ state
+ }
+
+ fn assert_success_rsp_blank(body: &warp::hyper::body::Bytes) {
+ assert_eq!(String::from_utf8_lossy(&body), r#"{}"#)
+ }
+
+ fn assert_creds_equals_test_token_params(
+ a: &[CredentialParameters],
+ b: &[TestTokenCredential],
+ ) {
+ assert_eq!(a.len(), b.len());
+
+ for (i, j) in a.iter().zip(b.iter()) {
+ assert_eq!(
+ i.credential_id,
+ base64::encode_config(&j.credential, base64::URL_SAFE)
+ );
+ assert_eq!(
+ i.user_handle,
+ base64::encode_config(&j.user_handle, base64::URL_SAFE)
+ );
+ assert_eq!(
+ i.private_key,
+ base64::encode_config(&j.privkey, base64::URL_SAFE)
+ );
+ assert_eq!(i.rp_id, j.rp_id);
+ assert_eq!(i.sign_count, j.sign_count);
+ assert_eq!(i.is_resident_credential, j.is_resident_credential);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_add() {
+ init();
+ let filter = authenticator_add(mk_state_with_token_list(&[]));
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ let valid_add = AuthenticatorConfiguration {
+ protocol: "ctap1/u2f".to_string(),
+ transport: "usb".to_string(),
+ has_resident_key: false,
+ has_user_verification: false,
+ is_user_consenting: false,
+ is_user_verified: false,
+ };
+
+ {
+ let mut invalid = valid_add.clone();
+ invalid.protocol = "unknown".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ assert!(String::from_utf8_lossy(&res.body())
+ .contains(&String::from("unknown protocol: unknown")));
+ }
+
+ {
+ let mut unknown = valid_add.clone();
+ unknown.transport = "unknown".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&unknown)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_eq!(
+ String::from_utf8_lossy(&res.body()),
+ r#"{"authenticatorId":1}"#
+ )
+ }
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&valid_add)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_eq!(
+ String::from_utf8_lossy(&res.body()),
+ r#"{"authenticatorId":2}"#
+ )
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_delete() {
+ init();
+ let filter = authenticator_delete(mk_state_with_token_list(&[32]));
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/3")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/32")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/42")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_change_uv() {
+ init();
+ let state = mk_state_with_token_list(&[1]);
+ let filter = authenticator_set_uv(state.clone());
+
+ {
+ let state_obj = state.lock().unwrap();
+ assert_eq!(true, state_obj.tokens[0].is_user_verified);
+ }
+
+ {
+ // Empty POST is bad
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ // Unexpected POST structure is bad
+ #[derive(Serialize)]
+ struct Unexpected {
+ id: u64,
+ }
+ let unexpected = Unexpected { id: 4 };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&unexpected)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let param_false = UserVerificationParameters {
+ is_user_verified: false,
+ };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&param_false)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(false, state_obj.tokens[0].is_user_verified);
+ }
+
+ {
+ let param_false = UserVerificationParameters {
+ is_user_verified: true,
+ };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&param_false)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(true, state_obj.tokens[0].is_user_verified);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_credentials() {
+ init();
+ let state = mk_state_with_token_list(&[1]);
+ let filter = authenticator_credential_add(state.clone())
+ .or(authenticator_credential_delete(state.clone()))
+ .or(authenticator_credentials_get(state.clone()))
+ .or(authenticator_credentials_clear(state.clone()));
+
+ let valid_add_credential = CredentialParameters {
+ credential_id: r"c3VwZXIgcmVhZGVy".to_string(),
+ is_resident_credential: true,
+ rp_id: "valid.rpid".to_string(),
+ private_key: base64::encode_config(b"hello internet~", base64::URL_SAFE),
+ user_handle: base64::encode_config(b"hello internet~", base64::URL_SAFE),
+ sign_count: 0,
+ };
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.credential_id = "!@#$ invalid base64".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.rp_id = "example".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.rp_id = "https://example.com".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(0, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let mut no_user_handle = valid_add_credential.clone();
+ no_user_handle.user_handle = "".to_string();
+ no_user_handle.credential_id = "YQo=".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&no_user_handle)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(1, state_obj.tokens[0].credentials.len());
+ let c = &state_obj.tokens[0].credentials[0];
+ assert!(c.user_handle.is_empty());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&valid_add_credential)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), StatusCode::CREATED);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(2, state_obj.tokens[0].credentials.len());
+ let c = &state_obj.tokens[0].credentials[1];
+ assert!(!c.user_handle.is_empty());
+ }
+
+ {
+ // Duplicate, should still be two credentials
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&valid_add_credential)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), StatusCode::CREATED);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(2, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("GET")
+ .path("/webauthn/authenticator/1/credentials")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ let (_, body) = res.into_parts();
+ let cred = serde_json::de::from_slice::<Vec<CredentialParameters>>(&body).unwrap();
+
+ let state_obj = state.lock().unwrap();
+ assert_creds_equals_test_token_params(&cred, &state_obj.tokens[0].credentials);
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials/YmxhbmsK")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 404);
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials/c3VwZXIgcmVhZGVy")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(1, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(0, state_obj.tokens[0].credentials.len());
+ }
+ }
+}
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
diff --git a/third_party/rust/authenticator/webdriver-tools/requirements.txt b/third_party/rust/authenticator/webdriver-tools/requirements.txt
new file mode 100644
index 0000000000..ba2e06d163
--- /dev/null
+++ b/third_party/rust/authenticator/webdriver-tools/requirements.txt
@@ -0,0 +1,2 @@
+requests>=2.23.0
+rich>=3.0
diff --git a/third_party/rust/authenticator/webdriver-tools/webdriver-driver.py b/third_party/rust/authenticator/webdriver-tools/webdriver-driver.py
new file mode 100644
index 0000000000..6647d595d3
--- /dev/null
+++ b/third_party/rust/authenticator/webdriver-tools/webdriver-driver.py
@@ -0,0 +1,207 @@
+# 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/.
+
+from rich.console import Console
+from rich.logging import RichHandler
+
+import argparse
+import logging
+import requests
+
+console = Console()
+log = logging.getLogger("webdriver-driver")
+
+parser = argparse.ArgumentParser()
+subparsers = parser.add_subparsers(help="sub-command help")
+
+parser.add_argument(
+ "--verbose", "-v", help="Be more verbose", action="count", default=0
+)
+parser.add_argument(
+ "--url",
+ default="http://localhost:8080/webauthn/authenticator",
+ help="webdriver url",
+)
+
+
+def device_add(args):
+ data = {
+ "protocol": args.protocol,
+ "transport": args.transport,
+ "hasResidentKey": args.residentkey in ["true", "yes"],
+ "isUserConsenting": args.consent in ["true", "yes"],
+ "hasUserVerification": args.uv in ["available", "verified"],
+ "isUserVerified": args.uv in ["verified"],
+ }
+ console.print("Adding new device: ", data)
+ rsp = requests.post(args.url, json=data)
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_add = subparsers.add_parser("add", help="Add a device")
+parser_add.set_defaults(func=device_add)
+parser_add.add_argument(
+ "--consent",
+ choices=["yes", "no", "true", "false"],
+ default="true",
+ help="consent automatically",
+)
+parser_add.add_argument(
+ "--residentkey",
+ choices=["yes", "no", "true", "false"],
+ default="no",
+ help="indicate a resident key",
+)
+parser_add.add_argument(
+ "--uv",
+ choices=["no", "available", "verified"],
+ default="no",
+ help="indicate user verification",
+)
+parser_add.add_argument(
+ "--protocol", choices=["ctap1/u2f", "ctap2"], default="ctap1/u2f", help="protocol"
+)
+parser_add.add_argument("--transport", default="usb", help="transport type(s)")
+
+
+def device_delete(args):
+ rsp = requests.delete(f"{args.url}/{args.id}")
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_delete = subparsers.add_parser("delete", help="Delete a device")
+parser_delete.set_defaults(func=device_delete)
+parser_delete.add_argument("id", type=int, help="device ID to delete")
+
+
+def device_view(args):
+ rsp = requests.get(f"{args.url}/{args.id}")
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_view = subparsers.add_parser("view", help="View data about a device")
+parser_view.set_defaults(func=device_view)
+parser_view.add_argument("id", type=int, help="device ID to view")
+
+
+def device_update_uv(args):
+ data = {"isUserVerified": args.uv in ["verified", "yes"]}
+ rsp = requests.post(f"{args.url}/{args.id}/uv", json=data)
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_update_uv = subparsers.add_parser(
+ "update-uv", help="Update the User Verified setting"
+)
+parser_update_uv.set_defaults(func=device_update_uv)
+parser_update_uv.add_argument("id", type=int, help="device ID to update")
+parser_update_uv.add_argument(
+ "uv",
+ choices=["no", "yes", "verified"],
+ default="no",
+ help="indicate user verification",
+)
+
+
+def credential_add(args):
+ data = {
+ "credentialId": args.credentialId,
+ "isResidentCredential": args.isResidentCredential in ["true", "yes"],
+ "rpId": args.rpId,
+ "privateKey": args.privateKey,
+ "signCount": args.signCount,
+ }
+ if args.userHandle:
+ data["userHandle"] = (args.userHandle,)
+
+ console.print(f"Adding new credential to device {args.id}: ", data)
+ rsp = requests.post(f"{args.url}/{args.id}/credential", json=data)
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_credential_add = subparsers.add_parser("addcred", help="Add a credential")
+parser_credential_add.set_defaults(func=credential_add)
+parser_credential_add.add_argument(
+ "--id", required=True, type=int, help="device ID to use"
+)
+parser_credential_add.add_argument(
+ "--credentialId", required=True, help="base64url-encoded credential ID"
+)
+parser_credential_add.add_argument(
+ "--isResidentCredential",
+ choices=["yes", "no", "true", "false"],
+ default="no",
+ help="indicate a resident key",
+)
+parser_credential_add.add_argument("--rpId", required=True, help="RP id (hostname)")
+parser_credential_add.add_argument(
+ "--privateKey", required=True, help="base64url-encoded private key per RFC 5958"
+)
+parser_credential_add.add_argument("--userHandle", help="base64url-encoded user handle")
+parser_credential_add.add_argument(
+ "--signCount", default=0, type=int, help="initial signature counter"
+)
+
+
+def credentials_get(args):
+ rsp = requests.get(f"{args.url}/{args.id}/credentials")
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_credentials_get = subparsers.add_parser("getcreds", help="Get credentials")
+parser_credentials_get.set_defaults(func=credentials_get)
+parser_credentials_get.add_argument("id", type=int, help="device ID to query")
+
+
+def credential_delete(args):
+ rsp = requests.delete(f"{args.url}/{args.id}/credentials/{args.credentialId}")
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_credentials_get = subparsers.add_parser("delcred", help="Delete a credential")
+parser_credentials_get.set_defaults(func=credential_delete)
+parser_credentials_get.add_argument("id", type=int, help="device ID to affect")
+parser_credentials_get.add_argument(
+ "credentialId", help="base64url-encoded credential ID"
+)
+
+
+def credentials_clear(args):
+ rsp = requests.delete(f"{args.url}/{args.id}/credentials")
+ console.print(rsp)
+ console.print(rsp.json())
+
+
+parser_credentials_get = subparsers.add_parser(
+ "clearcreds", help="Clear all credentials for a device"
+)
+parser_credentials_get.set_defaults(func=credentials_clear)
+parser_credentials_get.add_argument("id", type=int, help="device ID to affect")
+
+
+def main():
+ args = parser.parse_args()
+
+ loglevel = logging.INFO
+ if args.verbose > 0:
+ loglevel = logging.DEBUG
+ logging.basicConfig(
+ level=loglevel, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()]
+ )
+
+ try:
+ args.func(args)
+ except requests.exceptions.ConnectionError as ce:
+ log.error(f"Connection refused to {args.url}: {ce}")
+
+
+if __name__ == "__main__":
+ main()