From 9767b27518c71ddbc0d24d0558232467cf38ffb7 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Thu, 27 Apr 2023 14:16:55 -0700 Subject: [PATCH] Adds support for YubiHSM Auth This adds support for the YubiHSM Auth protocol as described in https://docs.yubico.com/yesdk/users-manual/application-yubihsm-auth/interacting-yubihsm-2.html This protocol ensure the derivation password for the authentication keys are kept in secure devices. Signed-off-by: Arthur Gautier --- .github/workflows/ci.yml | 26 +- Cargo.lock | 541 ++++++++++++++++++++++++- Cargo.toml | 2 + src/client.rs | 29 ++ src/session.rs | 120 +++++- src/session/securechannel.rs | 203 +++++++--- src/session/securechannel/challenge.rs | 40 ++ src/session/securechannel/context.rs | 7 + 8 files changed, 879 insertions(+), 89 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e283f2e3..3b03cf99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,15 +11,22 @@ env: jobs: build: - runs-on: ubuntu-latest strategy: matrix: - platform: - - ubuntu-latest - - macos-latest - toolchain: - - stable - - 1.67.0 # MSRV + include: + - platform: ubuntu-latest + toolchain: stable + deps: sudo apt-get install libpcsclite-dev + - platform: macos-latest + toolchain: stable + deps: true + - platform: ubuntu-latest + toolchain: 1.67.0 # MSRV + deps: sudo apt-get install libpcsclite-dev + - platform: macos-latest + toolchain: 1.67.0 # MSRV + deps: true + runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v1 - name: cache .cargo/registry @@ -41,14 +48,15 @@ jobs: with: toolchain: ${{ matrix.toolchain }} override: true + - run: ${{ matrix.deps }} - run: cargo build --release - run: cargo build --release --no-default-features - run: cargo build --release --no-default-features --features=passwords - run: cargo build --release --features=usb + - run: cargo build --release --features=yubihsm-auth - run: cargo build --benches test: - runs-on: ubuntu-latest strategy: matrix: platform: @@ -57,6 +65,7 @@ jobs: toolchain: - stable - 1.67.0 # MSRV + runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v1 - name: cache .cargo/registry @@ -103,6 +112,7 @@ jobs: toolchain: 1.71.0 # pinned to prevent CI breakages components: clippy override: true + - run: sudo apt-get install libpcsclite-dev - uses: actions-rs/cargo@v1 with: command: clippy diff --git a/Cargo.lock b/Cargo.lock index c81c064c..4cfbc5bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,66 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ascii" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.25", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -41,12 +95,24 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.0" @@ -71,6 +137,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + [[package]] name = "byteorder" version = "1.4.3" @@ -113,6 +185,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + [[package]] name = "chunked_transfer" version = "1.4.1" @@ -146,6 +233,18 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "cookie-factory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.9" @@ -212,9 +311,15 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "dbl" version = "0.3.2" @@ -235,6 +340,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.3.7" @@ -244,6 +363,15 @@ dependencies = [ "serde", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + [[package]] name = "digest" version = "0.10.7" @@ -256,6 +384,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "ecdsa" version = "0.16.8" @@ -306,6 +445,7 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core", @@ -349,7 +489,7 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -363,6 +503,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -378,6 +527,29 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "inout" version = "0.1.3" @@ -394,6 +566,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "k256" version = "0.13.1" @@ -447,6 +628,39 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -460,6 +674,7 @@ dependencies = [ "num-iter", "num-traits", "rand", + "serde", "smallvec", "zeroize", ] @@ -495,6 +710,15 @@ dependencies = [ "libm", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -535,6 +759,25 @@ dependencies = [ "hmac", ] +[[package]] +name = "pcsc" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37cab0be9d04e808a8d8059fa54befcd71dc8b168f9f0c04bdb7e59832abbab4" +dependencies = [ + "bitflags 1.3.2", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b7bfecba2c0f1b5efb0e7caf7533ab1c295024165bcbb066231f60d33e23ea" +dependencies = [ + "pkg-config", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -616,6 +859,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", "rand_chacha", "rand_core", ] @@ -690,6 +934,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "ryu" version = "1.0.15" @@ -710,6 +963,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + [[package]] name = "semver" version = "1.0.18" @@ -733,7 +995,7 @@ checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -747,6 +1009,17 @@ dependencies = [ "serde", ] +[[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.7" @@ -777,7 +1050,7 @@ checksum = "acdf373908a51854e3c302a3cad677fadcf719255d4d3bd844405e379e9415fc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] @@ -808,6 +1081,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.28" @@ -819,6 +1103,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "thiserror" version = "1.0.44" @@ -836,7 +1132,18 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", ] [[package]] @@ -846,6 +1153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ "deranged", + "itoa", "serde", "time-core", "time-macros", @@ -890,12 +1198,19 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "uuid" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ + "getrandom", "serde", ] @@ -911,18 +1226,194 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[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-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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" +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.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" + +[[package]] +name = "x509" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3cec94c3999f31341553f358ef55f65fc031291a022cd42ec0ce7219560c76" +dependencies = [ + "chrono", + "cookie-factory", +] + +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time 0.3.25", +] + [[package]] name = "yubihsm" version = "0.42.1" dependencies = [ "aes", - "bitflags", + "bitflags 2.4.0", "cbc", "ccm", "cmac", @@ -946,9 +1437,43 @@ dependencies = [ "signature", "subtle", "thiserror", - "time", + "time 0.3.25", "tiny_http", "uuid", + "yubikey", + "zeroize", +] + +[[package]] +name = "yubikey" +version = "0.8.0-pre.0" +source = "git+https://github.com/baloo/yubikey.rs?branch=baloo/yubihsm-auth#ed78a52992b65f25d954b224601ce3bb7626279e" +dependencies = [ + "base16ct", + "chrono", + "cookie-factory", + "der-parser", + "des", + "elliptic-curve", + "hmac", + "log", + "nom", + "num-bigint-dig", + "num-integer", + "num-traits", + "p256", + "p384", + "pbkdf2", + "pcsc", + "rand_core", + "rsa", + "secrecy", + "sha1", + "sha2", + "subtle", + "uuid", + "x509", + "x509-parser", "zeroize", ] @@ -969,5 +1494,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] diff --git a/Cargo.toml b/Cargo.toml index 1134cf96..9445739c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ serde_json = { version = "1", optional = true } rusb = { version = "0.9", optional = true } sha2 = { version = "0.10", optional = true } tiny_http = { version = "0.12", optional = true } +yubikey = { git = "https://github.com/baloo/yubikey.rs", branch = "baloo/yubihsm-auth", optional = true } [dev-dependencies] ed25519-dalek = "2" @@ -63,6 +64,7 @@ secp256k1 = ["k256"] setup = ["passwords", "serde_json", "uuid/serde"] untested = ["sha2"] usb = ["rusb"] +yubihsm-auth = ["yubikey"] [package.metadata.docs.rs] all-features = true diff --git a/src/client.rs b/src/client.rs index 038e63fd..60babd12 100644 --- a/src/client.rs +++ b/src/client.rs @@ -43,6 +43,9 @@ use std::{ #[cfg(feature = "passwords")] use std::{thread, time::SystemTime}; +#[cfg(feature = "yubihsm-auth")] +use crate::session::PendingSession; + #[cfg(feature = "untested")] use { crate::{ @@ -106,6 +109,20 @@ impl Client { Ok(client) } + /// Open session with YubiHSM Auth scheme + #[cfg(feature = "yubihsm-auth")] + pub fn yubihsm_auth( + connector: Connector, + authentication_key_id: object::Id, + host_challenge: session::securechannel::Challenge, + ) -> Result { + let timeout = session::Timeout::default(); + + let session = + PendingSession::new(connector, timeout, authentication_key_id, host_challenge)?; + Ok(session) + } + /// Borrow this client's YubiHSM connector (which is `Clone`able) pub fn connector(&self) -> &Connector { &self.connector @@ -1165,3 +1182,15 @@ impl Client { .0) } } + +impl From for Client { + fn from(session: Session) -> Self { + let connector = session.connector(); + let session = Arc::new(Mutex::new(Some(session))); + Self { + connector, + session, + credentials: None, + } + } +} diff --git a/src/session.rs b/src/session.rs index 6bd14231..883c4c7d 100644 --- a/src/session.rs +++ b/src/session.rs @@ -17,6 +17,7 @@ pub use self::{ error::{Error, ErrorKind}, guard::Guard, id::Id, + securechannel::{Challenge, Context, SessionKeys}, timeout::Timeout, }; @@ -30,6 +31,9 @@ use crate::{ }; use std::time::{Duration, Instant}; +#[cfg(feature = "yubihsm-auth")] +use crate::object; + /// Timeout fuzz factor: to avoid races/skew with the YubiHSM's clock, /// we consider sessions to be timed out slightly earlier than the actual /// timeout. This should (hopefully) ensure we always time out first, @@ -37,6 +41,98 @@ use std::time::{Duration, Instant}; /// than opaque "lost connection to HSM"-style errors. const TIMEOUT_FUZZ_FACTOR: Duration = Duration::from_secs(1); +/// Session created on the device for which we do not +/// have credentials for yet. +/// +/// This is used for YubiHSM Auth scheme support. +#[cfg(feature = "yubihsm-auth")] +pub struct PendingSession { + ///// HSM Public key + //card_public_key: PublicKey, + /// Connector which communicates with the HSM (HTTP or USB) + connector: Connector, + + /// Session creation timestamp + created_at: Instant, + + /// Timestamp when this session was last active + last_active: Instant, + + /// Inactivity timeout for this session + timeout: Timeout, + + /// Challenge generate by the HSM. + hsm_challenge: Challenge, + + /// ID for this session + id: Id, + + context: Context, +} + +#[cfg(feature = "yubihsm-auth")] +impl PendingSession { + /// Creates a new session with the device. + pub fn new( + connector: Connector, + timeout: Timeout, + authentication_key_id: object::Id, + host_challenge: Challenge, + ) -> Result { + let (id, session_response) = + SecureChannel::create(&connector, authentication_key_id, host_challenge)?; + + let hsm_challenge = session_response.card_challenge; + let context = Context::from_challenges(host_challenge, hsm_challenge); + + let created_at = Instant::now(); + let last_active = Instant::now(); + + Ok(PendingSession { + id, + connector, + created_at, + last_active, + timeout, + context, + hsm_challenge, + }) + } + + /// Create the session with the provided session keys + pub fn realize(self, session_keys: SessionKeys) -> Result { + let secure_channel = Some(SecureChannel::with_session_keys( + self.id, + self.context, + session_keys, + )); + + let mut session = Session { + id: self.id, + secure_channel, + connector: self.connector, + created_at: self.created_at, + last_active: self.last_active, + timeout: self.timeout, + }; + + let response = session.start_authenticate()?; + session.finish_authenticate_session(&response)?; + + Ok(session) + } + + /// Return the challenge emitted by the HSM when opening the session + pub fn get_challenge(&self) -> Challenge { + self.hsm_challenge + } + + /// Return the id of the session + pub fn id(&self) -> Id { + self.id + } +} + /// Authenticated and encrypted (SCP03) `Session` with the HSM. A `Session` is /// needed to perform any command. /// @@ -247,13 +343,9 @@ impl Session { credentials.authentication_key_id ); - let command = self.secure_channel()?.authenticate_session()?; - let response = self.send_message(command)?; + let response = self.start_authenticate()?; - if let Err(e) = self - .secure_channel()? - .finish_authenticate_session(&response) - { + if let Err(e) = self.finish_authenticate_session(&response) { session_error!( self, "failed={:?} key={} err={:?}", @@ -269,10 +361,26 @@ impl Session { Ok(()) } + /// Send the message to the card to start authentication + fn start_authenticate(&mut self) -> Result { + let command = self.secure_channel()?.authenticate_session()?; + self.send_message(command) + } + + /// Read authenticate session message from the card + fn finish_authenticate_session(&mut self, response: &response::Message) -> Result<(), Error> { + self.secure_channel()?.finish_authenticate_session(response) + } + /// Get the underlying channel or return an error fn secure_channel(&mut self) -> Result<&mut SecureChannel, Error> { self.secure_channel .as_mut() .ok_or_else(|| format_err!(ErrorKind::ClosedError, "session is already closed").into()) } + + /// Get the underlying connector used by this session + pub(crate) fn connector(&self) -> Connector { + self.connector.clone() + } } diff --git a/src/session/securechannel.rs b/src/session/securechannel.rs index 05c2e146..95dc3ea1 100644 --- a/src/session/securechannel.rs +++ b/src/session/securechannel.rs @@ -24,9 +24,9 @@ mod cryptogram; mod kdf; mod mac; +pub use self::{challenge::Challenge, context::Context}; pub(crate) use self::{ - challenge::{Challenge, CHALLENGE_SIZE}, - context::Context, + challenge::CHALLENGE_SIZE, cryptogram::{Cryptogram, CRYPTOGRAM_SIZE}, mac::Mac, }; @@ -35,7 +35,7 @@ use crate::{ authentication::{self, Credentials}, command, connector::Connector, - device, response, + device, object, response, serialization::deserialize, session::{self, ErrorKind}, }; @@ -47,6 +47,7 @@ use aes::{ Aes128, }; use cmac::{digest::Mac as _, Cmac}; +use serde::Serialize; use subtle::ConstantTimeEq; use zeroize::{Zeroize, Zeroizing}; @@ -70,6 +71,42 @@ const AES_BLOCK_SIZE: usize = 16; type Aes128CbcEnc = cbc::Encryptor; type Aes128CbcDec = cbc::Decryptor; +/// SCP03 AES Session Keys +#[derive(Serialize)] +pub struct SessionKeys { + /// Session encryption key (S-ENC) + pub enc_key: [u8; KEY_SIZE], + + /// Session Command MAC key (S-MAC) + pub mac_key: [u8; KEY_SIZE], + + /// Session Respose MAC key (S-RMAC) + pub rmac_key: [u8; KEY_SIZE], +} + +impl Zeroize for SessionKeys { + fn zeroize(&mut self) { + self.enc_key.zeroize(); + self.mac_key.zeroize(); + self.rmac_key.zeroize(); + } +} + +#[cfg(feature = "yubihsm-auth")] +impl From for SessionKeys { + fn from(keys: yubikey::hsmauth::SessionKeys) -> Self { + let enc_key = *keys.enc_key; + let mac_key = *keys.mac_key; + let rmac_key = *keys.rmac_key; + + Self { + enc_key, + mac_key, + rmac_key, + } + } +} + /// SCP03 Secure Channel pub(crate) struct SecureChannel { /// ID of this channel (a.k.a. session ID) @@ -85,14 +122,8 @@ pub(crate) struct SecureChannel { /// Context (card + host challenges) context: Context, - /// Session encryption key (S-ENC) - enc_key: [u8; KEY_SIZE], - - /// Session Command MAC key (S-MAC) - mac_key: [u8; KEY_SIZE], - - /// Session Respose MAC key (S-RMAC) - rmac_key: [u8; KEY_SIZE], + /// Session keys + session_keys: SessionKeys, /// Chaining value to be included when computing MACs mac_chaining_value: [u8; Mac::BYTE_SIZE * 2], @@ -107,45 +138,8 @@ impl SecureChannel { ) -> Result { let host_challenge = Challenge::new(); - let command_message = command::Message::from(&CreateSessionCommand { - authentication_key_id: credentials.authentication_key_id, - host_challenge, - }); - - let uuid = command_message.uuid; - let response_body = connector.send_message(uuid, command_message.into())?; - let response_message = response::Message::parse(response_body)?; - - if response_message.is_err() { - match device::ErrorKind::from_response_message(&response_message) { - Some(device::ErrorKind::ObjectNotFound) => fail!( - ErrorKind::AuthenticationError, - "auth key not found: 0x{:04x}", - credentials.authentication_key_id - ), - Some(kind) => return Err(kind.into()), - None => fail!( - ErrorKind::ResponseError, - "HSM error: {:?}", - response_message.code - ), - } - } - - if response_message.command().unwrap() != command::Code::CreateSession { - fail!( - ErrorKind::ProtocolError, - "command type mismatch: expected {:?}, got {:?}", - command::Code::CreateSession, - response_message.command().unwrap() - ); - } - - let id = response_message - .session_id - .ok_or_else(|| format_err!(ErrorKind::CreateFailed, "no session ID in response"))?; - - let session_response: CreateSessionResponse = deserialize(response_message.data.as_ref())?; + let (id, session_response) = + Self::create(connector, credentials.authentication_key_id, host_challenge)?; // Derive session keys from the combination of host and card challenges. // If either of them are incorrect (indicating a key mismatch) it will @@ -185,6 +179,20 @@ impl SecureChannel { let enc_key = derive_key(authentication_key.enc_key(), 0b100, &context); let mac_key = derive_key(authentication_key.mac_key(), 0b110, &context); let rmac_key = derive_key(authentication_key.mac_key(), 0b111, &context); + + let session_keys = SessionKeys { + enc_key, + mac_key, + rmac_key, + }; + Self::with_session_keys(id, context, session_keys) + } + + pub(crate) fn with_session_keys( + id: session::Id, + context: Context, + session_keys: SessionKeys, + ) -> Self { let mac_chaining_value = [0u8; Mac::BYTE_SIZE * 2]; Self { @@ -192,13 +200,62 @@ impl SecureChannel { counter: 0, security_level: SecurityLevel::None, context, - enc_key, - mac_key, - rmac_key, + session_keys, mac_chaining_value, } } + /// Open a SecureChannel with the HSM. This will not complete authentication. + /// + /// This will return the session id as well as the card challenge. + pub(crate) fn create( + connector: &Connector, + authentication_key_id: object::Id, + host_challenge: Challenge, + ) -> Result<(session::Id, CreateSessionResponse), session::Error> { + let command_message = command::Message::from(&CreateSessionCommand { + authentication_key_id, //: credentials.authentication_key_id, + host_challenge, + }); + + let uuid = command_message.uuid; + let response_body = connector.send_message(uuid, command_message.into())?; + let response_message = response::Message::parse(response_body)?; + + if response_message.is_err() { + match device::ErrorKind::from_response_message(&response_message) { + Some(device::ErrorKind::ObjectNotFound) => fail!( + ErrorKind::AuthenticationError, + "auth key not found: 0x{:04x}", + authentication_key_id + ), + Some(kind) => return Err(kind.into()), + None => fail!( + ErrorKind::ResponseError, + "HSM error: {:?}", + response_message.code + ), + } + } + + if response_message.command().unwrap() != command::Code::CreateSession { + fail!( + ErrorKind::ProtocolError, + "command type mismatch: expected {:?}, got {:?}", + command::Code::CreateSession, + response_message.command().unwrap() + ); + } + + let id = response_message + .session_id + .ok_or_else(|| format_err!(ErrorKind::CreateFailed, "no session ID in response"))?; + + let session_response: CreateSessionResponse = deserialize(response_message.data.as_ref())?; + + Ok((id, session_response)) + } + /// Get the channel (i.e. session) ID pub fn id(&self) -> session::Id { self.id @@ -207,14 +264,24 @@ impl SecureChannel { /// Calculate the card's cryptogram for this session pub fn card_cryptogram(&self) -> Cryptogram { let mut result_bytes = Zeroizing::new([0u8; CRYPTOGRAM_SIZE]); - kdf::derive(&self.mac_key, 0, &self.context, result_bytes.as_mut()); + kdf::derive( + &self.session_keys.mac_key, + 0, + &self.context, + result_bytes.as_mut(), + ); Cryptogram::from_slice(result_bytes.as_ref()) } /// Calculate the host's cryptogram for this session pub fn host_cryptogram(&self) -> Cryptogram { let mut result_bytes = Zeroizing::new([0u8; CRYPTOGRAM_SIZE]); - kdf::derive(&self.mac_key, 1, &self.context, result_bytes.as_mut()); + kdf::derive( + &self.session_keys.mac_key, + 1, + &self.context, + result_bytes.as_mut(), + ); Cryptogram::from_slice(result_bytes.as_ref()) } @@ -233,7 +300,8 @@ impl SecureChannel { ); } - let mut mac = as KeyInit>::new_from_slice(self.mac_key.as_ref()).unwrap(); + let mut mac = + as KeyInit>::new_from_slice(self.session_keys.mac_key.as_ref()).unwrap(); mac.update(&self.mac_chaining_value); mac.update(&[command_type.to_u8()]); @@ -298,7 +366,7 @@ impl SecureChannel { // Provide space at the end of the vec for the padding message.extend_from_slice(&[0u8; AES_BLOCK_SIZE]); - let cipher = Aes128::new_from_slice(&self.enc_key).unwrap(); + let cipher = Aes128::new_from_slice(&self.session_keys.enc_key).unwrap(); let icv = compute_icv(&cipher, self.counter); let cbc_encryptor = Aes128CbcEnc::inner_iv_init(cipher, &icv); let ciphertext = cbc_encryptor @@ -315,7 +383,7 @@ impl SecureChannel { ) -> Result { assert_eq!(self.security_level, SecurityLevel::Authenticated); - let cipher = Aes128::new_from_slice(&self.enc_key).unwrap(); + let cipher = Aes128::new_from_slice(&self.session_keys.enc_key).unwrap(); let icv = compute_icv(&cipher, self.counter); self.verify_response_mac(&encrypted_response)?; @@ -364,7 +432,8 @@ impl SecureChannel { ); } - let mut mac = as KeyInit>::new_from_slice(self.rmac_key.as_ref()).unwrap(); + let mut mac = + as KeyInit>::new_from_slice(self.session_keys.rmac_key.as_ref()).unwrap(); mac.update(&self.mac_chaining_value); mac.update(&[response.code.to_u8()]); @@ -441,12 +510,12 @@ impl SecureChannel { ) -> Result { assert_eq!(self.security_level, SecurityLevel::Authenticated); - let cipher = Aes128::new_from_slice(&self.enc_key).unwrap(); + let cipher = Aes128::new_from_slice(&self.session_keys.enc_key).unwrap(); let icv = compute_icv(&cipher, self.counter); self.verify_command_mac(&encrypted_command)?; - let cipher = Aes128::new_from_slice(&self.enc_key).unwrap(); + let cipher = Aes128::new_from_slice(&self.session_keys.enc_key).unwrap(); let cbc_decryptor = Aes128CbcDec::inner_iv_init(cipher, &icv); let mut command_data = encrypted_command.data; @@ -479,7 +548,8 @@ impl SecureChannel { command.session_id ); - let mut mac = as KeyInit>::new_from_slice(self.mac_key.as_ref()).unwrap(); + let mut mac = + as KeyInit>::new_from_slice(self.session_keys.mac_key.as_ref()).unwrap(); mac.update(&self.mac_chaining_value); mac.update(&[command.command_type.to_u8()]); @@ -519,7 +589,7 @@ impl SecureChannel { // Provide space at the end of the vec for the padding message.extend_from_slice(&[0u8; AES_BLOCK_SIZE]); - let cipher = Aes128::new_from_slice(&self.enc_key).unwrap(); + let cipher = Aes128::new_from_slice(&self.session_keys.enc_key).unwrap(); let icv = compute_icv(&cipher, self.counter); let cbc_encryptor = Aes128CbcEnc::inner_iv_init(cipher, &icv); @@ -548,7 +618,8 @@ impl SecureChannel { assert_eq!(self.security_level, SecurityLevel::Authenticated); let body = response_data.into(); - let mut mac = as KeyInit>::new_from_slice(self.rmac_key.as_ref()).unwrap(); + let mut mac = + as KeyInit>::new_from_slice(self.session_keys.rmac_key.as_ref()).unwrap(); mac.update(&self.mac_chaining_value); mac.update(&[code.to_u8()]); @@ -584,9 +655,7 @@ impl SecureChannel { /// Terminate the session fn terminate(&mut self) { self.security_level = SecurityLevel::Terminated; - self.enc_key.zeroize(); - self.mac_key.zeroize(); - self.rmac_key.zeroize(); + self.session_keys.zeroize(); } } diff --git a/src/session/securechannel/challenge.rs b/src/session/securechannel/challenge.rs index dc03c4ee..3069bb8f 100644 --- a/src/session/securechannel/challenge.rs +++ b/src/session/securechannel/challenge.rs @@ -3,6 +3,9 @@ use rand_core::{OsRng, RngCore}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "yubihsm-auth")] +use crate::session::error::{Error, ErrorKind}; + /// Size of a challenge message pub const CHALLENGE_SIZE: usize = 8; @@ -35,4 +38,41 @@ impl Challenge { pub fn as_slice(&self) -> &[u8] { &self.0 } + + /// Creates `Challenge` from a `yubikey::hsmauth::Challenge`. + /// + /// `YubiKey` firmware 5.4.3 will generate an empty challenge, this will + /// generate one from RNG if we're provided an empty challenge + // Note(baloo): because of the side-effect described above, this is not + // made a regular From. + #[cfg(feature = "yubihsm-auth")] + pub fn from_yubikey_challenge(yc: yubikey::hsmauth::Challenge) -> Self { + if yc.is_empty() { + Self::new() + } else { + let mut challenge = [0u8; CHALLENGE_SIZE]; + challenge.copy_from_slice(yc.as_slice()); + Challenge(challenge) + } + } +} + +#[cfg(feature = "yubihsm-auth")] +impl TryFrom for yubikey::hsmauth::Challenge { + type Error = Error; + + fn try_from(c: Challenge) -> Result { + let mut challenge = yubikey::hsmauth::Challenge::default(); + challenge + .copy_from_slice(c.as_slice()) + .map_err(|e| Error::from(ErrorKind::ProtocolError.context(e)))?; + + Ok(challenge) + } +} + +impl Default for Challenge { + fn default() -> Self { + Self::new() + } } diff --git a/src/session/securechannel/context.rs b/src/session/securechannel/context.rs index 1a50769c..e84a34d1 100644 --- a/src/session/securechannel/context.rs +++ b/src/session/securechannel/context.rs @@ -22,3 +22,10 @@ impl Context { &self.0 } } + +#[cfg(feature = "yubihsm-auth")] +impl From for yubikey::hsmauth::Context { + fn from(context: Context) -> Self { + Self::from_buf(context.0) + } +}