diff options
Diffstat (limited to 'third_party/rust/authenticator')
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, ®ister_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(¤t) { + 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, ®ister_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(¶m_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(¶m_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() |