diff --git a/libwebauthn/Cargo.lock b/libwebauthn/Cargo.lock index d09a874..9216758 100644 --- a/libwebauthn/Cargo.lock +++ b/libwebauthn/Cargo.lock @@ -213,7 +213,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools 0.12.1", @@ -236,7 +236,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools 0.13.0", @@ -256,9 +256,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "blake2" @@ -311,7 +311,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "bluez-generated", "dbus", "dbus-tokio", @@ -341,7 +341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" dependencies = [ "async-trait", - "bitflags 2.9.2", + "bitflags 2.9.4", "bluez-async", "dashmap 6.1.0", "dbus", @@ -415,10 +415,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -755,15 +756,15 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbus" -version = "0.9.7" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" dependencies = [ "futures-channel", "futures-util", "libc", "libdbus-sys", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -835,9 +836,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -1046,6 +1047,12 @@ dependencies = [ "trussed-hkdf", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "flexiber" version = "0.1.3" @@ -1220,7 +1227,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.4+wasi-0.2.4", ] [[package]] @@ -1404,20 +1411,21 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", + "moxcms", "num-traits", ] [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -1444,11 +1452,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -1532,9 +1540,9 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -1542,9 +1550,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -1570,9 +1578,9 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libdbus-sys" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" dependencies = [ "pkg-config", ] @@ -1594,7 +1602,7 @@ dependencies = [ "aes", "async-trait", "base64-url", - "bitflags 2.9.2", + "bitflags 2.9.4", "btleplug", "byteorder", "cbc", @@ -1627,6 +1635,7 @@ dependencies = [ "serde_bytes", "serde_cbor_2", "serde_derive", + "serde_json", "serde_repr", "sha2 0.10.9", "snow", @@ -1677,7 +1686,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a81a4745d38b288b7583fe8ea3736897628df81f4d0f1d0314fa5a3af570de4" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "heapless-bytes", "serde", ] @@ -1704,9 +1713,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" @@ -1729,11 +1738,11 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1794,6 +1803,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "nb" version = "1.1.0" @@ -1812,12 +1831,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1909,7 +1927,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "objc2", "objc2-foundation", ] @@ -1926,7 +1944,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "block2", "libc", "objc2", @@ -1968,12 +1986,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.9.0" @@ -2203,6 +2215,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84" +dependencies = [ + "num-traits", +] + [[package]] name = "qrcode" version = "0.14.1" @@ -2292,7 +2313,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", ] [[package]] @@ -2303,47 +2324,32 @@ checksum = "09c30c54dffee5b40af088d5d50aa3455c91a0127164b51f0215efc4cb28fb3c" [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rfc6979" @@ -2405,7 +2411,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2418,7 +2424,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.9.4", @@ -2480,6 +2486,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "salty" version = "0.3.0" @@ -2543,7 +2555,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -2648,6 +2660,18 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -2963,12 +2987,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde", @@ -2978,15 +3001,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3136,14 +3159,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -3158,7 +3181,7 @@ version = "0.1.0" source = "git+https://github.com/trussed-dev/trussed.git?rev=024e0eca5fb7dbd2457831f7c7bffe4341e08775#024e0eca5fb7dbd2457831f7c7bffe4341e08775" dependencies = [ "aes", - "bitflags 2.9.2", + "bitflags 2.9.4", "cbc", "cbor-smol", "cfg-if", @@ -3317,9 +3340,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -3357,30 +3380,31 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.4+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", @@ -3392,9 +3416,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3402,9 +3426,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", @@ -3415,9 +3439,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] @@ -3434,37 +3458,15 @@ dependencies = [ "rustix 0.38.44", ] -[[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.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] -[[package]] -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" version = "0.48.0" @@ -3483,7 +3485,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -3504,7 +3506,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -3516,7 +3518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -3548,6 +3550,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -3555,7 +3563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3564,7 +3572,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3573,7 +3581,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3612,6 +3620,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3649,7 +3666,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3666,7 +3683,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3809,21 +3826,18 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.2", -] +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" [[package]] name = "x509-parser" @@ -3850,18 +3864,18 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index 61705ca..c4f4f1f 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -64,6 +64,7 @@ snow = { version = "0.10", features = ["use-p256"] } ctap-types = { version = "0.4.0" } btleplug = "0.11.7" thiserror = "2.0.12" +serde_json = "1.0.141" [dev-dependencies] tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } diff --git a/libwebauthn/examples/prf_test.rs b/libwebauthn/examples/prf_test.rs index 5d0178e..fc61e49 100644 --- a/libwebauthn/examples/prf_test.rs +++ b/libwebauthn/examples/prf_test.rs @@ -14,7 +14,7 @@ use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ GetAssertionHmacOrPrfInput, GetAssertionRequest, GetAssertionRequestExtensions, PRFValue, - UserVerificationRequirement, + PrfInput, UserVerificationRequirement, }; use libwebauthn::pin::PinRequestReason; use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}; @@ -126,10 +126,11 @@ pub async fn main() -> Result<(), Box> { }); let eval_by_credential = HashMap::new(); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); + run_success_test( &mut channel, &credential, @@ -154,10 +155,10 @@ async fn run_success_test( hash: Vec::from(challenge), allow: vec![credential.clone()], user_verification: UserVerificationRequirement::Preferred, - extensions: Some(GetAssertionRequestExtensions { - hmac_or_prf, + extensions: GetAssertionRequestExtensions { + hmac_or_prf: Some(hmac_or_prf), ..Default::default() - }), + }, timeout: TIMEOUT, }; diff --git a/libwebauthn/examples/webauthn_cable.rs b/libwebauthn/examples/webauthn_cable.rs index f6ae857..d2b933a 100644 --- a/libwebauthn/examples/webauthn_cable.rs +++ b/libwebauthn/examples/webauthn_cable.rs @@ -19,7 +19,8 @@ use tokio::time::sleep; use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ - GetAssertionRequest, MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement, + GetAssertionRequest, GetAssertionRequestExtensions, MakeCredentialRequest, + ResidentKeyRequirement, UserVerificationRequirement, }; use libwebauthn::proto::ctap2::{ Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, @@ -161,7 +162,7 @@ pub async fn main() -> Result<(), Box> { hash: Vec::from(challenge), allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, - extensions: None, + extensions: GetAssertionRequestExtensions::default(), timeout: TIMEOUT, }; diff --git a/libwebauthn/examples/webauthn_extensions_hid.rs b/libwebauthn/examples/webauthn_extensions_hid.rs index 22f6bdd..4b4c831 100644 --- a/libwebauthn/examples/webauthn_extensions_hid.rs +++ b/libwebauthn/examples/webauthn_extensions_hid.rs @@ -11,8 +11,7 @@ use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ CredentialProtectionExtension, CredentialProtectionPolicy, GetAssertionHmacOrPrfInput, - GetAssertionRequest, GetAssertionRequestExtensions, HMACGetSecretInput, - MakeCredentialHmacOrPrfInput, MakeCredentialLargeBlobExtension, MakeCredentialRequest, + GetAssertionRequest, GetAssertionRequestExtensions, HMACGetSecretInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement, }; use libwebauthn::pin::PinRequestReason; @@ -88,10 +87,11 @@ pub async fn main() -> Result<(), Box> { policy: CredentialProtectionPolicy::UserVerificationRequired, enforce_policy: true, }), - cred_blob: Some(r"My own little blob".into()), - large_blob: MakeCredentialLargeBlobExtension::None, + cred_blob: Some("My own little blob".as_bytes().into()), + large_blob: None, min_pin_length: Some(true), - hmac_or_prf: MakeCredentialHmacOrPrfInput::HmacGetSecret, + hmac_create_secret: Some(true), + prf: None, cred_props: Some(true), }; @@ -147,14 +147,16 @@ pub async fn main() -> Result<(), Box> { hash: Vec::from(challenge), allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, - extensions: Some(GetAssertionRequestExtensions { - cred_blob: Some(true), - hmac_or_prf: GetAssertionHmacOrPrfInput::HmacGetSecret(HMACGetSecretInput { - salt1: [1; 32], - salt2: None, - }), + extensions: GetAssertionRequestExtensions { + cred_blob: true, + hmac_or_prf: Some(GetAssertionHmacOrPrfInput::HmacGetSecret( + HMACGetSecretInput { + salt1: [1; 32], + salt2: None, + }, + )), ..Default::default() - }), + }, timeout: TIMEOUT, }; diff --git a/libwebauthn/examples/webauthn_hid.rs b/libwebauthn/examples/webauthn_hid.rs index b5bdb48..8448f52 100644 --- a/libwebauthn/examples/webauthn_hid.rs +++ b/libwebauthn/examples/webauthn_hid.rs @@ -10,7 +10,8 @@ use tokio::sync::broadcast::Receiver; use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ - GetAssertionRequest, MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement, + GetAssertionRequest, GetAssertionRequestExtensions, MakeCredentialRequest, + ResidentKeyRequirement, UserVerificationRequirement, }; use libwebauthn::pin::PinRequestReason; use libwebauthn::proto::ctap2::{ @@ -129,7 +130,7 @@ pub async fn main() -> Result<(), Box> { hash: Vec::from(challenge), allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, - extensions: None, + extensions: GetAssertionRequestExtensions::default(), timeout: TIMEOUT, }; diff --git a/libwebauthn/examples/webauthn_json_hid.rs b/libwebauthn/examples/webauthn_json_hid.rs new file mode 100644 index 0000000..4b08ac2 --- /dev/null +++ b/libwebauthn/examples/webauthn_json_hid.rs @@ -0,0 +1,166 @@ +use std::error::Error; +use std::io::{self, Write}; +use std::time::Duration; + +use libwebauthn::UvUpdate; +use text_io::read; +use tokio::sync::broadcast::Receiver; +use tracing_subscriber::{self, EnvFilter}; + +use libwebauthn::ops::webauthn::{ + GetAssertionRequest, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL as _, +}; +use libwebauthn::pin::PinRequestReason; +use libwebauthn::transport::hid::list_devices; +use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; + +const TIMEOUT: Duration = Duration::from_secs(10); + +fn setup_logging() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .without_time() + .init(); +} + +async fn handle_updates(mut state_recv: Receiver) { + while let Ok(update) = state_recv.recv().await { + match update { + UvUpdate::PresenceRequired => println!("Please touch your device!"), + UvUpdate::UvRetry { attempts_left } => { + print!("UV failed."); + if let Some(attempts_left) = attempts_left { + print!(" You have {attempts_left} attempts left."); + } + } + UvUpdate::PinRequired(update) => { + let mut attempts_str = String::new(); + if let Some(attempts) = update.attempts_left { + attempts_str = format!(". You have {attempts} attempts left!"); + }; + + match update.reason { + PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), + PinRequestReason::AuthenticatorPolicy => { + println!("Your device requires a PIN.") + } + PinRequestReason::FallbackFromUV => { + println!("UV failed too often and is blocked. Falling back to PIN.") + } + } + print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); + io::stdout().flush().unwrap(); + let pin_raw: String = read!("{}\n"); + + if pin_raw.is_empty() { + println!("PIN: No PIN provided, cancelling operation."); + update.cancel(); + } else { + let _ = update.send_pin(&pin_raw); + } + } + } + } +} + +#[tokio::main] +pub async fn main() -> Result<(), Box> { + setup_logging(); + + let devices = list_devices().await.unwrap(); + println!("Devices found: {:?}", devices); + + for mut device in devices { + println!("Selected HID authenticator: {}", &device); + let mut channel = device.channel().await?; + channel.wink(TIMEOUT).await?; + + // Relying + let rpid = RelyingPartyId("example.org".to_owned()); + let request_json = r#" + { + "rp": { + "id": "example.org", + "name": "Example Relying Party" + }, + "user": { + "id": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg", + "name": "Mario Rossi", + "displayName": "Mario Rossi" + }, + "challenge": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg", + "pubKeyCredParams": [ + {"type": "public-key", "alg": -7} + ], + "timeout": 60000, + "excludeCredentials": [], + "authenticatorSelection": { + "residentKey": "discouraged", + "userVerification": "preferred" + }, + "attestation": "none" + } + "#; + let make_credentials_request: MakeCredentialRequest = + MakeCredentialRequest::from_json(&rpid, request_json) + .expect("Failed to parse request JSON"); + println!( + "WebAuthn MakeCredential request: {:?}", + make_credentials_request + ); + + let state_recv = channel.get_ux_update_receiver(); + tokio::spawn(handle_updates(state_recv)); + + let response = loop { + match channel + .webauthn_make_credential(&make_credentials_request) + .await + { + Ok(response) => break Ok(response), + Err(WebAuthnError::Ctap(ctap_error)) => { + if ctap_error.is_retryable_user_error() { + println!("Oops, try again! Error: {}", ctap_error); + continue; + } + break Err(WebAuthnError::Ctap(ctap_error)); + } + Err(err) => break Err(err), + }; + } + .unwrap(); + println!("WebAuthn MakeCredential response: {:?}", response); + + let request_json = r#" + { + "challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu", + "timeout": 30000, + "rpId": "example.org", + "userVerification": "discouraged" + } + "#; + let get_assertion: GetAssertionRequest = + GetAssertionRequest::from_json(&rpid, request_json) + .expect("Failed to parse request JSON"); + println!("WebAuthn GetAssertion request: {:?}", get_assertion); + + let response = loop { + match channel.webauthn_get_assertion(&get_assertion).await { + Ok(response) => break Ok(response), + Err(WebAuthnError::Ctap(ctap_error)) => { + if ctap_error.is_retryable_user_error() { + println!("Oops, try again! Error: {}", ctap_error); + continue; + } + break Err(WebAuthnError::Ctap(ctap_error)); + } + Err(err) => break Err(err), + }; + } + .unwrap(); + println!("WebAuthn GetAssertion response: {:?}", response); + } + + Ok(()) +} diff --git a/libwebauthn/examples/webauthn_preflight_hid.rs b/libwebauthn/examples/webauthn_preflight_hid.rs index bc54eee..dfca1cd 100644 --- a/libwebauthn/examples/webauthn_preflight_hid.rs +++ b/libwebauthn/examples/webauthn_preflight_hid.rs @@ -12,8 +12,8 @@ use tokio::sync::broadcast::Receiver; use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ - GetAssertionRequest, GetAssertionResponse, MakeCredentialRequest, ResidentKeyRequirement, - UserVerificationRequirement, + GetAssertionRequest, GetAssertionRequestExtensions, GetAssertionResponse, + MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement, }; use libwebauthn::pin::PinRequestReason; use libwebauthn::proto::ctap2::{ @@ -203,7 +203,7 @@ async fn get_assertion_call( hash: Vec::from(challenge), allow: allow_list, user_verification: UserVerificationRequirement::Discouraged, - extensions: None, + extensions: GetAssertionRequestExtensions::default(), timeout: TIMEOUT, }; diff --git a/libwebauthn/examples/webauthn_prf_hid.rs b/libwebauthn/examples/webauthn_prf_hid.rs index 35d34a7..ce244a2 100644 --- a/libwebauthn/examples/webauthn_prf_hid.rs +++ b/libwebauthn/examples/webauthn_prf_hid.rs @@ -13,8 +13,8 @@ use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ GetAssertionHmacOrPrfInput, GetAssertionRequest, GetAssertionRequestExtensions, - MakeCredentialHmacOrPrfInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, - PRFValue, ResidentKeyRequirement, UserVerificationRequirement, + MakeCredentialPrfInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, PRFValue, + PrfInput, ResidentKeyRequirement, UserVerificationRequirement, }; use libwebauthn::pin::PinRequestReason; use libwebauthn::proto::ctap2::{ @@ -85,7 +85,7 @@ pub async fn main() -> Result<(), Box> { let challenge: [u8; 32] = thread_rng().gen(); let extensions = MakeCredentialsRequestExtensions { - hmac_or_prf: MakeCredentialHmacOrPrfInput::Prf, + prf: Some(MakeCredentialPrfInput { _eval: None }), ..Default::default() }; @@ -148,10 +148,10 @@ pub async fn main() -> Result<(), Box> { second: None, }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_success_test( &mut channel, &credential, @@ -175,10 +175,10 @@ pub async fn main() -> Result<(), Box> { second: None, }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_success_test( &mut channel, &credential, @@ -195,10 +195,10 @@ pub async fn main() -> Result<(), Box> { }); let eval_by_credential = HashMap::new(); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_success_test( &mut channel, &credential, @@ -243,10 +243,10 @@ pub async fn main() -> Result<(), Box> { second: None, }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_success_test( &mut channel, &credential, @@ -284,10 +284,10 @@ pub async fn main() -> Result<(), Box> { second: Some([8; 32]), }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_success_test( &mut channel, &credential, @@ -322,10 +322,10 @@ pub async fn main() -> Result<(), Box> { second: Some([8; 32]), }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_success_test( &mut channel, &credential, @@ -349,10 +349,10 @@ pub async fn main() -> Result<(), Box> { second: None, }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_failed_test( &mut channel, Some(&credential), @@ -373,10 +373,10 @@ pub async fn main() -> Result<(), Box> { second: None, }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf: GetAssertionHmacOrPrfInput = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_failed_test( &mut channel, Some(&credential), @@ -397,10 +397,10 @@ pub async fn main() -> Result<(), Box> { second: None, }, ); - let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { + let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf(PrfInput { eval, eval_by_credential, - }; + }); run_failed_test( &mut channel, None, @@ -426,10 +426,10 @@ async fn run_success_test( hash: Vec::from(challenge), allow: vec![credential.clone()], user_verification: UserVerificationRequirement::Discouraged, - extensions: Some(GetAssertionRequestExtensions { - hmac_or_prf, + extensions: GetAssertionRequestExtensions { + hmac_or_prf: Some(hmac_or_prf), ..Default::default() - }), + }, timeout: TIMEOUT, }; @@ -468,10 +468,10 @@ async fn run_failed_test( hash: Vec::from(challenge), allow: credential.map(|x| vec![x.clone()]).unwrap_or_default(), user_verification: UserVerificationRequirement::Discouraged, - extensions: Some(GetAssertionRequestExtensions { - hmac_or_prf, + extensions: GetAssertionRequestExtensions { + hmac_or_prf: Some(hmac_or_prf), ..Default::default() - }), + }, timeout: TIMEOUT, }; diff --git a/libwebauthn/src/fido.rs b/libwebauthn/src/fido.rs index 55ce46b..532a08a 100644 --- a/libwebauthn/src/fido.rs +++ b/libwebauthn/src/fido.rs @@ -273,11 +273,10 @@ mod tests { 0x86, 0xce, 0x19, 0x47, ]; let flag_bits = 0b1100_0101; - let flags = - AuthenticatorDataFlags::USER_PRESENT | - AuthenticatorDataFlags::USER_VERIFIED | - AuthenticatorDataFlags::ATTESTED_CREDENTIALS | - AuthenticatorDataFlags::EXTENSION_DATA; + let flags = AuthenticatorDataFlags::USER_PRESENT + | AuthenticatorDataFlags::USER_VERIFIED + | AuthenticatorDataFlags::ATTESTED_CREDENTIALS + | AuthenticatorDataFlags::EXTENSION_DATA; assert_eq!(flag_bits, flags.bits()); let signature_count = 0; let aaguid = [ @@ -316,7 +315,7 @@ mod tests { flags, signature_count, attested_credential: Some(attested_credential.clone()), - extensions: Some(extensions.clone()) + extensions: Some(extensions.clone()), }; let webauthn_auth_data = auth_data.to_response_bytes().unwrap(); assert_eq!(rp_id_hash, &webauthn_auth_data[..32]); @@ -341,14 +340,8 @@ mod tests { let authdata_wrapped = cbor::to_vec(&ByteBuf::from(webauthn_auth_data)).unwrap(); let auth_data_reparsed: AuthenticatorData = cbor::from_slice(authdata_wrapped.as_slice()).unwrap(); - assert_eq!( - auth_data.rp_id_hash, - auth_data_reparsed.rp_id_hash - ); - assert_eq!( - auth_data.flags.bits(), - auth_data_reparsed.flags.bits() - ); + assert_eq!(auth_data.rp_id_hash, auth_data_reparsed.rp_id_hash); + assert_eq!(auth_data.flags.bits(), auth_data_reparsed.flags.bits()); assert_eq!( auth_data.signature_count, auth_data_reparsed.signature_count @@ -366,9 +359,6 @@ mod tests { attested_credential.credential_public_key, attested_credential_reparsed.credential_public_key ); - assert_eq!( - extensions, - auth_data_reparsed.extensions.unwrap() - ); + assert_eq!(extensions, auth_data_reparsed.extensions.unwrap()); } } diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index c442b81..0b6b43f 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -9,7 +9,8 @@ use x509_parser::nom::AsBytes; use super::webauthn::MakeCredentialRequest; use crate::fido::{AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags}; use crate::ops::webauthn::{ - GetAssertionRequest, GetAssertionResponse, MakeCredentialResponse, UserVerificationRequirement, + GetAssertionRequest, GetAssertionRequestExtensions, GetAssertionResponse, + MakeCredentialResponse, UserVerificationRequirement, }; use crate::proto::ctap1::{Ctap1RegisterRequest, Ctap1SignRequest}; use crate::proto::ctap1::{Ctap1RegisterResponse, Ctap1SignResponse}; @@ -206,7 +207,7 @@ impl UpgradableResponse for SignResponse { id: request.key_handle.clone().into(), transports: None, }], - extensions: None, + extensions: GetAssertionRequestExtensions::default(), user_verification: if request.require_user_presence { UserVerificationRequirement::Required } else { diff --git a/libwebauthn/src/ops/webauthn/client_data.rs b/libwebauthn/src/ops/webauthn/client_data.rs new file mode 100644 index 0000000..9145a48 --- /dev/null +++ b/libwebauthn/src/ops/webauthn/client_data.rs @@ -0,0 +1,36 @@ +use crate::ops::webauthn::Operation; + +use serde::Deserialize; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct ClientData { + pub operation: Operation, + pub challenge: Vec, + pub origin: String, + #[serde(rename = "crossOrigin")] + pub cross_origin: Option, +} + +impl ClientData { + pub fn hash(&self) -> Vec { + let op_str = match &self.operation { + Operation::MakeCredential => "webauthn.create", + Operation::GetAssertion => "webauthn.get", + }; + let challenge_str = base64_url::encode(&self.challenge); + let origin_str = &self.origin; + let cross_origin_str = if self.cross_origin.unwrap_or(false) { + "true" + } else { + "false" + }; + let json = + format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge_str}\",\"origin\":\"{origin_str}\",\"crossOrigin\":{cross_origin_str}}}"); + + let mut hasher = Sha256::new(); + hasher.update(json.as_bytes()); + hasher.finalize().to_vec() + } +} + diff --git a/libwebauthn/src/ops/webauthn/get_assertion.rs b/libwebauthn/src/ops/webauthn/get_assertion.rs index 336ff00..5043a7e 100644 --- a/libwebauthn/src/ops/webauthn/get_assertion.rs +++ b/libwebauthn/src/ops/webauthn/get_assertion.rs @@ -6,6 +6,17 @@ use tracing::{debug, error, trace}; use crate::{ fido::AuthenticatorData, + ops::webauthn::{ + client_data::ClientData, + idl::{ + get::{ + HmacGetSecretInputJson, LargeBlobInputJson, PrfInputJson, + PublicKeyCredentialRequestOptionsJSON, + }, + FromInnerModel, JsonError, + }, + Operation, WebAuthnIDL, + }, pin::PinUvAuthProtocol, proto::ctap2::{ Ctap2AttestationStatement, Ctap2GetAssertionResponseExtensions, @@ -14,9 +25,11 @@ use crate::{ webauthn::CtapError, }; -use super::{DowngradableRequest, SignRequest, UserVerificationRequirement}; +use super::{DowngradableRequest, RelyingPartyId, SignRequest, UserVerificationRequirement}; -#[derive(Debug, Default, Clone, Serialize)] +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); + +#[derive(Debug, Default, Clone, Serialize, PartialEq)] pub struct PRFValue { #[serde(with = "serde_bytes")] pub first: [u8; 32], @@ -24,25 +37,178 @@ pub struct PRFValue { pub second: Option<[u8; 32]>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct GetAssertionRequest { pub relying_party_id: String, pub hash: Vec, pub allow: Vec, - pub extensions: Option, + pub extensions: GetAssertionRequestExtensions, pub user_verification: UserVerificationRequirement, pub timeout: Duration, } -#[derive(Debug, Default, Clone)] +#[derive(thiserror::Error, Debug)] +pub enum GetAssertionRequestParsingError { + /// The client must throw an "EncodingError" DOMException. + #[error("Invalid JSON format: {0}")] + EncodingError(#[from] JsonError), + + #[error("Unexpected length for {0}: {1}")] + UnexpectedLengthError(String, usize), + + #[error("Not supported: {0}")] + NotSupported(String), +} + +impl WebAuthnIDL for GetAssertionRequest { + type Error = GetAssertionRequestParsingError; + type InnerModel = PublicKeyCredentialRequestOptionsJSON; +} + +/** dictionary PublicKeyCredentialRequestOptionsJSON { + required Base64URLString challenge; + unsigned long timeout; + DOMString rpId; + sequence allowCredentials = []; + DOMString userVerification = "preferred"; + sequence hints = []; + AuthenticationExtensionsClientInputsJSON extensions; +}; */ + +impl FromInnerModel + for GetAssertionRequest +{ + fn from_inner_model( + rpid: &RelyingPartyId, + inner: PublicKeyCredentialRequestOptionsJSON, + ) -> Result { + let hmac_or_prf = match inner.extensions.clone() { + Some(ext) => { + if let Some(prf) = ext.prf { + let prf_input = PrfInput::try_from(prf)?; + Some(GetAssertionHmacOrPrfInput::Prf(prf_input)) + } else if let Some(hmac) = ext.hamc_get_secret { + let hmac_input = HMACGetSecretInput::try_from(hmac)?; + Some(GetAssertionHmacOrPrfInput::HmacGetSecret(hmac_input)) + } else { + None + } + } + None => None, + }; + + let extensions_opt = inner.extensions.clone(); + let extensions = GetAssertionRequestExtensions { + cred_blob: extensions_opt + .as_ref() + .and_then(|ext| ext.cred_blob) + .unwrap_or(false), + large_blob: extensions_opt + .as_ref() + .and_then(|ext| ext.large_blob.clone()) + .map(Option::::try_from) + .transpose()? + .flatten(), + hmac_or_prf, + }; + + let timeout: Duration = inner + .timeout + .map(|s| Duration::from_millis(s.into())) + .unwrap_or(DEFAULT_TIMEOUT); + + let client_data_json = ClientData { + operation: Operation::GetAssertion, + challenge: inner.challenge.to_vec(), + origin: rpid.to_string(), + cross_origin: None, + }; + + Ok(GetAssertionRequest { + relying_party_id: rpid.to_string(), + hash: client_data_json.hash(), + allow: inner + .allow_credentials + .into_iter() + .map(|c| c.into()) + .collect(), + extensions, + user_verification: inner.uv_requirement, + timeout, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] pub enum GetAssertionHmacOrPrfInput { - #[default] - None, HmacGetSecret(HMACGetSecretInput), - Prf { - eval: Option, - eval_by_credential: HashMap, - }, + Prf(PrfInput), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PrfInput { + pub eval: Option, + pub eval_by_credential: HashMap, +} + +impl TryFrom for PrfInput { + type Error = GetAssertionRequestParsingError; + + fn try_from(value: PrfInputJson) -> Result { + let eval = match value.eval { + Some(value) => Some(PRFValue { + first: value.first.as_slice().try_into().map_err(|_| { + GetAssertionRequestParsingError::UnexpectedLengthError( + "extensions.prf.eval.first".to_string(), + value.first.as_slice().len(), + ) + })?, + second: match value.second { + Some(s) => Some(s.as_slice().try_into().map_err(|_| { + GetAssertionRequestParsingError::UnexpectedLengthError( + "extensions.prf.eval.second".to_string(), + s.as_slice().len(), + ) + })?), + None => None, + }, + }), + None => None, + }; + let eval_by_credential = match value.eval_by_credential { + Some(map) => map + .into_iter() + .map(|(k, v)| { + Ok(( + k, + PRFValue { + first: v.first.as_slice().try_into().map_err(|_| { + GetAssertionRequestParsingError::UnexpectedLengthError( + "extensions.prf.eval_by_credential[i].first".to_string(), + v.first.as_slice().len(), + ) + })?, + second: match v.second { + Some(s) => Some(s.as_slice().try_into().map_err(|_| { + GetAssertionRequestParsingError::UnexpectedLengthError( + "extensions.prf.eval_by_credential[i].second".to_string(), + s.as_slice().len(), + ) + })?), + None => None, + }, + }, + )) + }) + .collect::, GetAssertionRequestParsingError>>()?, + None => HashMap::new(), + }; + + Ok(PrfInput { + eval, + eval_by_credential, + }) + } } #[derive(Debug, Default, Clone, Serialize)] @@ -57,15 +223,50 @@ pub struct HMACGetSecretInput { pub salt2: Option<[u8; 32]>, } -#[derive(Debug, Default, Clone, PartialEq, Eq)] +impl TryFrom for HMACGetSecretInput { + type Error = GetAssertionRequestParsingError; + + fn try_from(value: HmacGetSecretInputJson) -> Result { + let salt1 = value.salt1.as_slice().try_into().map_err(|_| { + GetAssertionRequestParsingError::UnexpectedLengthError( + "extensions.hmacCreateSecret.salt1".to_string(), + value.salt1.as_slice().len(), + ) + })?; + let salt2 = match value.salt2 { + Some(s) => Some(s.as_slice().try_into().map_err(|_| { + GetAssertionRequestParsingError::UnexpectedLengthError( + "extensions.hmacCreateSecret.salt2".to_string(), + s.as_slice().len(), + ) + })?), + None => None, + }; + Ok(HMACGetSecretInput { salt1, salt2 }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum GetAssertionLargeBlobExtension { - #[default] - None, Read, // Not yet supported // Write(Vec), } +impl TryFrom for Option { + type Error = GetAssertionRequestParsingError; + + fn try_from(value: LargeBlobInputJson) -> Result { + match value.read { + Some(true) => Ok(Some(GetAssertionLargeBlobExtension::Read)), + Some(false) => Err(GetAssertionRequestParsingError::NotSupported( + "largeBlob writes not supported".to_string(), + )), + None => Ok(None), + } + } +} + #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] pub struct GetAssertionLargeBlobExtensionOutput { #[serde(skip_serializing_if = "Option::is_none")] @@ -75,11 +276,11 @@ pub struct GetAssertionLargeBlobExtensionOutput { // pub written: Option, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct GetAssertionRequestExtensions { - pub cred_blob: Option, - pub hmac_or_prf: GetAssertionHmacOrPrfInput, - pub large_blob: GetAssertionLargeBlobExtension, + pub cred_blob: bool, + pub hmac_or_prf: Option, + pub large_blob: Option, } #[derive(Clone, Debug, Default, Serialize)] @@ -227,3 +428,147 @@ impl DowngradableRequest> for GetAssertionRequest { Ok(downgraded_requests) } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use serde_bytes::ByteBuf; + + use crate::ops::webauthn::GetAssertionRequest; + use crate::ops::webauthn::RelyingPartyId; + use crate::proto::ctap2::Ctap2PublicKeyCredentialType; + + use super::*; + + pub const REQUEST_BASE_JSON: &str = r#" + { + "challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu", + "timeout": 30000, + "rpId": "example.org", + "allowCredentials": [ + { + "type": "public-key", + "id": "bXktY3JlZGVudGlhbC1pZA" + } + ], + "userVerification": "preferred" + } + "#; + + fn request_base() -> GetAssertionRequest { + let client_data_json = ClientData { + operation: Operation::GetAssertion, + challenge: base64_url::decode("Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu").unwrap(), + origin: "example.org".to_string(), + cross_origin: None, + }; + GetAssertionRequest { + relying_party_id: "example.org".to_owned(), + hash: client_data_json.hash(), + allow: vec![Ctap2PublicKeyCredentialDescriptor { + r#type: Ctap2PublicKeyCredentialType::PublicKey, + id: ByteBuf::from(base64_url::decode("bXktY3JlZGVudGlhbC1pZA").unwrap()), + transports: None, + }], + extensions: GetAssertionRequestExtensions::default(), + user_verification: UserVerificationRequirement::Preferred, + timeout: Duration::from_secs(30), + } + } + + fn json_field_add(str: &str, field: &str, value: &str) -> String { + let mut v: serde_json::Value = serde_json::from_str(str).unwrap(); + v.as_object_mut() + .unwrap() + .insert(field.to_owned(), serde_json::from_str(value).unwrap()); + serde_json::to_string(&v).unwrap() + } + + fn json_field_rm(str: &str, field: &str) -> String { + let mut v: serde_json::Value = serde_json::from_str(str).unwrap(); + v.as_object_mut().unwrap().remove(field); + serde_json::to_string(&v).unwrap() + } + + #[test] + fn test_request_from_json_base() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req: GetAssertionRequest = + GetAssertionRequest::from_json(&rpid, REQUEST_BASE_JSON).unwrap(); + assert_eq!(req, request_base()); + } + + #[test] + fn test_request_from_json_ignore_missing_rp_id() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_rm(REQUEST_BASE_JSON, "rpId"); + + let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap(); + assert_eq!(req, request_base()); + } + + #[test] + fn test_request_from_json_ignore_request_rp_id() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_rm(REQUEST_BASE_JSON, "rpId"); + let req_json = json_field_add(&req_json, "rpId", r#""another-example.org""#); + + let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap(); + assert_eq!(req, request_base()); + } + + #[test] + fn test_request_from_json_ignore_missing_allow_credentials() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_rm(REQUEST_BASE_JSON, "allowCredentials"); + + let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap(); + assert_eq!( + req, + GetAssertionRequest { + allow: vec![], + ..request_base() + } + ); + } + + #[test] + fn test_request_from_json_default_timeout() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_rm(REQUEST_BASE_JSON, "timeout"); + + let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap(); + assert_eq!(req.timeout, DEFAULT_TIMEOUT); + } + + #[test] + #[ignore] // FIXME(#134) allow arbitrary size input + fn test_request_from_json_prf_extension() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_add( + REQUEST_BASE_JSON, + "extensions", + r#"{"prf":{"eval":{"first": "second"}}}"#, + ); + + let req: GetAssertionRequest = GetAssertionRequest::from_json(&rpid, &req_json).unwrap(); + if let GetAssertionRequestExtensions { + hmac_or_prf: + Some(GetAssertionHmacOrPrfInput::Prf(PrfInput { + eval: Some(ref prf_value), + .. + })), + .. + } = &req.extensions + { + assert_eq!(&prf_value.first[..], b"first"); + assert_eq!( + prf_value.second.as_ref().map(|s| &s[..]), + Some(&b"second"[..]) + ); + } else { + panic!("Expected PRF extension with correct values"); + } + } +} diff --git a/libwebauthn/src/ops/webauthn/idl/base64url.rs b/libwebauthn/src/ops/webauthn/idl/base64url.rs new file mode 100644 index 0000000..8d5e43e --- /dev/null +++ b/libwebauthn/src/ops/webauthn/idl/base64url.rs @@ -0,0 +1,67 @@ +use std::ops::Deref; + +use base64_url; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Base64UrlString(pub Vec); + +impl From> for Base64UrlString { + fn from(bytes: Vec) -> Self { + Base64UrlString(bytes) + } +} + +impl From<&[u8]> for Base64UrlString { + fn from(bytes: &[u8]) -> Self { + Base64UrlString(bytes.to_vec()) + } +} + +impl Deref for Base64UrlString { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + &self.0 + } +} + +impl AsRef<[u8]> for Base64UrlString { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'de> Deserialize<'de> for Base64UrlString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + base64_url::decode(&s) + .map_err(serde::de::Error::custom) + .map(|bytes| Base64UrlString(bytes)) + } +} + +impl Serialize for Base64UrlString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let encoded = base64_url::encode(&self.0); + serializer.serialize_str(&encoded) + } +} + +impl Into> for Base64UrlString { + fn into(self) -> Vec { + self.0 + } +} + +impl Base64UrlString { + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} diff --git a/libwebauthn/src/ops/webauthn/idl/create.rs b/libwebauthn/src/ops/webauthn/idl/create.rs new file mode 100644 index 0000000..c2bf23c --- /dev/null +++ b/libwebauthn/src/ops/webauthn/idl/create.rs @@ -0,0 +1,60 @@ +use super::Base64UrlString; +use crate::{ + ops::webauthn::{ + MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement, + }, + proto::ctap2::{ + Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, + }, +}; + +use serde::Deserialize; + +/** + * https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON + */ + +#[derive(Debug, Clone, Deserialize)] +pub struct AuthenticatorSelectionCriteria { + #[serde(rename = "authenticatorAttachment")] + pub authenticator_attachment: Option, + #[serde(rename = "residentKey")] + pub resident_key: Option, + #[serde(rename = "requireResidentKey")] + #[serde(default)] + pub require_resident_key: bool, + #[serde(rename = "userVerification")] + #[serde(default = "default_user_verification")] + pub user_verification: UserVerificationRequirement, +} + +fn default_user_verification() -> UserVerificationRequirement { + UserVerificationRequirement::Preferred +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct PublicKeyCredentialUserEntity { + pub id: Base64UrlString, + pub name: String, + #[serde(rename = "displayName")] + pub display_name: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PublicKeyCredentialCreationOptionsJSON { + pub rp: Ctap2PublicKeyCredentialRpEntity, + pub user: PublicKeyCredentialUserEntity, + pub challenge: Base64UrlString, + #[serde(rename = "pubKeyCredParams")] + pub params: Vec, + pub timeout: Option, + #[serde(rename = "excludeCredentials")] + pub exclude_credentials: Vec, + #[serde(rename = "authenticatorSelection")] + pub authenticator_selection: Option, + pub hints: Option>, + pub attestation: Option, + #[serde(rename = "attestationFormats")] + pub attestation_formats: Option>, + pub extensions: Option, +} diff --git a/libwebauthn/src/ops/webauthn/idl/get.rs b/libwebauthn/src/ops/webauthn/idl/get.rs new file mode 100644 index 0000000..3267a69 --- /dev/null +++ b/libwebauthn/src/ops/webauthn/idl/get.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +use serde::Deserialize; +use serde_bytes::ByteBuf; + +use crate::{ + ops::webauthn::{Base64UrlString, UserVerificationRequirement}, + proto::ctap2::{ + Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, Ctap2Transport, + }, +}; + +#[derive(Deserialize, Debug, Clone)] +pub struct PublicKeyCredentialRequestOptionsJSON { + pub challenge: Base64UrlString, + pub timeout: Option, + #[serde(rename = "rpId")] + pub relying_party_id: Option, + #[serde(rename = "allowCredentials")] + #[serde(default)] + pub allow_credentials: Vec, + #[serde(rename = "userVerification")] + pub uv_requirement: UserVerificationRequirement, + #[serde(default)] + pub hints: Vec, + pub extensions: Option, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct PublicKeyCredentialDescriptorJSON { + pub id: Base64UrlString, + pub r#type: Ctap2PublicKeyCredentialType, + + #[serde(skip_serializing_if = "Option::is_none")] + pub transports: Option>, +} + +impl Into for PublicKeyCredentialDescriptorJSON { + fn into(self) -> Ctap2PublicKeyCredentialDescriptor { + Ctap2PublicKeyCredentialDescriptor { + r#type: self.r#type, + id: ByteBuf::from(self.id), + transports: self.transports, + } + } +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct GetAssertionRequestExtensionsJSON { + #[serde(rename = "getCredBlob")] + pub cred_blob: Option, + #[serde(rename = "largeBlobKey")] + pub large_blob: Option, + #[serde(rename = "hmacCreateSecret")] + pub hamc_get_secret: Option, + #[serde(rename = "prf")] + pub prf: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LargeBlobInputJson { + pub read: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PrfInputJson { + pub eval: Option, + pub eval_by_credential: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PrfValuesJson { + pub first: Base64UrlString, + pub second: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HmacGetSecretInputJson { + pub salt1: Base64UrlString, + pub salt2: Option, +} diff --git a/libwebauthn/src/ops/webauthn/idl/mod.rs b/libwebauthn/src/ops/webauthn/idl/mod.rs new file mode 100644 index 0000000..c392756 --- /dev/null +++ b/libwebauthn/src/ops/webauthn/idl/mod.rs @@ -0,0 +1,39 @@ +mod base64url; +pub mod create; +pub mod get; +pub mod rpid; + +pub use base64url::Base64UrlString; + +use rpid::RelyingPartyId; + +use serde::de::DeserializeOwned; +use serde_json; + +pub type JsonError = serde_json::Error; + +pub trait WebAuthnIDL: Sized +where + E: std::error::Error, // Validation error type. + Self: FromInnerModel, +{ + /// An error type that can be returned when deserializing from JSON, including + /// JSON parsing errors and any additional validation errors. + type Error: std::error::Error + From + From; + + /// The JSON model that this IDL can deserialize from. + type InnerModel: DeserializeOwned; + + fn from_json(rpid: &RelyingPartyId, json: &str) -> Result { + let inner_model: Self::InnerModel = serde_json::from_str(json)?; + Self::from_inner_model(rpid, inner_model).map_err(From::from) + } +} + +pub trait FromInnerModel: Sized +where + T: DeserializeOwned, + E: std::error::Error, +{ + fn from_inner_model(rpid: &RelyingPartyId, inner: T) -> Result; +} diff --git a/libwebauthn/src/ops/webauthn/idl/rpid.rs b/libwebauthn/src/ops/webauthn/idl/rpid.rs new file mode 100644 index 0000000..a92b12b --- /dev/null +++ b/libwebauthn/src/ops/webauthn/idl/rpid.rs @@ -0,0 +1,49 @@ +use serde::Deserialize; +use std::{convert::TryFrom, ops::Deref}; + +#[derive(Clone, Debug)] +pub struct RelyingPartyId(pub String); + +impl Deref for RelyingPartyId { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl Into for RelyingPartyId { + fn into(self) -> String { + self.0 + } +} + +#[derive(thiserror::Error, Debug, Clone)] +// TODO(#137): Validate RelyingPartyId +pub enum Error { + #[error("Empty Relying Party ID is not allowed")] + EmptyRelyingPartyId, +} + +impl TryFrom<&str> for RelyingPartyId { + type Error = Error; + + fn try_from(value: &str) -> Result { + // TODO(#137): Validate RelyingPartyId, including IDNA normalization + // and checking for valid characters. + match value { + "" => Err(Error::EmptyRelyingPartyId), + _ => Ok(RelyingPartyId(value.to_string())), + } + } +} + +impl<'de> Deserialize<'de> for RelyingPartyId { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + RelyingPartyId::try_from(s.as_str()).map_err(serde::de::Error::custom) + } +} diff --git a/libwebauthn/src/ops/webauthn/make_credential.rs b/libwebauthn/src/ops/webauthn/make_credential.rs index 1526c98..622e981 100644 --- a/libwebauthn/src/ops/webauthn/make_credential.rs +++ b/libwebauthn/src/ops/webauthn/make_credential.rs @@ -2,11 +2,20 @@ use std::time::Duration; use ctap_types::ctap2::credential_management::CredentialProtectionPolicy as Ctap2CredentialProtectionPolicy; use serde::{Deserialize, Serialize}; +use serde_json::{self, Value as JsonValue}; use sha2::{Digest, Sha256}; use tracing::{debug, instrument, trace}; use crate::{ fido::AuthenticatorData, + ops::webauthn::{ + client_data::ClientData, + idl::{ + create::PublicKeyCredentialCreationOptionsJSON, Base64UrlString, FromInnerModel, + JsonError, WebAuthnIDL, + }, + Operation, RelyingPartyId, + }, proto::{ ctap1::{Ctap1RegisteredKey, Ctap1Version}, ctap2::{ @@ -20,6 +29,8 @@ use crate::{ use super::{DowngradableRequest, RegisterRequest, UserVerificationRequirement}; +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); + #[derive(Debug, Clone)] pub struct MakeCredentialResponse { pub format: String, @@ -63,17 +74,17 @@ impl MakeCredentialsResponseUnsignedExtensions { let mut prf = None; if let Some(signed_extensions) = signed_extensions { (hmac_create_secret, prf) = if let Some(incoming_ext) = &request.extensions { - match &incoming_ext.hmac_or_prf { - MakeCredentialHmacOrPrfInput::None => (None, None), - MakeCredentialHmacOrPrfInput::HmacGetSecret => { - (signed_extensions.hmac_secret, None) - } - MakeCredentialHmacOrPrfInput::Prf => ( + if let Some(_hmac_create_secret) = incoming_ext.hmac_create_secret { + (signed_extensions.hmac_secret, None) + } else if let Some(_prf) = &incoming_ext.prf { + ( None, Some(MakeCredentialPrfOutput { enabled: signed_extensions.hmac_secret, }), - ), + ) + } else { + (None, None) } } else { (None, None) @@ -126,7 +137,12 @@ impl MakeCredentialsResponseUnsignedExtensions { // largeBlob extension // https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension - let large_blob = match &request.extensions.as_ref().map(|x| &x.large_blob) { + let large_blob = match &request + .extensions + .as_ref() + .and_then(|x| x.large_blob.as_ref()) + .map(|x| x.support) + { None | Some(MakeCredentialLargeBlobExtension::None) => None, // Not requested, so we don't give an answer Some(MakeCredentialLargeBlobExtension::Preferred) | Some(MakeCredentialLargeBlobExtension::Required) => { @@ -149,14 +165,17 @@ impl MakeCredentialsResponseUnsignedExtensions { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Deserialize, PartialEq)] pub enum ResidentKeyRequirement { + #[serde(rename = "required")] Required, + #[serde(rename = "preferred")] Preferred, + #[serde(rename = "discouraged", other)] Discouraged, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct MakeCredentialRequest { pub hash: Vec, pub origin: String, @@ -175,22 +194,81 @@ pub struct MakeCredentialRequest { pub timeout: Duration, } -#[derive(Debug, Default, Clone)] -pub enum MakeCredentialHmacOrPrfInput { - #[default] - None, - HmacGetSecret, - Prf, - // The spec tells us that in theory, we could hand in - // an `eval` here, IF the CTAP2 would get an additional - // extension to handle that. There is no such CTAP-extension - // right now, so we don't expose it for now, as it would just - // be ignored anyways. - // https://w3c.github.io/webauthn/#prf - // "If eval is present and a future extension to [FIDO-CTAP] permits evaluation of the PRF at creation time, configure hmac-secret inputs accordingly: .." - // Prf { - // eval: Option, - // }, +impl FromInnerModel + for MakeCredentialRequest +{ + fn from_inner_model( + rpid: &RelyingPartyId, + inner: PublicKeyCredentialCreationOptionsJSON, + ) -> Result { + let resident_key = if inner + .authenticator_selection + .as_ref() + .and_then(|s| Some(s.require_resident_key)) + == Some(true) + { + Some(ResidentKeyRequirement::Required) + } else { + inner + .authenticator_selection + .as_ref() + .and_then(|s| s.resident_key) + }; + + let user_verification = inner + .authenticator_selection + .as_ref() + .map_or(UserVerificationRequirement::Discouraged, |s| { + s.user_verification + }); + + let timeout: Duration = inner + .timeout + .map(|s| Duration::from_millis(s.into())) + .unwrap_or(DEFAULT_TIMEOUT); + + let client_data_json = ClientData { + operation: Operation::MakeCredential, + challenge: inner.challenge.to_vec(), + origin: rpid.to_string(), + cross_origin: None, + }; + + Ok(Self { + hash: client_data_json.hash(), + origin: rpid.to_owned().into(), + relying_party: inner.rp, + user: inner.user.into(), + resident_key, + user_verification, + algorithms: inner.params, + exclude: if inner.exclude_credentials.is_empty() { + None + } else { + Some(inner.exclude_credentials) + }, + extensions: inner.extensions, + timeout: timeout, + }) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum MakeCredentialRequestParsingError { + /// The client must throw an "EncodingError" DOMException. + #[error("Invalid JSON format: {0}")] + EncodingError(#[from] JsonError), +} + +impl WebAuthnIDL for MakeCredentialRequest { + type Error = MakeCredentialRequestParsingError; + type InnerModel = PublicKeyCredentialCreationOptionsJSON; +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct MakeCredentialPrfInput { + #[serde(rename = "eval")] + pub _eval: Option, } #[derive(Debug, Default, Clone, Serialize, PartialEq)] @@ -199,9 +277,11 @@ pub struct MakeCredentialPrfOutput { pub enabled: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct CredentialProtectionExtension { + #[serde(rename = "credentialProtectionPolicy")] pub policy: CredentialProtectionPolicy, + #[serde(rename = "enforceCredentialProtectionPolicy")] pub enforce_policy: bool, } @@ -254,13 +334,20 @@ pub struct CredentialPropsExtension { pub rk: Option, } -#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Default, Clone, Deserialize, PartialEq)] +pub struct MakeCredentialLargeBlobExtensionInput { + pub support: MakeCredentialLargeBlobExtension, +} + +#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] pub enum MakeCredentialLargeBlobExtension { - #[default] - None, + #[serde(rename = "preferred")] Preferred, + #[serde(rename = "required")] Required, + #[default] + #[serde(other)] + None, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] @@ -269,14 +356,22 @@ pub struct MakeCredentialLargeBlobExtensionOutput { pub supported: Option, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Deserialize, PartialEq)] pub struct MakeCredentialsRequestExtensions { + #[serde(rename = "credProps")] pub cred_props: Option, + #[serde(rename = "credProtect")] pub cred_protect: Option, - pub cred_blob: Option>, - pub large_blob: MakeCredentialLargeBlobExtension, + #[serde(rename = "credBlob")] + pub cred_blob: Option, + #[serde(rename = "largeBlob")] + pub large_blob: Option, + #[serde(rename = "minPinLength")] pub min_pin_length: Option, - pub hmac_or_prf: MakeCredentialHmacOrPrfInput, + #[serde(rename = "hmacCreateSecret")] + pub hmac_create_secret: Option, + #[serde(rename = "prf")] + pub prf: Option, } pub type MakeCredentialsResponseExtensions = Ctap2MakeCredentialsResponseExtensions; @@ -367,3 +462,178 @@ impl DowngradableRequest for MakeCredentialRequest { Ok(downgraded) } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::ops::webauthn::MakeCredentialRequest; + use crate::ops::webauthn::RelyingPartyId; + use crate::proto::ctap2::Ctap2PublicKeyCredentialType; + + use super::*; + + pub const REQUEST_BASE_JSON: &str = r#" + { + "rp": { + "id": "example.org", + "name": "example.org" + }, + "user": { + "id": "dXNlcmlk", + "name": "mario.rossi", + "displayName": "Mario Rossi" + }, + "challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu", + "pubKeyCredParams": [ + { + "type": "public-key", + "alg": -7 + } + ], + "timeout": 30000, + "excludeCredentials": [], + "authenticatorSelection": { + "residentKey": "discouraged", + "userVerification": "preferred" + }, + "attestation": "none", + "attestationFormats": ["packed", "fido-u2f"] + } + "#; + + fn request_base() -> MakeCredentialRequest { + MakeCredentialRequest { + origin: "example.org".to_string(), + hash: ClientData { + operation: Operation::MakeCredential, + challenge: base64_url::decode("Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu") + .unwrap(), + origin: "example.org".to_string(), + cross_origin: None, + } + .hash(), + relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), + user: Ctap2PublicKeyCredentialUserEntity::new(b"userid", "mario.rossi", "Mario Rossi"), + resident_key: Some(ResidentKeyRequirement::Discouraged), + user_verification: UserVerificationRequirement::Preferred, + algorithms: vec![Ctap2CredentialType::default()], + exclude: None, + extensions: None, + timeout: Duration::from_secs(30), + } + } + + fn json_field_add(str: &str, field: &str, value: &str) -> String { + let mut v: serde_json::Value = serde_json::from_str(str).unwrap(); + v.as_object_mut() + .unwrap() + .insert(field.to_owned(), serde_json::from_str(value).unwrap()); + serde_json::to_string(&v).unwrap() + } + + fn json_field_rm(str: &str, field: &str) -> String { + let mut v: serde_json::Value = serde_json::from_str(str).unwrap(); + v.as_object_mut().unwrap().remove(field); + serde_json::to_string(&v).unwrap() + } + + fn test_request_from_json_required_field(field: &str) { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_rm(REQUEST_BASE_JSON, field); + + let result = MakeCredentialRequest::from_json(&rpid, &req_json); + assert!(matches!( + result, + Err(MakeCredentialRequestParsingError::EncodingError(_)) + )); + } + + #[test] + fn test_request_from_json_base() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req: MakeCredentialRequest = + MakeCredentialRequest::from_json(&rpid, REQUEST_BASE_JSON).unwrap(); + assert_eq!(req, request_base()); + } + + #[test] + fn test_request_from_json_require_rp() { + test_request_from_json_required_field("rp"); + } + + #[test] + fn test_request_from_json_require_user() { + test_request_from_json_required_field("user"); + } + + #[test] + fn test_request_from_json_require_pub_key_cred_params() { + test_request_from_json_required_field("pubKeyCredParams"); + } + + #[test] + fn test_request_from_json_require_challenge() { + test_request_from_json_required_field("challenge"); + } + + #[test] + #[ignore] // FIXME(#134): Add validation for challenges + fn test_request_from_json_challenge_empty() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json: String = json_field_rm(REQUEST_BASE_JSON, "challenge"); + let req_json = json_field_add(&req_json, "challenge", r#""""#); + + let result = MakeCredentialRequest::from_json(&rpid, &req_json); + assert!(matches!( + result, + Err(MakeCredentialRequestParsingError::EncodingError(_)) + )); + } + + #[test] + fn test_request_from_json_prf_extension() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_add( + REQUEST_BASE_JSON, + "extensions", + r#"{"prf": {"eval": {"first": "second"}}}"#, + ); + + let req: MakeCredentialRequest = + MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + assert!(matches!( + req.extensions, + Some(MakeCredentialsRequestExtensions { prf: Some(_), .. }) + )); + } + + #[test] + fn test_request_from_json_unknown_pub_key_cred_params() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_add( + REQUEST_BASE_JSON, + "pubKeyCredParams", + r#"[{"type": "something", "alg": -12345}]"#, + ); + let req: MakeCredentialRequest = + MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + assert_eq!( + req.algorithms, + vec![Ctap2CredentialType { + algorithm: Ctap2COSEAlgorithmIdentifier::Unknown, // FIXME(#148): Passhtrough unknown algorithms + public_key_type: Ctap2PublicKeyCredentialType::Unknown, + }] + ); + } + + #[test] + fn test_request_from_json_default_timeout() { + let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let req_json = json_field_rm(REQUEST_BASE_JSON, "timeout"); + + let req: MakeCredentialRequest = + MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + assert_eq!(req.timeout, DEFAULT_TIMEOUT); + } +} diff --git a/libwebauthn/src/ops/webauthn.rs b/libwebauthn/src/ops/webauthn/mod.rs similarity index 81% rename from libwebauthn/src/ops/webauthn.rs rename to libwebauthn/src/ops/webauthn/mod.rs index 9ee0930..d390dc0 100644 --- a/libwebauthn/src/ops/webauthn.rs +++ b/libwebauthn/src/ops/webauthn/mod.rs @@ -1,4 +1,6 @@ +mod client_data; mod get_assertion; +pub mod idl; mod make_credential; use super::u2f::{RegisterRequest, SignRequest}; @@ -8,21 +10,32 @@ pub use get_assertion::{ GetAssertionLargeBlobExtension, GetAssertionLargeBlobExtensionOutput, GetAssertionPrfOutput, GetAssertionRequest, GetAssertionRequestExtensions, GetAssertionResponse, GetAssertionResponseExtensions, GetAssertionResponseUnsignedExtensions, HMACGetSecretInput, - HMACGetSecretOutput, PRFValue, + HMACGetSecretOutput, PRFValue, PrfInput, }; +pub use idl::{rpid::RelyingPartyId, Base64UrlString, WebAuthnIDL}; pub use make_credential::{ CredentialPropsExtension, CredentialProtectionExtension, CredentialProtectionPolicy, - MakeCredentialHmacOrPrfInput, MakeCredentialLargeBlobExtension, - MakeCredentialLargeBlobExtensionOutput, MakeCredentialPrfOutput, MakeCredentialRequest, - MakeCredentialResponse, MakeCredentialsRequestExtensions, MakeCredentialsResponseExtensions, + MakeCredentialLargeBlobExtension, MakeCredentialLargeBlobExtensionOutput, + MakeCredentialPrfInput, MakeCredentialPrfOutput, MakeCredentialRequest, MakeCredentialResponse, + MakeCredentialsRequestExtensions, MakeCredentialsResponseExtensions, MakeCredentialsResponseUnsignedExtensions, ResidentKeyRequirement, }; +use serde::Deserialize; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Deserialize, PartialEq)] +pub enum Operation { + MakeCredential, + GetAssertion, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq)] pub enum UserVerificationRequirement { + #[serde(rename = "required")] Required, - Preferred, + #[serde(rename = "discouraged")] Discouraged, + #[serde(rename = "preferred", other)] + Preferred, } impl UserVerificationRequirement { diff --git a/libwebauthn/src/ops/webauthn/timeout.rs b/libwebauthn/src/ops/webauthn/timeout.rs new file mode 100644 index 0000000..432e2b1 --- /dev/null +++ b/libwebauthn/src/ops/webauthn/timeout.rs @@ -0,0 +1,3 @@ +use std::time::Duration; + +pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index 9f8de49..054fa01 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -1,5 +1,5 @@ -use crate::pin::PinUvAuthProtocol; use crate::proto::ctap1::Ctap1Transport; +use crate::{ops::webauthn::idl::create::PublicKeyCredentialUserEntity, pin::PinUvAuthProtocol}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use serde_bytes::ByteBuf; @@ -113,6 +113,16 @@ impl Ctap2PublicKeyCredentialUserEntity { } } +impl From for Ctap2PublicKeyCredentialUserEntity { + fn from(user: PublicKeyCredentialUserEntity) -> Self { + Self { + id: ByteBuf::from(user.id), + name: Some(user.name), + display_name: Some(user.display_name), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum Ctap2PublicKeyCredentialType { #[serde(rename = "public-key")] @@ -143,7 +153,7 @@ impl From<&Ctap1Transport> for Ctap2Transport { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Ctap2PublicKeyCredentialDescriptor { pub id: ByteBuf, pub r#type: Ctap2PublicKeyCredentialType, diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 775d520..843b716 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -152,18 +152,17 @@ impl Ctap2GetAssertionRequest { ) -> Result { // Cloning it, so we can modify it let mut req = req.clone(); - if let Some(ext) = req.extensions.as_mut() { - // LargeBlob (NOTE: Not to be confused with LargeBlobKey) - // https://w3c.github.io/webauthn/#sctn-large-blob-extension - // If read is present and has the value true: - // [..] - // 3. If successful, set blob to the result. - // - // So we silently drop the extension if the device does not support it. - if !info.option_enabled("largeBlobs") { - ext.large_blob = GetAssertionLargeBlobExtension::None; - } + // LargeBlob (NOTE: Not to be confused with LargeBlobKey) + // https://w3c.github.io/webauthn/#sctn-large-blob-extension + // If read is present and has the value true: + // [..] + // 3. If successful, set blob to the result. + // + // So we silently drop the extension if the device does not support it. + if !info.option_enabled("largeBlobs") { + req.extensions.large_blob = None; } + Ok(Ctap2GetAssertionRequest::from(req)) } } @@ -174,7 +173,7 @@ impl From for Ctap2GetAssertionRequest { relying_party_id: op.relying_party_id, client_data_hash: ByteBuf::from(op.hash), allow: op.allow, - extensions: op.extensions.map(|x| x.into()), + extensions: Some(op.extensions.into()), options: Some(Ctap2GetAssertionOptions { require_user_presence: true, require_user_verification: op.user_verification.is_required(), @@ -185,11 +184,11 @@ impl From for Ctap2GetAssertionRequest { } } -#[derive(Debug, Default, Clone, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct Ctap2GetAssertionRequestExtensions { - #[serde(skip_serializing_if = "Option::is_none")] - pub cred_blob: Option, + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub cred_blob: bool, // Thanks, FIDO-spec for this consistent naming scheme... #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, @@ -197,16 +196,16 @@ pub struct Ctap2GetAssertionRequestExtensions { #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, #[serde(skip)] - pub hmac_or_prf: GetAssertionHmacOrPrfInput, + pub hmac_or_prf: Option, } impl From for Ctap2GetAssertionRequestExtensions { fn from(other: GetAssertionRequestExtensions) -> Self { Ctap2GetAssertionRequestExtensions { cred_blob: other.cred_blob, - hmac_secret: None, // Get's calculated later + hmac_secret: None, // Gets calculated later hmac_or_prf: other.hmac_or_prf, - large_blob_key: if other.large_blob == GetAssertionLargeBlobExtension::Read { + large_blob_key: if other.large_blob == Some(GetAssertionLargeBlobExtension::Read) { Some(true) } else { None @@ -217,7 +216,10 @@ impl From for Ctap2GetAssertionRequestExtensions impl Ctap2GetAssertionRequestExtensions { pub fn skip_serializing(&self) -> bool { - self.cred_blob.is_none() && self.hmac_secret.is_none() + !self.cred_blob + && self.hmac_secret.is_none() + && self.large_blob_key.is_none() + && self.hmac_or_prf.is_none() } pub fn calculate_hmac( @@ -226,14 +228,13 @@ impl Ctap2GetAssertionRequestExtensions { auth_data: &AuthTokenData, ) -> Result<(), Error> { let input = match &self.hmac_or_prf { - GetAssertionHmacOrPrfInput::None => None, - GetAssertionHmacOrPrfInput::HmacGetSecret(hmacget_secret_input) => { - Some(hmacget_secret_input.clone()) + None => None, + Some(GetAssertionHmacOrPrfInput::HmacGetSecret(hmac_get_secret_input)) => { + Some(hmac_get_secret_input.clone()) + } + Some(GetAssertionHmacOrPrfInput::Prf(prf_input)) => { + Self::prf_to_hmac_input(&prf_input.eval, &prf_input.eval_by_credential, allow_list)? } - GetAssertionHmacOrPrfInput::Prf { - eval, - eval_by_credential, - } => Self::prf_to_hmac_input(eval, eval_by_credential, allow_list)?, }; let input = match input { @@ -451,7 +452,7 @@ impl Ctap2UserVerifiableRequest for Ctap2GetAssertionRequest { let hmac_requested = self .extensions .as_ref() - .map(|e| !matches!(e.hmac_or_prf, GetAssertionHmacOrPrfInput::None)) + .map(|e| e.hmac_or_prf.is_some()) .unwrap_or_default(); hmac_requested && hmac_supported } @@ -506,44 +507,37 @@ impl Ctap2GetAssertionResponseExtensions { response: &Ctap2GetAssertionResponse, auth_data: Option<&AuthTokenData>, ) -> GetAssertionResponseUnsignedExtensions { - let (hmac_get_secret, prf) = if let Some(orig_ext) = &request.extensions { - // Decrypt the raw HMAC extension - let decrypted_hmac = self.hmac_secret.as_ref().and_then(|x| { - if let Some(auth_data) = auth_data { - let uv_proto = auth_data.protocol_version.create_protocol_object(); - x.decrypt_output(&auth_data.shared_secret, &uv_proto) - } else { - None - } - }); - if let Some(decrypted) = decrypted_hmac { - // Repackaging it into output - match &orig_ext.hmac_or_prf { - GetAssertionHmacOrPrfInput::None => (None, None), - GetAssertionHmacOrPrfInput::HmacGetSecret(..) => (Some(decrypted), None), - GetAssertionHmacOrPrfInput::Prf { .. } => ( - None, - Some(GetAssertionPrfOutput { - results: Some(PRFValue { - first: decrypted.output1, - second: decrypted.output2, - }), - }), - ), - } + let decrypted_hmac = self.hmac_secret.as_ref().and_then(|x| { + if let Some(auth_data) = auth_data { + let uv_proto = auth_data.protocol_version.create_protocol_object(); + x.decrypt_output(&auth_data.shared_secret, &uv_proto) } else { - (None, None) + None + } + }); + + let (hmac_get_secret, prf) = if let Some(decrypted) = decrypted_hmac { + match &request.extensions.hmac_or_prf { + None => (None, None), + Some(GetAssertionHmacOrPrfInput::HmacGetSecret(..)) => (Some(decrypted), None), + Some(GetAssertionHmacOrPrfInput::Prf(..)) => ( + None, + Some(GetAssertionPrfOutput { + results: Some(PRFValue { + first: decrypted.output1, + second: decrypted.output2, + }), + }), + ), } } else { (None, None) }; // LargeBlobs was requested - let large_blob = request - .extensions - .as_ref() - .filter(|x| x.large_blob != GetAssertionLargeBlobExtension::None) - .map(|_| { + let large_blob = match request.extensions.large_blob { + None => None, + Some(GetAssertionLargeBlobExtension::Read) => { Some(GetAssertionLargeBlobExtensionOutput { blob: response .large_blob_key @@ -552,8 +546,8 @@ impl Ctap2GetAssertionResponseExtensions { // Not yet supported // written: None, }) - }) - .unwrap_or_default(); + } + }; GetAssertionResponseUnsignedExtensions { hmac_get_secret, diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index afc2d02..739dc9f 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -7,8 +7,8 @@ use super::{ use crate::{ fido::AuthenticatorData, ops::webauthn::{ - CredentialProtectionPolicy, MakeCredentialHmacOrPrfInput, MakeCredentialLargeBlobExtension, - MakeCredentialRequest, MakeCredentialResponse, MakeCredentialsRequestExtensions, + CredentialProtectionPolicy, MakeCredentialLargeBlobExtension, MakeCredentialRequest, + MakeCredentialResponse, MakeCredentialsRequestExtensions, MakeCredentialsResponseUnsignedExtensions, ResidentKeyRequirement, }, pin::PinUvAuthProtocol, @@ -225,8 +225,12 @@ impl Ctap2MakeCredentialsRequestExtensions { // LargeBlob (NOTE: Not to be confused with LargeBlobKey. LargeBlob has "Preferred" as well) // https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/WebAuthn_extensions#largeblob // - let large_blob_key = match requested_extensions.large_blob { - MakeCredentialLargeBlobExtension::Required => { + let large_blob_key = match requested_extensions + .large_blob + .as_ref() + .map(|info| info.support) + { + Some(MakeCredentialLargeBlobExtension::Required) => { // "required": The credential will be created with an authenticator to store blobs. The create() call will fail if this is impossible. if !info.option_enabled("largeBlobs") { warn!("This request will potentially fail. Large blob extension required, but device does not support it."); @@ -235,7 +239,7 @@ impl Ctap2MakeCredentialsRequestExtensions { // We only add a warning for easier debugging. Some(true) } - MakeCredentialLargeBlobExtension::Preferred => { + Some(MakeCredentialLargeBlobExtension::Preferred) => { if info.option_enabled("largeBlobs") { Some(true) } else { @@ -244,19 +248,23 @@ impl Ctap2MakeCredentialsRequestExtensions { None } } - MakeCredentialLargeBlobExtension::None => None, + _ => None, }; // HMAC Secret - let hmac_secret = match requested_extensions.hmac_or_prf { - MakeCredentialHmacOrPrfInput::None => None, - MakeCredentialHmacOrPrfInput::HmacGetSecret | MakeCredentialHmacOrPrfInput::Prf => { - Some(true) - } + let hmac_secret = if requested_extensions.hmac_create_secret == Some(true) + || requested_extensions.prf.is_some() + { + Some(true) + } else { + None }; Ok(Ctap2MakeCredentialsRequestExtensions { - cred_blob: requested_extensions.cred_blob.clone(), + cred_blob: requested_extensions + .cred_blob + .as_ref() + .map(|inner| inner.0.clone()), hmac_secret, cred_protect: requested_extensions .cred_protect diff --git a/libwebauthn/src/tests/basic_ctap2.rs b/libwebauthn/src/tests/basic_ctap2.rs index cc62d40..b351efe 100644 --- a/libwebauthn/src/tests/basic_ctap2.rs +++ b/libwebauthn/src/tests/basic_ctap2.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::ops::webauthn::GetAssertionRequest; +use crate::ops::webauthn::{GetAssertionRequest, GetAssertionRequestExtensions}; use crate::proto::ctap2::Ctap2PublicKeyCredentialDescriptor; use crate::transport::hid::get_virtual_device; use crate::transport::{Channel, Device}; @@ -65,7 +65,7 @@ async fn test_webauthn_basic_ctap2() { hash: Vec::from(challenge), allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, - extensions: None, + extensions: GetAssertionRequestExtensions::default(), timeout: TIMEOUT, };