diff --git a/.cargo/audit.toml b/.cargo/audit.toml index c36407a0..a88a8142 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -4,4 +4,5 @@ ignore = [ "RUSTSEC-2019-0036", # failure: type confusion if __private_get_type_id__ is overridden "RUSTSEC-2020-0036", # failure is officially deprecated/unmaintained + "RUSTSEC-2023-0071", # rsa marvin attack, waiting for an upstream fix (rsa package is used by hashicorp feature) ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bd2387f..55c3825d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: matrix: toolchain: - stable - - 1.74.0 # MSRV + - 1.74.0 # MSRV runs-on: ubuntu-latest steps: - name: Checkout sources @@ -91,11 +91,24 @@ jobs: test: name: Test Suite + services: + vault: + image: vault:1.13.3 + ports: + - "8400:8400" + env: + VAULT_DEV_ROOT_TOKEN_ID: test + VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8400 + options: >- + --health-cmd "vault status -address='http://127.0.0.1:8400'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: matrix: toolchain: - stable - - 1.74.0 # MSRV + - 1.74.0 # MSRV runs-on: ubuntu-latest steps: - name: Checkout sources @@ -128,8 +141,17 @@ jobs: - name: Install libudev-dev run: sudo apt-get update && sudo apt-get install libudev-dev + # used by integration test to configure running hashicorp vault container + - name: Install HashiCorp vault CLI + run: wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg && + gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint && + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list && + sudo apt update && sudo apt install vault + - name: Run cargo test uses: actions-rs/cargo@v1 + env: + NO_VAULT_SERVER: true with: command: test args: --all-features -- --test-threads 1 @@ -222,7 +244,7 @@ jobs: - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: - toolchain: 1.74.0 # MSRV + toolchain: 1.74.0 # MSRV override: true - name: Install libudev-dev diff --git a/.gitignore b/.gitignore index e134f360..02a38871 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ tmkms.toml *.swp \.idea/ +/state +/secrets +/.vscode +**/*.bin diff --git a/Cargo.lock b/Cargo.lock index 9f42bda6..d6ce6919 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -77,6 +83,29 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" +dependencies = [ + "aes", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -168,12 +197,49 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-rs" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -184,7 +250,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -207,12 +273,41 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.85", + "which", +] + [[package]] name = "bip32" version = "0.5.2" @@ -222,7 +317,7 @@ dependencies = [ "bs58", "hmac", "k256", - "rand_core", + "rand_core 0.6.4", "ripemd", "sha2 0.10.8", "subtle", @@ -319,6 +414,8 @@ version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -334,6 +431,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -401,6 +507,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.20" @@ -452,6 +569,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -471,6 +597,22 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + [[package]] name = "const-oid" version = "0.9.6" @@ -515,7 +657,7 @@ dependencies = [ "ecdsa", "eyre", "k256", - "rand_core", + "rand_core 0.6.4", "serde", "serde_json", "signature", @@ -526,13 +668,32 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -540,7 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -552,7 +713,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -600,7 +761,7 @@ checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" dependencies = [ "byteorder", "digest 0.9.0", - "rand_core", + "rand_core 0.6.4", "subtle-ng", "zeroize", ] @@ -614,13 +775,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid 0.7.1", + "crypto-bigint 0.3.2", + "pem-rfc7468", +] + [[package]] name = "der" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ - "const-oid", + "const-oid 0.9.6", "zeroize", ] @@ -650,23 +822,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", + "const-oid 0.9.6", "crypto-common", "subtle", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.9", "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", - "spki", + "spki 0.7.3", ] [[package]] @@ -675,7 +853,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "signature", ] @@ -687,7 +865,7 @@ checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" dependencies = [ "curve25519-dalek-ng", "hex 0.4.3", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.9.9", "thiserror", @@ -702,7 +880,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.10.8", "subtle", @@ -722,13 +900,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", - "crypto-bigint", + "crypto-bigint 0.5.5", "digest 0.10.7", "ff", "generic-array", "group", - "pkcs8", - "rand_core", + "pkcs8 0.10.2", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -794,7 +972,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -804,6 +982,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "flex-error" version = "0.4.4" @@ -862,6 +1050,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -946,6 +1140,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -954,7 +1159,17 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -963,6 +1178,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.13.0" @@ -970,7 +1191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1068,7 +1289,7 @@ checksum = "1e013a4f0b8772418eee1fc462e74017aba13c364a7b61bd3df1ddcbfe47b065" dependencies = [ "hmac", "once_cell", - "rand_core", + "rand_core 0.6.4", "sha2 0.10.8", "zeroize", ] @@ -1091,6 +1312,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1245,6 +1475,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.72" @@ -1282,6 +1521,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "ledger" @@ -1305,6 +1553,22 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if 1.0.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libusb1-sys" version = "0.7.0" @@ -1368,7 +1632,7 @@ checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", "keccak", - "rand_core", + "rand_core 0.6.4", "zeroize", ] @@ -1378,6 +1642,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -1387,6 +1657,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.2" @@ -1395,10 +1674,34 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "mockito" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f9fece9bd97ab74339fe19f4bcaf52b76dcc18e5364c7977c1838f76b38de9" +dependencies = [ + "assert-json-diff", + "colored", + "httparse", + "lazy_static", + "log", + "rand 0.8.5", + "regex", + "serde_json", + "serde_urlencoded", + "similar", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -1429,6 +1732,16 @@ dependencies = [ "void", ] +[[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 = "nu-ansi-term" version = "0.46.0" @@ -1439,6 +1752,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1456,6 +1786,26 @@ dependencies = [ "syn 2.0.85", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1463,6 +1813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1582,6 +1933,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -1606,14 +1966,36 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" +dependencies = [ + "der 0.5.1", + "pkcs8 0.8.0", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der 0.5.1", + "spki 0.5.4", + "zeroize", +] + [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.9", + "spki 0.7.3", ] [[package]] @@ -1633,6 +2015,18 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1648,6 +2042,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.85", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1713,6 +2117,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -1720,8 +2137,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -1731,7 +2158,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -1740,7 +2176,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -1797,6 +2242,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "getrandom 0.2.15", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -1817,6 +2277,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rsa" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +dependencies = [ + "byteorder", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8 0.8.0", + "rand_core 0.6.4", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "rtoolbox" version = "0.0.2" @@ -1843,6 +2323,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1865,6 +2351,40 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.18" @@ -1912,9 +2432,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.9", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -2022,6 +2542,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2079,7 +2611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", "signature_derive", ] @@ -2094,6 +2626,12 @@ dependencies = [ "syn 2.0.85", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "simple-hyper-client" version = "0.1.3" @@ -2143,6 +2681,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der 0.5.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -2150,7 +2698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.9", ] [[package]] @@ -2288,7 +2836,7 @@ dependencies = [ "hkdf", "merlin", "prost", - "rand_core", + "rand_core 0.6.4", "sha2 0.10.8", "signature", "subtle", @@ -2424,6 +2972,9 @@ name = "tmkms" version = "0.14.0" dependencies = [ "abscissa_core", + "aes-gcm", + "aes-kw", + "base64 0.13.1", "byteorder", "bytes", "chrono", @@ -2433,17 +2984,21 @@ dependencies = [ "ed25519-consensus", "elliptic-curve", "eyre", - "getrandom", + "getrandom 0.2.15", "hkd32", "hkdf", "k256", "ledger", + "mockito", "once_cell", "prost", "prost-derive", - "rand", - "rand_core", + "rand 0.7.3", + "rand 0.8.5", + "rand_core 0.6.4", "rpassword", + "rsa", + "rustls", "sdkms", "serde", "serde_json", @@ -2457,6 +3012,7 @@ dependencies = [ "tendermint-p2p", "tendermint-proto", "thiserror", + "ureq", "url 2.5.2", "uuid", "wait-timeout", @@ -2695,6 +3251,30 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url 2.5.2", + "webpki-roots", +] + [[package]] name = "url" version = "1.7.2" @@ -2730,7 +3310,7 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ - "getrandom", + "getrandom 0.2.15", "serde", ] @@ -2776,6 +3356,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2837,6 +3423,27 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3055,7 +3662,7 @@ dependencies = [ "p256", "p384", "pbkdf2", - "rand_core", + "rand_core 0.6.4", "rusb", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 8626e58e..cafe664d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,16 +54,27 @@ wait-timeout = "0.2" yubihsm = { version = "0.42", features = ["secp256k1", "setup", "usb"], optional = true } zeroize = "1" +# HashiCorp deps +ureq = { version = "2.10.1", default-features=false, features = ["tls", "json", "gzip"], optional = true} +base64 = { version = "0.13.0", optional = true} +aes-kw = { version = "0.2.1", features = ["std"], optional = true} +rsa = { version = "0.6.1", default = true, optional = true} +rand = { version = "0.7", optional = true} +aes-gcm = { version = "0.10.1", optional = true} +rustls = { version = "0.23.18", features = ["ring"] } + [dev-dependencies] abscissa_core = { version = "0.7", features = ["testing"] } byteorder = "1" rand = "0.8" +mockito = "0.31.0" [features] softsign = [] yubihsm-mock = ["yubihsm/mockhsm"] yubihsm-server = ["yubihsm/http-server", "rpassword"] fortanixdsm = ["elliptic-curve", "sdkms", "url", "uuid"] +hashicorp = ["ureq", "base64", "aes-kw", "rsa", "rand", "aes-gcm"] # Enable integer overflow checks in release builds for security reasons [profile.release] diff --git a/README.hashicorp.md b/README.hashicorp.md new file mode 100644 index 00000000..e52254c7 --- /dev/null +++ b/README.hashicorp.md @@ -0,0 +1,189 @@ +# HashiCorp Vault + TMKMS + +HashiCorp Vault's `transit` engine mainly designed for data-in-transit encryption, it also provides additional features (sign and verify data, generate hashes and HMACs of data, and act as a source of random bytes). + +This implementation will use Vault as `signer as a service` where private key will not ever leave Vault + + +This document describes how to configure HashiCorp Vault for production use with Tendermint KMS. + +## Setting up Vault for `signer-as-service` +Start vault instance as per Hashicorp tutorial + +following script sets up Vault's configuration. Script designed for single chain signing... Extend it with additional keys+policies for additional chains. These are steps for `admin` +``` +#!/bin/bash +#login with root token +vault login + +echo "\nenabling transit engine..." +vault secrets enable transit +echo "\nenabling transit's engine sign path..." +vault secrets enable -path=sign transit + +echo "\ncreating cosmoshub signing key..." +vault write transit/keys/cosmoshub-sign-key type=ed25519 + +echo "\ncreating policy..." +cat < vault write transit/sign/<...sign key...> plaintext=$(base64 <<< "some-data") +``` + + +## Compiling `tmkms` with HashiCorp Vault support + +Refer the main README.md for compiling `tmkms` +from source code. You will need the prerequisities mentioned as indicated above. + +There are two ways to install `tmkms` with HashiCorp Vault, you need to pass the `--features=hashicorp` parameter to cargo. + +### Compiling from source code (via git) + +`tmkms` can be compiled directly from the git repository source code using the +following method. + +``` +$ git clone https://github.com/iqlusioninc/tmkms.git && cd tmkms +[...] +$ cargo build --release --features=hashicorp +``` + +If successful, this will produce a `tmkms` executable located at +`./target/release/tmkms` + +### Installing with the `cargo install` command + +With Rust (1.40+) installed, you can install tmkms with the following: + +``` +cargo install tmkms --features=hashicorp +``` + +Or to install a specific version (recommended): + +``` +cargo install tmkms --features=hashicorp --version=0.4.0 +``` + +This command installs `tmkms` directly from packages hosted on Rust's +[crates.io] service. Package authenticity is verified via the +[crates.io index] (itself a git repository) and by SHA-256 digests of +released artifacts. + +However, if newer dependencies are available, it may use newer versions +besides the ones which are "locked" in the source code repository. We +cannot verify those dependencies do not contain malicious code. If you would +like to ensure the dependencies in use are identical to the main repository, +please build from source code instead. + + +to run +``` +cargo run --features=hashicorp -- -c /path/to/tmkms.toml +``` + +## Production HashiCorp Vault setup + +`tmkms` contains support for HashiCorp Vault service, which enables tmkms to access the secure keys, stored in HashiCorp Vault's transit engine. This requires creation of the keys in Vault which can be done by referring to this [guide](https://www.vaultproject.io/docs/secrets/transit). Creating the key for signing and export should enable tmkms to use the keys on HashiCorp Vault. + +### Configuring `tmkms` for initial setup + +In order to perform setup, `tmkms` needs a configuration file which +contains the authentication details needed to authenticate to the HashiCorp Vault with an access token. + +This configuration should be placed in a file called: `tmkms.toml`. +You can specifty the path to the config with either `-c /path/to/tmkms.toml` or else tmkms will look in the current working directory for the same file. + +example: +```toml +[[providers.hashicorp]] + +[[providers.hashicorp.keys]] +chain_id = "<...chain id...>" +key = "<...ed25519 signing key...>" +auth.access_token = "<...token...>" + +[providers.hashicorp.adapter] +vault_addr = "https://<...host...>:8200" +vault_cacert = ... +vault_skip_verify = ... +``` + +You can [get](https://learn.hashicorp.com/tutorials/vault/tokens) the access token from the HashiCorp Vault. + +### Generating keys in HashiCorp Vault, transit engine +1. Enable transit engine +```bash +vault secrets enable transit +``` +2. Enable sign path on transit engine +```bash +vault secrets enable -path=sign transit +``` +3. Create a key +```bash +vault write transit/keys/<..key-name...> type=ed25519 +``` +4. Create a policy for the key + ```bash +vault policy write tmkms-transit-sign-policy - +path "transit/sign/<...key name...>" { + capabilities = [ "update"] +} +#used by HashiCorp API to verify connectivity on startup +path "auth/token/lookup-self" { + capabilities = [ "read" ] +} +``` +5. Create access token for the policy above +```bash +vault token create \ + -policy=tmkms-transit-sign-policy \ + -no-default-policy \ + -non-interactive \ + -renewable=false \ + -period=0 +``` +6. To import an existing tendermint key (this is TODO). +``` + +### Uploading a new key with the CLI +```bash +# generate new key +tmkms softsign keygen -t consensus ./new-softsign-key + +# setup VAULT env +export VAULT_ADDR="https://..." +export VAULT_TOKEN="..." + +# upload key +tmkms hashicorp upload new-key --payload-file ./new-softsign-key + +# test the key +tmkms hashicorp test new-key "test message" +``` diff --git a/src/commands.rs b/src/commands.rs index cff0c176..ca5b99d4 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,7 @@ //! Subcommands of the `tmkms` command-line application +#[cfg(feature = "hashicorp")] +pub mod hashicorp; pub mod init; #[cfg(feature = "ledger")] pub mod ledger; @@ -10,6 +12,8 @@ pub mod version; #[cfg(feature = "yubihsm")] pub mod yubihsm; +#[cfg(feature = "hashicorp")] +pub use self::hashicorp::HashicorpCommand; #[cfg(feature = "ledger")] pub use self::ledger::LedgerCommand; #[cfg(feature = "softsign")] @@ -50,6 +54,11 @@ pub enum KmsCommand { #[cfg(feature = "yubihsm")] #[clap(subcommand)] Yubihsm(YubihsmCommand), + + /// subcommands for HashiCorp + #[cfg(feature = "hashicorp")] + #[clap(subcommand)] + Hashicorp(HashicorpCommand), } impl KmsCommand { @@ -59,6 +68,8 @@ impl KmsCommand { KmsCommand::Start(run) => run.verbose, #[cfg(feature = "yubihsm")] KmsCommand::Yubihsm(yubihsm) => yubihsm.verbose(), + #[cfg(feature = "hashicorp")] + KmsCommand::Hashicorp(hashicorp) => hashicorp.verbose(), _ => false, } } diff --git a/src/commands/hashicorp.rs b/src/commands/hashicorp.rs new file mode 100644 index 00000000..01df7d7d --- /dev/null +++ b/src/commands/hashicorp.rs @@ -0,0 +1,36 @@ +//! `tmkms hashicorp` CLI (sub)commands + +mod pubkey; +mod test; +mod upload; +mod util; + +pub use self::pubkey::PubkeyCommand; +pub use self::test::TestCommand; +pub use self::upload::UploadCommand; + +use abscissa_core::{Command, Runnable}; +use clap::Subcommand; + +/// `hashicorp` subcommand +#[derive(Command, Debug, Runnable, Subcommand)] +pub enum HashicorpCommand { + /// perform a signing test + Test(TestCommand), + + /// upload priv/pub key + Upload(UploadCommand), + + /// print public key + Pubkey(PubkeyCommand), +} + +impl HashicorpCommand { + pub(super) fn verbose(&self) -> bool { + match self { + HashicorpCommand::Test(test) => test.verbose, + HashicorpCommand::Upload(test) => test.verbose, + HashicorpCommand::Pubkey(test) => test.verbose, + } + } +} diff --git a/src/commands/hashicorp/pubkey.rs b/src/commands/hashicorp/pubkey.rs new file mode 100644 index 00000000..ed5540e1 --- /dev/null +++ b/src/commands/hashicorp/pubkey.rs @@ -0,0 +1,73 @@ +//! Test the Hashicorp is working by performing signatures successively + +use crate::commands::hashicorp::util::read_config; +use crate::prelude::*; +use abscissa_core::{Command, Runnable}; +use base64; +use clap::Parser; +use std::{path::PathBuf, process}; + +/// The `hashicorp test` subcommand +#[derive(Command, Debug, Default, Parser)] +pub struct PubkeyCommand { + /// path to tmkms.toml + #[clap( + short = 'c', + long = "config", + value_name = "CONFIG", + help = "/path/to/tmkms.toml" + )] + pub config: Option, + + /// enable verbose debug logging + #[clap(short = 'v', long = "verbose")] + pub verbose: bool, + + /// signing key ID in Hashicorp Vault + #[clap(help = "vault's transit secret engine signing key")] + key_name: String, + + /// signing key chain-id (if there are multiple keys with the same name) + #[clap(long = "chain-id", help = "signing key chain-id")] + chain_id: Option, +} + +impl Runnable for PubkeyCommand { + /// Perform a signing test using the current TMKMS configuration + fn run(&self) { + if self.key_name.is_empty() { + status_err!("key_name cannot be empty!"); + process::exit(1); + } + + let cfg = read_config(&self.config, self.key_name.as_str()); + let signing_key = &cfg + .keys + .iter() + .find(|k| { + if self.chain_id.is_some() && self.chain_id.clone().unwrap() != k.chain_id.as_str() + { + return false; + } + k.key == self.key_name + }) + .expect("Unable to find key name in the config"); + + let mut app = + crate::keyring::providers::hashicorp::client::TendermintValidatorApp::connect( + &signing_key.auth.access_token(), + &self.key_name, + &cfg.adapter, + ) + .unwrap_or_else(|e| { + panic!( + "Unable to connect to Vault {} {}", + cfg.adapter.vault_addr, e + ) + }); + + let t = app.public_key().unwrap(); + + println!("{}", base64::encode(t)); + } +} diff --git a/src/commands/hashicorp/test.rs b/src/commands/hashicorp/test.rs new file mode 100644 index 00000000..c509442e --- /dev/null +++ b/src/commands/hashicorp/test.rs @@ -0,0 +1,85 @@ +//! Test the Hashicorp is working by performing signatures successively + +use crate::commands::hashicorp::util::read_config; +use crate::prelude::*; +use abscissa_core::{Command, Runnable}; +use clap::Parser; +use signature::SignerMut; +use std::{path::PathBuf, process, time::Instant}; + +/// The `hashicorp test` subcommand +#[derive(Command, Debug, Default, Parser)] +pub struct TestCommand { + /// path to tmkms.toml + #[clap( + short = 'c', + long = "config", + value_name = "CONFIG", + help = "/path/to/tmkms.toml" + )] + pub config: Option, + + /// enable verbose debug logging + #[clap(short = 'v', long = "verbose")] + pub verbose: bool, + + /// signing key ID in Hashicorp Vault + #[clap(help = "vault's transit secret engine signing key")] + key_name: String, + + /// test message + #[clap(help = "message to sign")] + test_messsage: String, + + /// signing key chain-id (if there are multiple keys with the same name) + #[clap(long = "chain-id", help = "signing key chain-id")] + chain_id: Option, +} + +impl Runnable for TestCommand { + /// Perform a signing test using the current TMKMS configuration + fn run(&self) { + if self.key_name.is_empty() { + status_err!("key_name cannot be empty!"); + process::exit(1); + } + + let cfg = read_config(&self.config, self.key_name.as_str()); + let signing_key = &cfg + .keys + .iter() + .find(|k| { + if self.chain_id.is_some() && self.chain_id.clone().unwrap() != k.chain_id.as_str() + { + return false; + } + k.key == self.key_name + }) + .expect("Unable to find key name in the config"); + + let started_at = Instant::now(); + + let app = crate::keyring::providers::hashicorp::client::TendermintValidatorApp::connect( + &signing_key.auth.access_token(), + &self.key_name, + &cfg.adapter, + ) + .unwrap_or_else(|e| { + panic!( + "Unable to connect to Vault {} {}", + cfg.adapter.vault_addr, e + ) + }); + + let mut app = + crate::keyring::providers::hashicorp::signer::Ed25519HashiCorpAppSigner::new(app); + + let signature = app.try_sign(self.test_messsage.as_bytes()).unwrap(); + + println!( + "Elapsed:{} ms. Result: {:?}", + started_at.elapsed().as_millis(), + signature + ); + } +} diff --git a/src/commands/hashicorp/upload.rs b/src/commands/hashicorp/upload.rs new file mode 100644 index 00000000..12bce80c --- /dev/null +++ b/src/commands/hashicorp/upload.rs @@ -0,0 +1,370 @@ +//! Test the Hashicorp is working by performing signatures successively + +use crate::keyring::ed25519; +use crate::{config::provider::hashicorp::HashiCorpConfig, prelude::*}; +use abscissa_core::{Command, Runnable}; +use aes_kw; +use clap::Parser; +use std::{path::PathBuf, process}; + +use crate::commands::hashicorp::util::read_config; +use crate::config::provider::softsign::KeyFormat; +use crate::keyring::providers::hashicorp::{client, error, vault_client}; +use rsa::{pkcs8::DecodePublicKey, PaddingScheme, PublicKey, RsaPublicKey}; +use tendermint::PrivateKey; +use tendermint_config::PrivValidatorKey; + +/// AES256 key length +const KEY_SIZE_AES256: usize = 32; // 256 bits +/// PKCS8 header +const PKCS8_HEADER: &[u8; 16] = b"\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x20"; + +/// The `hashicorp test` subcommand +#[derive(Command, Debug, Default, Parser)] +pub struct UploadCommand { + /// path to tmkms.toml + #[clap( + short = 'c', + long = "config", + value_name = "CONFIG", + help = "/path/to/tmkms.toml" + )] + pub config: Option, + + /// enable verbose debug logging + #[clap(short = 'v', long = "verbose")] + pub verbose: bool, + + /// key ID in Hashicorp Vault + #[clap(help = "Key ID")] + key_name: String, + + /// signing key chain-id (if there are multiple keys with the same name) + #[clap(long = "chain-id", help = "signing key chain-id")] + chain_id: Option, + + /// payload format to import: 'json' or 'base64' (default 'json') + #[clap(short = 'f', long = "payload-format")] + pub payload_format: Option, + + /// base64 encoded key file to upload + #[clap(group = "payload_arg", long = "payload-file")] + pub payload_file: Option, + + /// base64 encoded key to upload + #[clap(group = "payload_arg", long = "payload")] + pub payload: Option, + + /// this allows for all the valid keys in the key ring to be exported. Once set, this cannot be disabled. + #[clap(long = "exportable")] + exportable: bool, +} + +impl Runnable for UploadCommand { + /// Perform a import using the current TMKMS configuration + fn run(&self) { + if self.key_name.is_empty() { + status_err!("key_name cannot be empty!"); + process::exit(1); + } + + if self.payload.is_none() && self.payload_file.is_none() { + status_err!("either --payload or --payload_file must be set"); + process::exit(1); + } + + let cfg = read_config(&self.config, self.key_name.as_str()); + self.upload(&cfg); + } +} + +impl UploadCommand { + fn upload(&self, config: &HashiCorpConfig) { + // https://www.vaultproject.io/docs/secrets/transit#bring-your-own-key-byok + // https://learn.hashicorp.com/tutorials/vault/eaas-transit + + let payload_format = self + .payload_format + .as_ref() + .map(|f| { + f.parse::().unwrap_or_else(|e| { + status_err!("{} (must be 'json' or 'base64')", e); + process::exit(1); + }) + }) + .unwrap_or(KeyFormat::Json); + + let unknown_format_key: String = if self.payload.is_some() { + self.payload.clone().unwrap() + } else if self.payload_file.is_some() { + std::fs::read_to_string(self.payload_file.clone().unwrap()) + .expect("unable to read payload file") + .trim_end_matches('\n') + .into() + } else { + status_err!("payload and payload_file are undefined"); + process::exit(1); + }; + + let base64_key: String = match payload_format { + KeyFormat::Json => { + let secret_key = PrivValidatorKey::parse_json(unknown_format_key.clone()) + .unwrap_or_else(|e| { + status_err!("couldn't parse json {}: {}", unknown_format_key, e); + process::exit(1); + }) + .priv_key; + + match secret_key { + PrivateKey::Ed25519(sk) => base64::encode(sk.as_bytes()), + PrivateKey::Secp256k1(sk) => base64::encode(sk.to_bytes()), + _ => unreachable!("unsupported priv_validator.json algorithm"), + } + } + + KeyFormat::Base64 => unknown_format_key, + }; + + let ed25519_input_key = input_key(&base64_key) + .expect("secret: error converting \"key-to-upload\"[ed25519] with PKCS8 wrapping"); + + // create app instance + let app = client::TendermintValidatorApp::connect( + &config + .keys + .iter() + .find(|k| { + if self.chain_id.is_some() + && self.chain_id.clone().unwrap() != k.chain_id.as_str() + { + return false; + } + k.key == self.key_name + }) + .unwrap() + .auth + .access_token(), + &self.key_name, + &config.adapter, + ) + .unwrap_or_else(|_| { + panic!( + "Unable to connect to Vault at {}", + config.adapter.vault_addr + ) + }); + + use aes_gcm::KeyInit; + let v_aes_key = aes_gcm::Aes256Gcm::generate_key(&mut aes_gcm::aead::OsRng); + debug_assert_eq!( + KEY_SIZE_AES256, + v_aes_key.len(), + "expected aes key length {}, actual:{}", + KEY_SIZE_AES256, + v_aes_key.len() + ); + + let mut aes_key = [0u8; KEY_SIZE_AES256]; + aes_key.copy_from_slice(&v_aes_key[..KEY_SIZE_AES256]); + + let kek = aes_kw::KekAes256::from(aes_key); + let wrapped_input_key = kek + .wrap_with_padding_vec(&ed25519_input_key) + .expect("input key wrapping error!"); + + let wrapping_key_pem = app + .wrapping_key_pem() + .expect("wrapping key error: fetching error!"); + + let pub_key = RsaPublicKey::from_public_key_pem(&wrapping_key_pem).unwrap(); + + // wrap AES256 into RSA4096 + let wrapped_aes = pub_key + .encrypt( + &mut rand_core::OsRng, + PaddingScheme::new_oaep::(), + &aes_key, + ) + .expect("failed to encrypt"); + + debug_assert_eq!(wrapped_aes.len(), 512); + let wrapped_aes: Vec = [wrapped_aes.as_slice(), wrapped_input_key.as_slice()].concat(); + + app.import_key( + &self.key_name, + vault_client::CreateKeyType::Ed25519, + &base64::encode(wrapped_aes), + self.exportable, + ) + .expect("import key error!"); + } +} + +// https://docs.rs/ed25519/latest/ed25519/pkcs8/index.html +fn input_key(input_key: &str) -> Result, error::Error> { + let bytes = base64::decode(input_key)?; + + let secret_key = if bytes.len() == 64 { + ed25519::SigningKey::try_from(&bytes.as_slice()[..ed25519::SigningKey::BYTE_SIZE]) + } else { + ed25519::SigningKey::try_from(bytes.as_slice()) + } + .map_err(|e| error::Error::InvalidPubKey(e.to_string())); + + let mut secret_key: Vec = secret_key?.as_bytes().to_vec(); + + // HashiCorp Vault Transit engine expects PKCS8 + if secret_key.len() == ed25519::SigningKey::BYTE_SIZE { + let mut pkcs8_key = Vec::from(*PKCS8_HEADER); + pkcs8_key.extend_from_slice(&secret_key); + secret_key = pkcs8_key; + } + + debug_assert!(secret_key.len() == ed25519::SigningKey::BYTE_SIZE + PKCS8_HEADER.len()); + + Ok(secret_key) +} + +#[cfg(test)] +mod tests { + use crate::config::provider::hashicorp::{AdapterConfig, AuthConfig, SigningKeyConfig}; + use rand_core::{OsRng, RngCore}; + use std::convert::TryFrom; + + use super::*; + + fn new_rand_ed25519_key() -> ed25519::SigningKey { + let mut sk_bytes = [0u8; 32]; + OsRng.fill_bytes(&mut sk_bytes); + + ed25519::SigningKey::from(sk_bytes) + } + + #[test] + fn test_input_key_32bit_ok() { + let sk = new_rand_ed25519_key(); + let secret = base64::encode(sk.as_bytes()); + + // under test + let bytes = input_key(&secret).unwrap(); + + assert_eq!( + bytes.len(), + ed25519::SigningKey::BYTE_SIZE + PKCS8_HEADER.len() + ); + } + + #[test] + fn test_input_key_48bit_ok() { + let mut secret = PKCS8_HEADER.into_iter().cloned().collect::>(); + + let sk = new_rand_ed25519_key(); + + secret.extend_from_slice(sk.as_bytes()); + + let secret = base64::encode(sk.as_bytes()); + + // under test + let bytes = input_key(&secret).unwrap(); + + assert_eq!( + bytes.len(), + ed25519::SigningKey::BYTE_SIZE + PKCS8_HEADER.len() + ); + } + #[test] + fn test_input_key_64bit_ok() { + let mut secret = PKCS8_HEADER.into_iter().cloned().collect::>(); + + let sk = new_rand_ed25519_key(); + + secret.extend_from_slice(sk.as_bytes()); + + let secret = base64::encode(sk.as_bytes()); + + // under test + let bytes = input_key(&secret).unwrap(); + + assert_eq!( + bytes.len(), + ed25519::SigningKey::BYTE_SIZE + PKCS8_HEADER.len() + ); + } + + const KEY_NAME: &str = "upload-test"; + const VAULT_TOKEN: &str = "access-token"; + const CHAIN_ID: &str = "mock-chain-id"; + const ED25519: &str = + "4YZKJ/pfJj42tdcl40dXz/ugRgrBR0/Pp5C2kjHL6AZhBFozq5EspBwCb44zef0cLEO/WuLf3dI+BPCNOPwxRw=="; + + use mockito::{mock, server_address}; + + #[test] + fn test_upload() { + let cmd = UploadCommand { + verbose: false, + key_name: KEY_NAME.into(), + chain_id: None, + config: None, + payload: Some(ED25519.into()), + payload_file: None, + exportable: false, + payload_format: Some(String::from("base64")), + }; + + let config = HashiCorpConfig { + adapter: AdapterConfig { + vault_addr: format!("http://{}", server_address()), + vault_cacert: None, + vault_skip_verify: Some(false), + cache_pk: Some(false), + endpoints: None, + exit_on_error: None, + }, + keys: [SigningKeyConfig { + chain_id: tendermint::chain::Id::try_from(CHAIN_ID).unwrap(), + key: KEY_NAME.into(), + auth: AuthConfig::String { + access_token: VAULT_TOKEN.into(), + }, + key_type: crate::config::provider::KeyType::Consensus, + }] + .to_vec(), + }; + + // init + let lookup_self = mock("GET", "/v1/auth/token/lookup-self") + .match_header("X-Vault-Token", VAULT_TOKEN) + .with_body(TOKEN_DATA) + .create(); + + // wrap key + let wrapping_key = mock("GET", "/v1/transit/wrapping_key") + .match_header("X-Vault-Token", VAULT_TOKEN) + .with_body(WRAPPING_KEY_RESPONSE) + .create(); + + let end_point = format!("/v1/transit/keys/{}/import", KEY_NAME); + + // upload + let export = mock("POST", end_point.as_str()) + .match_header("X-Vault-Token", VAULT_TOKEN) + //.match_body(req.as_str()) // sipher string will be always different + .create(); + + // test + cmd.upload(&config); + + lookup_self.assert(); + export.assert(); + wrapping_key.expect(1).assert(); + } + + // curl --header "X-Vault-Token: hvs.<...valid.token...>>" http://127.0.0.1:8200/v1/auth/token/lookup-self + const TOKEN_DATA: &str = r#" + {"request_id":"119fcc9e-85e2-1fcf-c2a2-96cfb20f7446","lease_id":"","renewable":false,"lease_duration":0,"data":{"accessor":"k1g6PqNWVIlKK9NDCWLiTvrG","creation_time":1661247016,"creation_ttl":2764800,"display_name":"token","entity_id":"","expire_time":"2022-09-24T09:30:16.898359776Z","explicit_max_ttl":0,"id":"hvs.CAESIEzWRWLvyYLGlYsCRI_Vt653K26b-cx_lrxBlFo3_2GBGh4KHGh2cy5GVzZ5b25nMVFpSkwzM1B1eHM2Y0ZqbXA","issue_time":"2022-08-23T09:30:16.898363509Z","meta":null,"num_uses":0,"orphan":false,"path":"auth/token/create","policies":["tmkms-transit-sign-policy"],"renewable":false,"ttl":2758823,"type":"service"},"wrap_info":null,"warnings":null,"auth":null} + "#; + + const WRAPPING_KEY_RESPONSE: &str = r#"{"request_id":"1d739895-ea6d-2e18-3457-edbbf8dcd129","lease_id":"","renewable":false,"lease_duration":0,"data":{"public_key":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1hXp53II1GokeS6UyOvF\nbQnNgstRJ4IINjiQXL0iO+US3p5Zc/wwads6R3sTw6nwf+cXzPEkzyXXBIMgdLTH\nx/7kOuzT+mRJbKQgFXdHyEfm9T6jEKOSJFaQQxYQcMgUiMXiaXSonDnShwQ3BOxT\nzPo9TR8Z6+xMYIFTV9/kHJT2JHAX4xf5+EuRae4XsHW2yaWZzY//qVu/z0hXEeh3\nk0yK0kAULXMlzyJDpCNuWsdtB4ZpFv0eJ5ic84ZmA3B5Y/LQ0VSHLYnJOtt7hMe2\nsEEFHS7sfTbFxtBpSTySikoCLtHOAUXC0u3FQBJRta+uT82Iufdz7Qzw2xmR1WP2\nSTdqVINYci3/cql1xzEdKmieMwEwGbMOjFA7N4hBPgT9Tjod8vqCizk+Z1AH6ijd\nhfhDXlDi2owsngijdKJEoWCIC1IsqOTkZsKspw3a/9gdAkzXC8qkevCtOccC3Nwu\nAiA1Nh+FtFdvTDtwp7/G7lFLJT2E2PdtX8nZsI0TMmQg9Wh4wFP4pJfOGsYtMdNf\nN6cNVgYsTfkKIpXpxJdRf7YNKy1bvVNIPDAREuJTT8J5aSnnE/gjDiTbUDVnLulE\nYu7BaQqzE86k20MakAg1OLMftJJo0UhPxezanG43ZRW/K8OgBKnoD6UFFPzMiJ89\nQAzzkMa+CgjZr6zkIRy5FqkCAwEAAQ==\n-----END PUBLIC KEY-----\n"},"wrap_info":null,"warnings":null,"auth":null} + "#; +} diff --git a/src/commands/hashicorp/util.rs b/src/commands/hashicorp/util.rs new file mode 100644 index 00000000..00638bff --- /dev/null +++ b/src/commands/hashicorp/util.rs @@ -0,0 +1,60 @@ +use crate::config::provider::{ + hashicorp::{AuthConfig, HashiCorpConfig, SigningKeyConfig}, + KeyType, +}; +use crate::config::KmsConfig; +use crate::prelude::*; +use abscissa_core::{path::AbsPathBuf, Config}; +use std::{path::PathBuf, process}; + +pub fn read_config(config_path: &Option, key_name: &str) -> HashiCorpConfig { + if config_path.is_some() { + let canonical_path = AbsPathBuf::canonicalize(config_path.as_ref().unwrap()).unwrap(); + let config = KmsConfig::load_toml_file(canonical_path).expect("error loading config file"); + + if config.providers.hashicorp.len() != 1 { + status_err!( + "expected one [hashicorp.provider] in config, found: {}", + config.providers.hashicorp.len() + ); + } + + let cfg = config.providers.hashicorp[0].clone(); + + if !cfg.keys.iter().any(|k| k.key == key_name) { + status_err!( + "expected the key: {} to be present in the config, but it isn't there", + key_name + ); + process::exit(1); + } + + cfg + } else { + let vault_addr: String = std::env::var("VAULT_ADDR").expect("VAULT_ADDR is not set!"); + let vault_token: String = std::env::var("VAULT_TOKEN").expect("VAULT_TOKEN is not set!"); + let vault_cacert: Option = std::env::var("VAULT_CACERT").ok(); + let vault_skip_verify: Option = std::env::var("VAULT_SKIP_VERIFY") + .ok() + .map(|v| v.parse().unwrap()); + + HashiCorpConfig { + keys: vec![SigningKeyConfig { + chain_id: tendermint::chain::Id::try_from("mock-chain-id").unwrap(), + key: key_name.into(), + auth: AuthConfig::String { + access_token: vault_token, + }, + key_type: KeyType::Consensus, + }], + adapter: crate::config::provider::hashicorp::AdapterConfig { + vault_addr, + vault_cacert, + vault_skip_verify, + cache_pk: Some(false), + endpoints: None, + exit_on_error: None, + }, + } + } +} diff --git a/src/commands/init/config_builder.rs b/src/commands/init/config_builder.rs index 5465528c..73c4d8e5 100644 --- a/src/commands/init/config_builder.rs +++ b/src/commands/init/config_builder.rs @@ -83,6 +83,9 @@ impl ConfigBuilder { #[cfg(feature = "fortanixdsm")] self.add_fortanixdsm_provider_config(); + + #[cfg(feature = "hashicorp")] + self.add_hashicorp_provider_config(); } /// Add `[[validator]]` configurations @@ -148,6 +151,13 @@ impl ConfigBuilder { self.add_template_with_chain_id(include_str!("templates/keyring/fortanixdsm.toml")); } + /// Add `[[provider.hashicorp]]` configuration + #[cfg(feature = "hashicorp")] + fn add_hashicorp_provider_config(&mut self) { + self.add_str("### HashiCorp Vault Signer Configuration\n\n"); + self.add_template_with_chain_id(include_str!("templates/keyring/hashicorp.toml")); + } + /// Append a template to the config file, substituting `$KMS_HOME` fn add_template(&mut self, template: &str) { self.add_str(&format_template( diff --git a/src/commands/init/templates/keyring/hashicorp.toml b/src/commands/init/templates/keyring/hashicorp.toml new file mode 100644 index 00000000..86f904a4 --- /dev/null +++ b/src/commands/init/templates/keyring/hashicorp.toml @@ -0,0 +1,13 @@ +[[providers.hashicorp]] + +[[providers.hashicorp.keys]] +# ChainId this provider is configured for +chain_id = "$CHAIN_ID" +# Vault's transit secret engine key - vault write transit/keys/ type=ed25519 +key = "cosmoshub-sign-key" +# Vault's access token - vault token create -policy= +auth.access_token = "hvs.CAESINi91lCOFj-_dOGiUfpdZUPKk93LD8YyHz-qZcYLVwH_Gh4KHGh2cy5kdXV1T2tpcXliakFFblU1SUpqanczYjU" + +[providers.hashicorp.adapter] +# Vault's api url - VAULT_ADDR +vault_addr = "http://127.0.0.1:8200" diff --git a/src/config/provider.rs b/src/config/provider.rs index 07e5d5d4..8fb937f4 100644 --- a/src/config/provider.rs +++ b/src/config/provider.rs @@ -2,6 +2,8 @@ #[cfg(feature = "fortanixdsm")] pub mod fortanixdsm; +#[cfg(feature = "hashicorp")] +pub mod hashicorp; #[cfg(feature = "ledger")] pub mod ledgertm; #[cfg(feature = "softsign")] @@ -11,6 +13,8 @@ pub mod yubihsm; #[cfg(feature = "fortanixdsm")] use self::fortanixdsm::FortanixDsmConfig; +#[cfg(feature = "hashicorp")] +use self::hashicorp::HashiCorpConfig; #[cfg(feature = "ledger")] use self::ledgertm::LedgerTendermintConfig; #[cfg(feature = "softsign")] @@ -44,6 +48,11 @@ pub struct ProviderConfig { #[cfg(feature = "fortanixdsm")] #[serde(default)] pub fortanixdsm: Vec, + + /// HashiCorp Vault provider configurations + #[cfg(feature = "hashicorp")] + #[serde(default)] + pub hashicorp: Vec, } /// Types of cryptographic keys diff --git a/src/config/provider/hashicorp.rs b/src/config/provider/hashicorp.rs new file mode 100644 index 00000000..2f69c79a --- /dev/null +++ b/src/config/provider/hashicorp.rs @@ -0,0 +1,127 @@ +//! Configuration for HashiCorp Vault + +use super::KeyType; +use crate::chain; +use crate::prelude::*; +use serde::Deserialize; +use std::{fs, path::PathBuf, process}; +use zeroize::Zeroizing; + +/// Configuration options for this vault client +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields, untagged)] +pub enum AuthConfig { + /// Path to a vault token file + Path { + /// Vault auth token file path + access_token_file: PathBuf, + }, + /// Read auth token directly from the config file + String { + /// Auth token to use to authenticate to the HashiCorp Vault + access_token: String, + }, +} + +impl AuthConfig { + /// Get the `yubihsm::Credentials` for this `AuthConfig` + pub fn access_token(&self) -> String { + match self { + AuthConfig::Path { access_token_file } => { + let password = + Zeroizing::new(fs::read_to_string(access_token_file).unwrap_or_else(|e| { + status_err!( + "couldn't read access token from {}: {}", + access_token_file.display(), + e + ); + process::exit(1); + })); + + password.trim_end().to_owned() + } + AuthConfig::String { access_token } => access_token.to_owned(), + } + } +} + +/// Endpoints configuration for Hashicorp Vault instance +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct VaultEndpointConfig { + /// HashiCorp Vault API endpoint path to retrieve public keys etc. + pub keys: String, + + /// HashiCorp Vault API endpoint path to perform a handshake + pub handshake: String, + + /// HashiCorp Vault API endpoint path to recieve a wrapping key + pub wrapping_key: String, + + /// HashiCorp Vault API endpoint path to sign a message (e.g. prevote) + pub sign: String, +} + +impl Default for VaultEndpointConfig { + fn default() -> Self { + VaultEndpointConfig { + keys: "/v1/transit/keys".into(), + handshake: "/v1/auth/token/lookup-self".into(), + wrapping_key: "/v1/transit/wrapping_key".into(), + sign: "/v1/transit/sign".into(), + } + } +} + +/// Configuration for Hashicorp Vault instance +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct AdapterConfig { + /// HashiCorp Vault API endpoint, e.g. https://127.0.0.1:8200 + pub vault_addr: String, + + /// Path to a PEM-encoded CA certificate file on the local disk. This file is used to verify the HashiCorp Vault server's SSL certificate + pub vault_cacert: Option, + + /// Do not verify HashiCorp Vault's presented certificate before communicating with it + pub vault_skip_verify: Option, + + /// Enable tmkms in-memory public key caching. Vault API returns all key versions which may be expensive, in such case you can cache the public key and return it from tmkms cache + pub cache_pk: Option, + + /// Endpoints configuration for Vault core operations + pub endpoints: Option, + + /// Exit tmkms on given error codes. This is especially useful when operator manually revokes the Vault token and tmkms should exit because + /// it can't sign anymore (403) unless the new token is provided + pub exit_on_error: Option>, +} + +/// Signing key configuration +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SigningKeyConfig { + /// Chains this signing key is authorized to be used from + pub chain_id: chain::Id, + + /// Signing key ID + pub key: String, + + /// Type of key (account vs consensus, default consensus) + #[serde(default)] + pub key_type: KeyType, + + /// Authentication configuration + pub auth: AuthConfig, +} + +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +/// Hashicorp Vault signer configuration +pub struct HashiCorpConfig { + /// List of signing keys in the HashiCorp Vault + pub keys: Vec, + + /// Adapter configuration + pub adapter: AdapterConfig, +} diff --git a/src/keyring.rs b/src/keyring.rs index 364c658b..b572794b 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -229,5 +229,8 @@ pub fn load_config(registry: &mut chain::Registry, config: &ProviderConfig) -> R #[cfg(feature = "fortanixdsm")] providers::fortanixdsm::init(registry, &config.fortanixdsm)?; + #[cfg(feature = "hashicorp")] + providers::hashicorp::init(registry, &config.hashicorp)?; + Ok(()) } diff --git a/src/keyring/providers.rs b/src/keyring/providers.rs index d58bca2d..eba47d52 100644 --- a/src/keyring/providers.rs +++ b/src/keyring/providers.rs @@ -12,6 +12,9 @@ pub mod yubihsm; #[cfg(feature = "fortanixdsm")] pub mod fortanixdsm; +#[cfg(feature = "hashicorp")] +pub mod hashicorp; + use std::fmt::{self, Display}; /// Enumeration of signing key providers @@ -32,6 +35,10 @@ pub enum SigningProvider { /// Fortanix DSM signer #[cfg(feature = "fortanixdsm")] FortanixDsm, + + /// HashiCorp Vault provider + #[cfg(feature = "hashicorp")] + HashiCorp, } impl Display for SigningProvider { @@ -48,6 +55,9 @@ impl Display for SigningProvider { #[cfg(feature = "fortanixdsm")] SigningProvider::FortanixDsm => write!(f, "fortanixdsm"), + + #[cfg(feature = "hashicorp")] + SigningProvider::HashiCorp => write!(f, "hashicorp"), } } } diff --git a/src/keyring/providers/hashicorp.rs b/src/keyring/providers/hashicorp.rs new file mode 100644 index 00000000..f6c5720d --- /dev/null +++ b/src/keyring/providers/hashicorp.rs @@ -0,0 +1,98 @@ +//! HashiCorp Vault provider +pub(crate) mod client; +pub(crate) mod error; +pub(crate) mod signer; +pub(crate) mod vault_client; + +use crate::{ + chain, + config::provider::{hashicorp::HashiCorpConfig, KeyType}, + error::{Error, ErrorKind::*}, + keyring::{ + ed25519::{self, Signer}, + SigningProvider, + }, + prelude::*, +}; + +use tendermint::TendermintKey; + +use self::signer::Ed25519HashiCorpAppSigner; + +/// Create HashiCorp Vault Ed25519 signer objects from the given configuration +pub fn init( + chain_registry: &mut chain::Registry, + configs: &[HashiCorpConfig], +) -> Result<(), Error> { + if configs.is_empty() { + return Ok(()); + } + + if configs.len() != 1 { + fail!( + ConfigError, + "expected one [hashicorp.provider] in config, found: {}", + configs.len() + ); + } + + let mut loaded_consensus_key = false; + let config = &configs[0]; + + for key_config in config.keys.iter() { + match key_config.key_type { + KeyType::Account => panic!("account keys not supported with HashiCorp provider"), + KeyType::Consensus => { + if loaded_consensus_key { + fail!( + ConfigError, + "only one [[providers.hashicorp]] consensus key allowed" + ); + } + + let mut app = client::TendermintValidatorApp::connect( + &key_config.auth.access_token(), + &key_config.key, + &config.adapter, + ) + .unwrap_or_else(|_| { + panic!( + "Failed to authenticate to Vault for chain id:{}", + key_config.chain_id + ) + }); + + let public_key = app.public_key().unwrap_or_else(|e| { + panic!( + "Failed to get public key for chain id:{}, err: {}", + key_config.chain_id, e + ) + }); + + let public_key = ed25519::VerifyingKey::try_from(public_key.as_slice()) + .unwrap_or_else(|_| { + panic!( + "invalid Ed25519 public key for chain id:{}", + key_config.chain_id + ) + }); + + let provider = Ed25519HashiCorpAppSigner::new(app); + + loaded_consensus_key = true; + + chain_registry.add_consensus_key( + &key_config.chain_id, + // avoiding need for clone + Signer::new( + SigningProvider::HashiCorp, + TendermintKey::ConsensusKey(public_key.into()), + Box::new(provider), + ), + )?; + } + } + } + + Ok(()) +} diff --git a/src/keyring/providers/hashicorp/client.rs b/src/keyring/providers/hashicorp/client.rs new file mode 100644 index 00000000..739fc84f --- /dev/null +++ b/src/keyring/providers/hashicorp/client.rs @@ -0,0 +1,350 @@ +use super::error::Error; +use crate::config::provider::hashicorp::AdapterConfig; +use crate::keyring::ed25519; +use crate::keyring::providers::hashicorp::vault_client::{CreateKeyType, VaultClient}; +use abscissa_core::prelude::*; + +pub(crate) struct TendermintValidatorApp { + vault_client: VaultClient, + key_name: String, + + enable_pk_cache: Option, + pk_cache: Option<[u8; ed25519::VerifyingKey::BYTE_SIZE]>, +} + +// TODO(tarcieri): check this is actually sound?! :-) +#[allow(unsafe_code)] +unsafe impl Send for TendermintValidatorApp {} + +impl TendermintValidatorApp { + pub fn connect( + token: &str, + key_name: &str, + adapter_config: &AdapterConfig, + ) -> Result { + let vault_client = VaultClient::new( + &adapter_config.vault_addr, + token, + adapter_config.endpoints.to_owned(), + adapter_config.vault_cacert.to_owned(), + adapter_config.vault_skip_verify.to_owned(), + adapter_config.exit_on_error.to_owned(), + ); + + let app = TendermintValidatorApp { + vault_client, + key_name: key_name.to_owned(), + enable_pk_cache: adapter_config.cache_pk, + pk_cache: None, + }; + + debug!( + "Initialized with Vault host at {}", + adapter_config.vault_addr + ); + app.handshake()?; + + Ok(app) + } + + fn handshake(&self) -> Result<(), Error> { + let _ = self.vault_client.handshake(); + Ok(()) + } + + pub fn public_key(&mut self) -> Result<[u8; ed25519::VerifyingKey::BYTE_SIZE], Error> { + // if cache is enabled and we have a cached pk, return it + if self.enable_pk_cache.is_some() && self.enable_pk_cache.unwrap() { + if let Some(v) = self.pk_cache { + debug!("using cached public key {}...", self.key_name); + return Ok(v); + } + } + + let pk = self.vault_client.public_key(&self.key_name).unwrap(); + + // if cache is enabled, store the pk + if self.enable_pk_cache.is_some() && self.enable_pk_cache.unwrap() { + self.pk_cache = Some(pk); + debug!("Public key: value cached {}", self.key_name,); + } + + Ok(pk) + } + + pub fn sign(&self, message: &[u8]) -> Result<[u8; ed25519::Signature::BYTE_SIZE], Error> { + self.vault_client.sign(&self.key_name, message) + } + + /// fetch RSA wraping key from Vault/Transit. Returned key will be a 4096-bit RSA public key. + pub fn wrapping_key_pem(&self) -> Result { + self.vault_client.wrapping_key_pem() + } + + pub fn import_key( + &self, + key_name: &str, + key_type: CreateKeyType, + ciphertext: &str, + exportable: bool, + ) -> Result<(), Error> { + let _ = self + .vault_client + .import_key(key_name, key_type, ciphertext, exportable); + + Ok(()) + } +} + +#[cfg(feature = "hashicorp")] +#[cfg(test)] +mod tests { + use super::*; + use crate::keyring::providers::hashicorp::vault_client::{SignRequest, VAULT_TOKEN}; + use base64; + use mockito::{mock, server_address}; + + const TEST_TOKEN: &str = "test-token"; + const TEST_KEY_NAME: &str = "test-key-name"; + const TEST_PUB_KEY_VALUE: &str = "ng+ab41LawVupIXX3ocMn+AfV2W1DEMCfjAdtrwXND8="; // base64 + const TEST_PAYLOAD_TO_SIGN_BASE64: &str = "cXFxcXFxcXFxcXFxcXFxcXFxcXE="; // $(base64 <<< "qqqqqqqqqqqqqqqqqqqq") => "cXFxcXFxcXFxcXFxcXFxcXFxcXEK", 'K' vs "=" ???? + const TEST_PAYLOAD_TO_SIGN: &[u8] = b"qqqqqqqqqqqqqqqqqqqq"; + + const TEST_SIGNATURE:&str = /*vault:v1:*/ "pNcc/FAUu+Ta7itVegaMUMGqXYkzE777y3kOe8AtdRTgLbA8eFnrKbbX/m7zoiC+vArsIUJ1aMCEDRjDK3ZsBg=="; + + #[test] + fn hashicorp_connect_ok() { + // setup + let lookup_self = mock("GET", "/v1/auth/token/lookup-self") + .match_header(VAULT_TOKEN, TEST_TOKEN) + .with_body(TOKEN_DATA) + .create(); + + // test + let app = TendermintValidatorApp::connect( + TEST_TOKEN, + TEST_KEY_NAME, + &AdapterConfig { + vault_addr: format!("http://{}", server_address()), + endpoints: None, + vault_cacert: None, + vault_skip_verify: None, + exit_on_error: None, + cache_pk: Some(false), + }, + ); + + assert!(app.is_ok()); + lookup_self.assert(); + } + + #[test] + fn hashicorp_public_key_ok() { + // setup + let lookup_self = mock("GET", "/v1/auth/token/lookup-self") + .match_header("X-Vault-Token", TEST_TOKEN) + .with_body(TOKEN_DATA) + .create(); + + // app + let mut app = TendermintValidatorApp::connect( + TEST_TOKEN, + TEST_KEY_NAME, + &AdapterConfig { + vault_addr: format!("http://{}", server_address()), + endpoints: None, + vault_cacert: None, + vault_skip_verify: None, + exit_on_error: None, + cache_pk: Some(true), + }, + ) + .expect("Failed to connect"); + + // Vault call + let read_key = mock( + "GET", + format!("/v1/transit/keys/{}", TEST_KEY_NAME).as_str(), + ) + .match_header("X-Vault-Token", TEST_TOKEN) + .with_body(READ_KEY_RESP) + .expect_at_most(1) // one call only + .create(); + + // server call + let res = app.public_key(); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + base64::decode(TEST_PUB_KEY_VALUE).unwrap().as_slice() + ); + + // cached value + let res = app.public_key(); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + base64::decode(TEST_PUB_KEY_VALUE).unwrap().as_slice() + ); + + read_key.assert(); + lookup_self.assert(); + } + + #[test] + fn hashicorp_sign_ok() { + // setup + let lookup_self = mock("GET", "/v1/auth/token/lookup-self") + .match_header("X-Vault-Token", TEST_TOKEN) + .with_body(TOKEN_DATA) + .create(); + + // app + let app = TendermintValidatorApp::connect( + TEST_TOKEN, + TEST_KEY_NAME, + &AdapterConfig { + vault_addr: format!("http://{}", server_address()), + endpoints: Default::default(), + vault_cacert: None, + vault_skip_verify: None, + exit_on_error: None, + cache_pk: Some(false), + }, + ) + .expect("Failed to connect"); + + let body = serde_json::to_string(&SignRequest { + input: TEST_PAYLOAD_TO_SIGN_BASE64.into(), + }) + .unwrap(); + + let sign_mock = mock( + "POST", + format!("/v1/transit/sign/{}", TEST_KEY_NAME).as_str(), + ) + .match_header("X-Vault-Token", TEST_TOKEN) + .match_body(body.as_str()) + .with_body(SIGN_RESPONSE) + .create(); + + // server call + let res = app.sign(TEST_PAYLOAD_TO_SIGN); + assert!(res.is_ok()); + assert_eq!( + res.unwrap(), + base64::decode(TEST_SIGNATURE).unwrap().as_slice() + ); + + lookup_self.assert(); + sign_mock.assert(); + } + + #[test] + #[should_panic( + expected = "PoisonError prohibited Vault HTTP response code: 403, URL: http://127.0.0.1:1234/v1/transit/sign/test-key-name, exiting..." + )] + fn hashicorp_exit_on_error() { + // setup + let lookup_self = mock("GET", "/v1/auth/token/lookup-self") + .match_header("X-Vault-Token", TEST_TOKEN) + .with_body(TOKEN_DATA) + .create(); + + // app + let app = TendermintValidatorApp::connect( + TEST_TOKEN, + TEST_KEY_NAME, + &AdapterConfig { + vault_addr: format!("http://{}", server_address()), + endpoints: Default::default(), + vault_cacert: None, + vault_skip_verify: None, + exit_on_error: Some(vec![403]), + cache_pk: Some(false), + }, + ) + .expect("Failed to connect"); + + let body = serde_json::to_string(&SignRequest { + input: TEST_PAYLOAD_TO_SIGN_BASE64.into(), + }) + .unwrap(); + + let sign_mock = mock( + "POST", + format!("/v1/transit/sign/{}", TEST_KEY_NAME).as_str(), + ) + .match_header("X-Vault-Token", TEST_TOKEN) + .match_body(body.as_str()) + .with_body(SIGN_RESPONSE) + .with_status(403) + .create(); + + // server call + let _ = app.sign(TEST_PAYLOAD_TO_SIGN); + + lookup_self.assert(); + sign_mock.assert(); + } + + #[test] + fn hashicorp_sign_empty_payload_should_fail() { + // setup + let lookup_self = mock("GET", "/v1/auth/token/lookup-self") + .match_header("X-Vault-Token", TEST_TOKEN) + .with_body(TOKEN_DATA) + .create(); + + // app + let app = TendermintValidatorApp::connect( + TEST_TOKEN, + TEST_KEY_NAME, + &AdapterConfig { + vault_addr: format!("http://{}", server_address()), + endpoints: Default::default(), + vault_cacert: None, + vault_skip_verify: None, + exit_on_error: None, + cache_pk: Some(false), + }, + ) + .expect("Failed to connect"); + + let body = serde_json::to_string(&SignRequest { + input: TEST_PAYLOAD_TO_SIGN_BASE64.into(), + }) + .unwrap(); + + let sign_mock = mock( + "POST", + format!("/v1/transit/sign/{}", TEST_KEY_NAME).as_str(), + ) + .match_header("X-Vault-Token", TEST_TOKEN) + .match_body(body.as_str()) + .with_body(SIGN_RESPONSE) + .create(); + + // server call + let res = app.sign(&[]); + assert!(res.is_err()); + + lookup_self.assert(); + sign_mock.expect(0); + } + + // curl --header "X-Vault-Token: hvs.<...valid.token...>>" http://127.0.0.1:8200/v1/auth/token/lookup-self + const TOKEN_DATA: &str = r#" + {"request_id":"119fcc9e-85e2-1fcf-c2a2-96cfb20f7446","lease_id":"","renewable":false,"lease_duration":0,"data":{"accessor":"k1g6PqNWVIlKK9NDCWLiTvrG","creation_time":1661247016,"creation_ttl":2764800,"display_name":"token","entity_id":"","expire_time":"2022-09-24T09:30:16.898359776Z","explicit_max_ttl":0,"id":"hvs.CAESIEzWRWLvyYLGlYsCRI_Vt653K26b-cx_lrxBlFo3_2GBGh4KHGh2cy5GVzZ5b25nMVFpSkwzM1B1eHM2Y0ZqbXA","issue_time":"2022-08-23T09:30:16.898363509Z","meta":null,"num_uses":0,"orphan":false,"path":"auth/token/create","policies":["tmkms-transit-sign-policy"],"renewable":false,"ttl":2758823,"type":"service"},"wrap_info":null,"warnings":null,"auth":null} + "#; + + // curl --header "X-Vault-Token: $VAULT_TOKEN" "${VAULT_ADDR}/v1/transit/keys/" + const READ_KEY_RESP: &str = r#" + {"request_id":"9cb10d0a-1877-6da5-284b-8ece4b131ae3","lease_id":"","renewable":false,"lease_duration":0,"data":{"allow_plaintext_backup":false,"auto_rotate_period":0,"deletion_allowed":false,"derived":false,"exportable":false,"imported_key":false,"keys":{"1":{"creation_time":"2022-08-23T09:30:16.676998915Z","name":"ed25519","public_key":"ng+ab41LawVupIXX3ocMn+AfV2W1DEMCfjAdtrwXND8="}},"latest_version":1,"min_available_version":0,"min_decryption_version":1,"min_encryption_version":0,"name":"cosmoshub-sign-key","supports_decryption":false,"supports_derivation":true,"supports_encryption":false,"supports_signing":true,"type":"ed25519"},"wrap_info":null,"warnings":null,"auth":null} + "#; + + // curl --request POST --header "X-Vault-Token: $VAULT_TOKEN" "${VAULT_ADDR}/v1/transit/sign/<..key_name...>" -d '{"input":"base64 encoded"}' + const SIGN_RESPONSE: &str = r#" + {"request_id":"13534911-8e98-9a0f-a701-e9a7736140e2","lease_id":"","renewable":false,"lease_duration":0,"data":{"key_version":1,"signature":"vault:v1:pNcc/FAUu+Ta7itVegaMUMGqXYkzE777y3kOe8AtdRTgLbA8eFnrKbbX/m7zoiC+vArsIUJ1aMCEDRjDK3ZsBg=="},"wrap_info":null,"warnings":null,"auth":null} + "#; +} diff --git a/src/keyring/providers/hashicorp/error.rs b/src/keyring/providers/hashicorp/error.rs new file mode 100644 index 00000000..8eb31be6 --- /dev/null +++ b/src/keyring/providers/hashicorp/error.rs @@ -0,0 +1,64 @@ +//! Ledger errors + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("message cannot be empty")] + InvalidEmptyMessage, + + #[error("Public Key Error:{0}")] + InvalidPubKey(String), + + #[error("received no signature back")] + NoSignature, + + #[error("received an invalid signature: {0}")] + InvalidSignature(String), + + #[error("ApiClient error:{0}")] + ApiClient(String), + + #[error("Base64 decode error")] + Decode(base64::DecodeError), + + #[error("Serde error")] + SerDe(serde_json::Error), + + #[error("IO error")] + Io(std::io::Error), + + // PoisonError prefix is required for the application to exit + #[error("PoisonError prohibited Vault HTTP response code: {0}, URL: {1}, exiting...")] + ProhibitedResponseCode(String, String), +} + +impl From for Error { + fn from(err: ureq::Error) -> Error { + Error::ApiClient(err.to_string()) + } +} + +impl From for Error { + fn from(err: base64::DecodeError) -> Error { + Error::Decode(err) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Error { + Error::SerDe(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Error { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: signature::Error) -> Error { + Error::InvalidSignature(err.to_string()) + } +} diff --git a/src/keyring/providers/hashicorp/signer.rs b/src/keyring/providers/hashicorp/signer.rs new file mode 100644 index 00000000..aacd91bb --- /dev/null +++ b/src/keyring/providers/hashicorp/signer.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; +use std::sync::Mutex; + +use crate::keyring::ed25519::Signature; +use crate::keyring::providers::hashicorp::client::TendermintValidatorApp; +use signature::{Error, Signer}; + +/// ed25519 signature provider for the Ledger Tendermint Validator app +pub(crate) struct Ed25519HashiCorpAppSigner { + app: Arc>, +} + +impl Ed25519HashiCorpAppSigner { + pub fn new(app: TendermintValidatorApp) -> Self { + Ed25519HashiCorpAppSigner { + app: Arc::new(Mutex::new(app)), + } + } +} + +impl Signer for Ed25519HashiCorpAppSigner { + /// c: Compute a compact, fixed-sized signature of the given amino/json vote + fn try_sign(&self, msg: &[u8]) -> Result { + let app = self.app.lock().unwrap(); + let sig = app.sign(msg).map_err(Error::from_source)?; + Ok(Signature::from(sig)) + } +} diff --git a/src/keyring/providers/hashicorp/vault_client.rs b/src/keyring/providers/hashicorp/vault_client.rs new file mode 100644 index 00000000..0a2fb4f0 --- /dev/null +++ b/src/keyring/providers/hashicorp/vault_client.rs @@ -0,0 +1,472 @@ +use abscissa_core::prelude::*; +use std::collections::{BTreeMap, HashMap}; +use std::sync; + +use super::error::Error; + +use std::time::Duration; +use ureq::Agent; + +use crate::config::provider::hashicorp::VaultEndpointConfig; +use crate::keyring::ed25519; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::pki_types::{pem::PemObject, CertificateDer, ServerName, UnixTime}; +use rustls::{DigitallySignedStruct, SignatureScheme}; + +/// Vault message envelop +#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Root { + #[serde(rename = "request_id")] + pub request_id: String, + #[serde(rename = "lease_id")] + pub lease_id: String, + pub renewable: bool, + #[serde(rename = "lease_duration")] + pub lease_duration: i64, + pub data: Option, + #[serde(rename = "wrap_info")] + pub wrap_info: Value, + pub warnings: Value, + pub auth: Value, +} + +/// Sign Request Struct +#[derive(Debug, Serialize)] +pub(crate) struct SignRequest { + pub input: String, // Base64 encoded +} + +/// Sign Response Struct +#[derive(Debug, Deserialize)] +pub(crate) struct SignResponse { + pub signature: String, // Base64 encoded +} + +#[derive(Debug, Serialize)] +pub(crate) struct ImportRequest { + pub r#type: String, + pub ciphertext: String, + pub hash_function: String, + pub exportable: bool, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) enum ExportKeyType { + Encryption, + Signing, + Hmac, +} +impl std::fmt::Display for ExportKeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExportKeyType::Encryption => write!(f, "encryption-key"), + ExportKeyType::Signing => write!(f, "signing-key"), + ExportKeyType::Hmac => write!(f, "hmac-key"), + } + } +} +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) enum CreateKeyType { + /// AES-128 wrapped with GCM using a 96-bit nonce size AEAD (symmetric, supports derivation and convergent encryption) + Aes128Gcm96, + /// AES-256 wrapped with GCM using a 96-bit nonce size AEAD (symmetric, supports derivation and convergent encryption, default) + Aes256Gcm96, + /// ChaCha20-Poly1305 AEAD (symmetric, supports derivation and convergent encryption) + Chacha20Poly1305, + /// ED25519 (asymmetric, supports derivation). When using derivation, a sign operation with the same context will derive the same key and signature; this is a signing analogue to convergent_encryption. + Ed25519, + /// ECDSA using the P-256 elliptic curve (asymmetric) + EcdsaP256, + /// ECDSA using the P-384 elliptic curve (asymmetric) + EcdsaP384, + /// ECDSA using the P-521 elliptic curve (asymmetric) + EcdsaP521, + /// RSA with bit size of 2048 (asymmetric) + Rsa2048, + /// RSA with bit size of 3072 (asymmetric) + Rsa3072, + /// RSA with bit size of 4096 (asymmetric) + Rsa4096, +} + +impl std::fmt::Display for CreateKeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CreateKeyType::Aes128Gcm96 => write!(f, "aes128-gcm96"), + CreateKeyType::Aes256Gcm96 => write!(f, "aes256-gcm96"), + CreateKeyType::Chacha20Poly1305 => write!(f, "chacha20-poly1305"), + CreateKeyType::Ed25519 => write!(f, "ed25519"), + CreateKeyType::EcdsaP256 => write!(f, "ecdsa-p256"), + CreateKeyType::EcdsaP384 => write!(f, "ecdsa-p384"), + CreateKeyType::EcdsaP521 => write!(f, "ecdsa-p521"), + CreateKeyType::Rsa2048 => write!(f, "rsa-2048"), + CreateKeyType::Rsa3072 => write!(f, "rsa-3072"), + CreateKeyType::Rsa4096 => write!(f, "rsa-4096"), + } + } +} + +#[derive(Debug)] +struct NoVerification; + +impl ServerCertVerifier for NoVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } +} + +#[derive(Debug)] +pub(crate) struct VaultClient { + agent: Agent, + api_endpoint: String, + endpoints: VaultEndpointConfig, + token: String, + exit_on_error: Vec, +} + +pub const VAULT_TOKEN: &str = "X-Vault-Token"; +pub const CONSENUS_KEY_TYPE: &str = "ed25519"; + +impl VaultClient { + pub fn new( + api_endpoint: &str, + token: &str, + endpoints: Option, + ca_cert: Option, + skip_verify: Option, + exit_on_error: Option>, + ) -> Self { + // this call performs token self lookup, to fail fast + // let mut client = Client::new(host, token)?; + + // default conect timeout is 30s, this should be ok, since we block + let mut agent_builder = ureq::AgentBuilder::new() + .timeout_read(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) + .user_agent(&format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )); + + if ca_cert.is_some() || skip_verify.is_some() { + // see https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html#method.install_default + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + + let tls_config_builder = rustls::ClientConfig::builder(); + + if skip_verify.is_some_and(|x| x) { + let tls_config = tls_config_builder + .dangerous() + .with_custom_certificate_verifier(sync::Arc::new(NoVerification)) + .with_no_client_auth(); + + agent_builder = agent_builder.tls_config(sync::Arc::new(tls_config)); + } else if let Some(ca_cert) = ca_cert { + let mut roots = rustls::RootCertStore::empty(); + + let certs: Vec<_> = CertificateDer::pem_file_iter(ca_cert).unwrap().collect(); + for cert in certs { + roots.add(cert.unwrap()).unwrap(); + } + + let tls_config = tls_config_builder + .with_root_certificates(roots) + .with_no_client_auth(); + agent_builder = agent_builder.tls_config(sync::Arc::new(tls_config)); + } + } + + let agent: Agent = agent_builder.build(); + + VaultClient { + api_endpoint: api_endpoint.into(), + endpoints: endpoints.unwrap_or_default(), + agent, + token: token.into(), + exit_on_error: exit_on_error.unwrap_or_default(), + } + } + + pub fn public_key( + &self, + key_name: &str, + ) -> Result<[u8; ed25519::VerifyingKey::BYTE_SIZE], Error> { + /// Response struct + #[derive(Debug, Deserialize)] + struct PublicKeyResponse { + keys: BTreeMap>, + } + + // https://developer.hashicorp.com/vault/api-docs/secret/transit#read-key + let res = self + .agent + .get(&format!( + "{}{}/{}", + self.api_endpoint, self.endpoints.keys, key_name + )) + .set(VAULT_TOKEN, &self.token) + .call(); + + let response = self.check_response_status_code(res)?; + let data = if let Some(data) = response.into_json::>()?.data { + data + } else { + return Err(Error::InvalidPubKey( + "Public key: Vault response unavailable".into(), + )); + }; + + // latest key version + let key_data = data.keys.iter().last(); + + let pubk = if let Some((version, map)) = key_data { + debug!("public key version:{}", version); + if let Some(pubk) = map.get("public_key") { + if let Some(key_type) = map.get("name") { + if CONSENUS_KEY_TYPE != key_type { + return Err(Error::InvalidPubKey(format!( + "Public key \"{}\": expected key type:{}, received:{}", + key_name, CONSENUS_KEY_TYPE, key_type + ))); + } + } else { + return Err(Error::InvalidPubKey(format!( + "Public key \"{}\": expected key type:{}, unable to determine type", + key_name, CONSENUS_KEY_TYPE + ))); + } + pubk + } else { + return Err(Error::InvalidPubKey( + "Public key: unable to retrieve - \"public_key\" key is not found!".into(), + )); + } + } else { + return Err(Error::InvalidPubKey( + "Public key: unable to retrieve last version - not available!".into(), + )); + }; + + debug!("Public key: fetched {}={}...", key_name, pubk); + + let pubk = base64::decode(pubk)?; + + debug!( + "Public key: base64 decoded {}, size:{}", + key_name, + pubk.len() + ); + + let mut array = [0u8; ed25519::VerifyingKey::BYTE_SIZE]; + array.copy_from_slice(&pubk[..ed25519::VerifyingKey::BYTE_SIZE]); + + Ok(array) + } + + pub fn handshake(&self) -> Result<(), Error> { + let res = self + .agent + .get(&format!( + "{}{}", + self.api_endpoint, self.endpoints.handshake, + )) + .set(VAULT_TOKEN, &self.token) + .call(); + + self.check_response_status_code(res)?; + Ok(()) + } + + // vault write transit/sign/cosmoshub-sign-key plaintext=$(base64 <<< "some-data") + // "https://127.0.0.1:8200/v1/transit/sign/cosmoshub-sign-key" + /// Sign message + pub fn sign( + &self, + key_name: &str, + message: &[u8], + ) -> Result<[u8; ed25519::Signature::BYTE_SIZE], Error> { + debug!("signing request: received"); + if message.is_empty() { + return Err(Error::InvalidEmptyMessage); + } + + let body = SignRequest { + input: base64::encode(message), + }; + + debug!("signing request: base64 encoded and about to submit for signing..."); + + let res = self + .agent + .post(&format!( + "{}{}/{}", + self.api_endpoint, self.endpoints.sign, key_name + )) + .set(VAULT_TOKEN, &self.token) + .send_json(body); + + let response = self.check_response_status_code(res)?; + let data = if let Some(data) = response.into_json::>()?.data { + data + } else { + return Err(Error::NoSignature); + }; + + let parts = data.signature.split(':').collect::>(); + if parts.len() != 3 { + return Err(Error::InvalidSignature(format!( + "expected 3 parts, received:{} full:{}", + parts.len(), + data.signature + ))); + } + + // signature: "vault:v1:/bcnnk4p8Uvidrs1/IX9s66UCOmmfdJudcV1/yek9a2deMiNGsVRSjirz6u+ti2wqUZfG6UukaoSHIDSSRV5Cw==" + let base64_signature = if let Some(sign) = parts.last() { + sign.to_owned() + } else { + // this should never happen + return Err(Error::InvalidSignature("last part is not available".into())); + }; + + let signature = base64::decode(base64_signature)?; + if signature.len() != 64 { + return Err(Error::InvalidSignature(format!( + "invalid signature length! 64 == {}", + signature.len() + ))); + } + + let mut array = [0u8; ed25519::Signature::BYTE_SIZE]; + array.copy_from_slice(&signature[..ed25519::Signature::BYTE_SIZE]); + Ok(array) + } + + pub fn wrapping_key_pem(&self) -> Result { + #[derive(Debug, Deserialize)] + struct PublicKeyResponse { + public_key: String, + } + + let res = self + .agent + .get(&format!( + "{}{}", + self.api_endpoint, self.endpoints.wrapping_key + )) + .set(VAULT_TOKEN, &self.token) + .call(); + + let response = self.check_response_status_code(res)?; + let data = if let Some(data) = response.into_json::>()?.data { + data + } else { + return Err(Error::InvalidPubKey("Error getting wrapping key!".into())); + }; + + Ok(data.public_key.trim().to_owned()) + } + + pub fn import_key( + &self, + key_name: &str, + key_type: CreateKeyType, + ciphertext: &str, + exportable: bool, + ) -> Result<(), Error> { + let body = ImportRequest { + r#type: key_type.to_string(), + ciphertext: ciphertext.into(), + hash_function: "SHA256".into(), + exportable, + }; + + let res = self + .agent + .post(&format!( + "{}{}/{}/import", + self.api_endpoint, self.endpoints.keys, key_name + )) + .set(VAULT_TOKEN, &self.token) + .send_json(body); + + self.check_response_status_code(res)?; + + Ok(()) + } + + fn check_response_status_code( + &self, + response: Result, + ) -> Result { + match response { + Ok(response) => Ok(response), + Err(ureq::Error::Status(code, response)) => { + if self.exit_on_error.contains(&code) { + panic!( + "{}", + Error::ProhibitedResponseCode( + code.to_string(), + response.get_url().to_string(), + ) + ); + } else { + Err(ureq::Error::Status(code, response))? + } + } + Err(err) => Err(err.into()), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index dcb19beb..3e1f8dd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,11 +7,12 @@ feature = "softsign", feature = "yubihsm", feature = "ledger", - feature = "fortanixdsm" + feature = "fortanixdsm", + feature = "hashicorp" )))] compile_error!( "please enable one of the following backends with cargo's --features argument: \ - yubihsm, ledgertm, softsign, fortanixdsm (e.g. --features=yubihsm)" + yubihsm, ledgertm, softsign, fortanixdsm, hashicorp (e.g. --features=yubihsm)" ); pub mod application; diff --git a/tests/integration.rs b/tests/integration.rs index eda8b7f7..cf254d60 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -19,10 +19,13 @@ use tendermint_proto as proto; use tmkms::{ config::provider::KeyType, connection::unix::UnixConnection, - keyring::ed25519, + keyring::{ed25519, SigningProvider}, privval::{SignableMsg, SignedMsgType}, }; +#[cfg(feature = "hashicorp")] +use {std::sync::Once, zeroize::Zeroizing}; + /// Integration tests for the KMS command-line interface mod cli; @@ -33,6 +36,9 @@ const KMS_EXE_PATH: &str = "target/debug/tmkms"; const SIGNING_ED25519_KEY_PATH: &str = "tests/support/signing_ed25519.key"; const SIGNING_SECP256K1_KEY_PATH: &str = "tests/support/signing_secp256k1.key"; +#[cfg(feature = "hashicorp")] +static VAULT: Once = Once::new(); + enum KmsSocket { /// TCP socket type TCP(TcpStream), @@ -85,14 +91,19 @@ struct KmsProcess { impl KmsProcess { /// Spawn the KMS process and wait for an incoming TCP connection - pub fn create_tcp(key_type: &KeyType) -> Self { + pub fn create_tcp(key_type: &KeyType, provider: SigningProvider) -> Self { // Generate a random port and a config file let port: u16 = rand::thread_rng().gen_range(60000..=65535); - let config = KmsProcess::create_tcp_config(port, key_type); + let config = KmsProcess::create_tcp_config(port, key_type, provider); // Listen on a random port let listener = TcpListener::bind(format!("{}:{}", "127.0.0.1", port)).unwrap(); + #[cfg(feature = "hashicorp")] + if provider == SigningProvider::HashiCorp { + KmsProcess::load_vault_key(key_type, config.path().to_str().unwrap()); + } + let args = &["start", "-c", config.path().to_str().unwrap()]; let process = Command::new(KMS_EXE_PATH).args(args).spawn().unwrap(); @@ -104,17 +115,22 @@ impl KmsProcess { } /// Spawn the KMS process and connect to the Unix listener - pub fn create_unix(key_type: &KeyType) -> Self { + pub fn create_unix(key_type: &KeyType, provider: SigningProvider) -> Self { // Create a random socket path and a config file let mut rng = rand::thread_rng(); let letter: char = rng.gen_range(b'a'..=b'z') as char; let number: u32 = rng.gen_range(0..=999999); let socket_path = format!("/tmp/tmkms-{letter}{number:06}.sock"); - let config = KmsProcess::create_unix_config(&socket_path, key_type); + let config = KmsProcess::create_unix_config(&socket_path, key_type, provider); // Start listening for connections via the Unix socket let listener = UnixListener::bind(socket_path).unwrap(); + #[cfg(feature = "hashicorp")] + if provider == SigningProvider::HashiCorp { + KmsProcess::load_vault_key(key_type, config.path().to_str().unwrap()); + } + // Fire up the KMS process and allow it to connect to our Unix socket let args = &["start", "-c", config.path().to_str().unwrap()]; let process = Command::new(KMS_EXE_PATH).args(args).spawn().unwrap(); @@ -127,7 +143,11 @@ impl KmsProcess { } /// Create a config file for a TCP KMS and return its path - fn create_tcp_config(port: u16, key_type: &KeyType) -> NamedTempFile { + fn create_tcp_config( + port: u16, + key_type: &KeyType, + provider: SigningProvider, + ) -> NamedTempFile { let mut config_file = NamedTempFile::new().unwrap(); let pub_key = test_ed25519_keypair().verifying_key(); let peer_id = secret_connection::PublicKey::from(pub_key).peer_id(); @@ -146,24 +166,60 @@ impl KmsProcess { reconnect = false secret_key = "tests/support/secret_connection.key" protocol_version = "v0.34" - - [[providers.softsign]] - chain_ids = ["test_chain_id"] - key_format = "base64" - path = "{}" - key_type = "{}" "#, - &peer_id.to_string(), port, signing_key_path(key_type), key_type + &peer_id.to_string(), port ) .unwrap(); + match provider { + #[cfg(feature = "softsign")] + SigningProvider::SoftSign => writeln!( + config_file, + r#" + [[providers.softsign]] + chain_ids = ["test_chain_id"] + key_format = "base64" + path = "{}" + key_type = "{}" + "#, + signing_key_path(&key_type), + key_type + ), + + #[cfg(feature = "hashicorp")] + SigningProvider::HashiCorp => writeln!( + config_file, + r#" + [[providers.hashicorp]] + + [[providers.hashicorp.keys]] + chain_id = "test_chain_id" + key = "cosmoshub-sign-key" + auth.access_token = "test" + # key_type: {} + + [providers.hashicorp.adapter] + vault_addr = "http://127.0.0.1:8400" + "#, + key_type + ), + + #[allow(unreachable_patterns)] + _ => Ok(()), + } + .unwrap(); + config_file } /// Create a config file for a UNIX KMS and return its path - fn create_unix_config(socket_path: &str, key_type: &KeyType) -> NamedTempFile { + fn create_unix_config( + socket_path: &str, + key_type: &KeyType, + provider: SigningProvider, + ) -> NamedTempFile { let mut config_file = NamedTempFile::new().unwrap(); - let key_path = signing_key_path(key_type); + writeln!( config_file, r#" @@ -176,16 +232,48 @@ impl KmsProcess { chain_id = "test_chain_id" max_height = "500000" protocol_version = "v0.34" - - [[providers.softsign]] - chain_ids = ["test_chain_id"] - key_format = "base64" - path = "{key_path}" - key_type = "{key_type}" "# ) .unwrap(); + match provider { + #[cfg(feature = "softsign")] + SigningProvider::SoftSign => writeln!( + config_file, + r#" + [[providers.softsign]] + chain_ids = ["test_chain_id"] + key_format = "base64" + path = "{}" + key_type = "{}" + "#, + signing_key_path(&key_type), + key_type + ), + + #[cfg(feature = "hashicorp")] + SigningProvider::HashiCorp => writeln!( + config_file, + r#" + [[providers.hashicorp]] + + [[providers.hashicorp.keys]] + chain_id = "test_chain_id" + key = "cosmoshub-sign-key" + auth.access_token = "test" + # key_type: {} + + [providers.hashicorp.adapter] + vault_addr = "http://127.0.0.1:8400" + "#, + key_type + ), + + #[allow(unreachable_patterns)] + _ => Ok(()), + } + .unwrap(); + config_file } @@ -216,6 +304,28 @@ impl KmsProcess { } } } + + #[cfg(feature = "hashicorp")] + fn load_vault_key(key_type: &KeyType, config_path: &str) { + let base64_key = Zeroizing::new(fs::read_to_string(signing_key_path(key_type)).unwrap()); + let payload_arg = format!("--payload={}", base64_key.as_str()); + let args = &[ + "hashicorp", + "upload", + "cosmoshub-sign-key", + payload_arg.as_str(), + "-c", + config_path, + "-f", + "base64", + ]; + + // the first import will succeed, the latter won't (because key is already uploaded) + let _ = Command::new(KMS_EXE_PATH) + .env("VAULT_TOKEN", "test") + .args(args) + .output(); + } } /// A struct to hold protocol integration tests contexts @@ -227,13 +337,20 @@ struct ProtocolTester { } impl ProtocolTester { - pub fn apply(key_type: &KeyType, functor: F) + pub fn apply(key_type: &KeyType, provider: SigningProvider, functor: F) where F: FnOnce(ProtocolTester), { - let tcp_device = KmsProcess::create_tcp(key_type); + #[cfg(feature = "hashicorp")] + if provider == SigningProvider::HashiCorp { + VAULT.call_once(|| { + start_vault(); + }); + } + + let tcp_device = KmsProcess::create_tcp(&key_type, provider); let tcp_connection = tcp_device.create_connection(); - let unix_device = KmsProcess::create_unix(key_type); + let unix_device = KmsProcess::create_unix(&key_type, provider); let unix_connection = unix_device.create_connection(); functor(Self { @@ -320,16 +437,21 @@ pub fn extract_actual_len(buf: &[u8]) -> Result { } #[test] +#[cfg(feature = "softsign")] fn test_handle_and_sign_proposal_account() { - handle_and_sign_proposal(KeyType::Account) + handle_and_sign_proposal(KeyType::Account, SigningProvider::SoftSign); } #[test] fn test_handle_and_sign_proposal_consensus() { - handle_and_sign_proposal(KeyType::Consensus) + #[cfg(feature = "softsign")] + handle_and_sign_proposal(KeyType::Consensus, SigningProvider::SoftSign); + + #[cfg(feature = "hashicorp")] + handle_and_sign_proposal(KeyType::Consensus, SigningProvider::HashiCorp) } -fn handle_and_sign_proposal(key_type: KeyType) { +fn handle_and_sign_proposal(key_type: KeyType, provider: SigningProvider) { let chain_id = "test_chain_id"; let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); @@ -338,7 +460,7 @@ fn handle_and_sign_proposal(key_type: KeyType) { nanos: dt.timestamp_subsec_nanos() as i32, }; - ProtocolTester::apply(&key_type, |mut pt| { + ProtocolTester::apply(&key_type, provider, |mut pt| { let proposal = proto::types::Proposal { r#type: SignedMsgType::Proposal.into(), height: 12345, @@ -394,16 +516,21 @@ fn handle_and_sign_proposal(key_type: KeyType) { } #[test] +#[cfg(feature = "softsign")] fn test_handle_and_sign_vote_account() { - handle_and_sign_vote(KeyType::Account) + handle_and_sign_vote(KeyType::Account, SigningProvider::SoftSign) } #[test] fn test_handle_and_sign_vote_consensus() { - handle_and_sign_vote(KeyType::Consensus) + #[cfg(feature = "softsign")] + handle_and_sign_vote(KeyType::Consensus, SigningProvider::SoftSign); + + #[cfg(feature = "hashicorp")] + handle_and_sign_vote(KeyType::Consensus, SigningProvider::HashiCorp) } -fn handle_and_sign_vote(key_type: KeyType) { +fn handle_and_sign_vote(key_type: KeyType, provider: SigningProvider) { let chain_id = "test_chain_id"; let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); @@ -412,7 +539,7 @@ fn handle_and_sign_vote(key_type: KeyType) { nanos: dt.timestamp_subsec_nanos() as i32, }; - ProtocolTester::apply(&key_type, |mut pt| { + ProtocolTester::apply(&key_type, provider, |mut pt| { let vote_msg = proto::types::Vote { r#type: 0x01, height: 12345, @@ -480,17 +607,22 @@ fn handle_and_sign_vote(key_type: KeyType) { #[test] #[should_panic] +#[cfg(feature = "softsign")] fn test_exceed_max_height_account() { - exceed_max_height(KeyType::Account) + exceed_max_height(KeyType::Account, SigningProvider::SoftSign) } #[test] #[should_panic] fn test_exceed_max_height_consensus() { - exceed_max_height(KeyType::Consensus) + #[cfg(feature = "softsign")] + exceed_max_height(KeyType::Consensus, SigningProvider::SoftSign); + + #[cfg(feature = "hashicorp")] + exceed_max_height(KeyType::Consensus, SigningProvider::HashiCorp) } -fn exceed_max_height(key_type: KeyType) { +fn exceed_max_height(key_type: KeyType, provider: SigningProvider) { let chain_id = "test_chain_id"; let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); @@ -499,7 +631,7 @@ fn exceed_max_height(key_type: KeyType) { nanos: dt.timestamp_subsec_nanos() as i32, }; - ProtocolTester::apply(&key_type, |mut pt| { + ProtocolTester::apply(&key_type, provider, |mut pt| { let vote_msg = proto::types::Vote { r#type: 0x01, height: 500001, @@ -566,19 +698,24 @@ fn exceed_max_height(key_type: KeyType) { } #[test] +#[cfg(feature = "softsign")] fn test_handle_and_sign_get_publickey_account() { - handle_and_sign_get_publickey(KeyType::Account) + handle_and_sign_get_publickey(KeyType::Account, SigningProvider::SoftSign) } #[test] fn test_handle_and_sign_get_publickey_consensus() { - handle_and_sign_get_publickey(KeyType::Consensus) + #[cfg(feature = "softsign")] + handle_and_sign_get_publickey(KeyType::Consensus, SigningProvider::SoftSign); + + #[cfg(feature = "hashicorp")] + handle_and_sign_get_publickey(KeyType::Consensus, SigningProvider::HashiCorp) } -fn handle_and_sign_get_publickey(key_type: KeyType) { +fn handle_and_sign_get_publickey(key_type: KeyType, provider: SigningProvider) { let chain_id = "test_chain_id"; - ProtocolTester::apply(&key_type, |mut pt| { + ProtocolTester::apply(&key_type, provider, |mut pt| { let request = proto::privval::PubKeyRequest { chain_id: chain_id.into(), }; @@ -608,10 +745,12 @@ fn handle_and_sign_get_publickey(key_type: KeyType) { } #[test] +#[cfg(feature = "softsign")] fn test_handle_and_sign_ping_pong() { let key_type = KeyType::Consensus; + let provider = SigningProvider::SoftSign; - ProtocolTester::apply(&key_type, |mut pt| { + ProtocolTester::apply(&key_type, provider, |mut pt| { let request = proto::privval::PingRequest {}; send_request(proto::privval::message::Sum::PingRequest(request), &mut pt); read_response(&mut pt); @@ -620,8 +759,9 @@ fn test_handle_and_sign_ping_pong() { #[test] fn test_buffer_underflow_sign_proposal() { + let provider = SigningProvider::SoftSign; let key_type = KeyType::Consensus; - ProtocolTester::apply(&key_type, |mut pt| { + ProtocolTester::apply(&key_type, provider, |mut pt| { send_buffer_underflow_request(&mut pt); let response: Result<(), ()> = match read_response(&mut pt) { proto::privval::message::Sum::SignedProposalResponse(_) => Ok(()), @@ -663,3 +803,22 @@ fn read_response(pt: &mut ProtocolTester) -> proto::privval::message::Sum { let message = proto::privval::Message::decode_length_delimited(resp_bytes.as_ref()).unwrap(); message.sum.expect("no sum field in message") } + +#[cfg(feature = "hashicorp")] +fn start_vault() { + let no_vault_server = std::env::var("NO_VAULT_SERVER"); + + if no_vault_server.is_err() { + Command::new("sh") + .arg("-c") + .arg("./tests/support/start_vault.sh start background http 8400 0") + .spawn() + .expect("Failed to start vault server"); + } + + Command::new("bash") + .arg("-c") + .arg("./tests/support/start_vault.sh setup http 8400") + .output() + .expect("Failed to initalize vault server"); +} diff --git a/tests/support/start_vault.sh b/tests/support/start_vault.sh new file mode 100755 index 00000000..010943dd --- /dev/null +++ b/tests/support/start_vault.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +set -eum + +# kill everything in the process group +trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT + +export VAULT_TOKEN="test" + +# pre setup +TLS_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TLS_DIR"' EXIT + +export VAULT_CA_CERT="$TLS_DIR/vault-ca.pem" +export VAULT_SKIP_VERIFY="true" + +retry() { + local retries="$1" + local command="$2" + local options="$-" + + if [[ $options == *e* ]]; then + set +e + fi + + $command + local exit_code=$? + + if [[ $options == *e* ]]; then + set -e + fi + + if [[ $exit_code -ne 0 && $retries -gt 0 ]]; then + sleep 1 + retry $(($retries - 1)) "$command" + else + return $exit_code + fi +} + +function start_vault() { + PROTO="$2" + PORT="$3" + TERMINATE_TIMEOUT="$4" + TLS_ARGS="" + + pkill -9 -x vault || true + + if [[ "$PROTO" == "https" ]]; then + TLS_ARGS="-dev-tls -dev-tls-cert-dir=$TLS_DIR" + fi + + if [[ "$1" == "foreground" ]]; then + vault server -dev -dev-listen-address="127.0.0.1:$PORT" -dev-root-token-id="$VAULT_TOKEN" -dev-no-store-token $TLS_ARGS + fi + + if [[ "$1" == "background" ]]; then + vault server -dev -dev-listen-address="127.0.0.1:$PORT" -dev-root-token-id="$VAULT_TOKEN" -dev-no-store-token $TLS_ARGS & + VAULT_PID=$! + + if [[ $TERMINATE_TIMEOUT -gt 0 ]]; then + sleep $TERMINATE_TIMEOUT + kill $VAULT_PID + fi + fi +} + +function setup_vault() { + PROTO="$1" + PORT="$2" + + export VAULT_ADDR="$PROTO://127.0.0.1:$PORT" + export VAULT_API_ADDR="$PROTO://127.0.0.1:$PORT" + + echo "enabling transit engine..." + retry 5 "vault secrets enable transit" + + echo "enabling transit's engine sign path..." + retry 5 "vault secrets enable -path=sign transit" +} + +case "$1" in + +'start') + start_vault $2 $3 $4 $5 + ;; +'setup') + setup_vault $2 $3 + ;; +'all') + start_vault "background" $2 $3 0 + setup_vault $2 $3 + fg + ;; +*) echo "Unrecognized option $1" + ;; +esac diff --git a/tmkms.hashicorp.example.toml b/tmkms.hashicorp.example.toml new file mode 100644 index 00000000..897eb813 --- /dev/null +++ b/tmkms.hashicorp.example.toml @@ -0,0 +1,37 @@ +# Tendermint KMS configuration file + +## Chain Configuration +### Cosmos Hub Network +[[chain]] +id = "cosmoshub-4" +key_format = { type = "bech32", account_key_prefix = "cosmospub", consensus_key_prefix = "cosmosvalconspub" } +state_file = "/home/soleinik/work/rust/tmkms/state/cosmoshub-4-consensus.json" + +[[chain]] +id = "cosmoshub-3" +key_format = { type = "bech32", account_key_prefix = "cosmospub", consensus_key_prefix = "cosmosvalconspub" } +state_file = "/home/soleinik/work/rust/tmkms/state/cosmoshub-3-consensus.json" + +## Signing Provider Configuration +[[providers.hashicorp]] +[[providers.hashicorp.keys]] +chain_id = "cosmoshub-4" +auth.access_token = "..." +key = "cosmoshub-sign-key" + +[[providers.hashicorp.keys]] +chain_id = "cosmoshub-3" +auth.access_token = "..." +key = "cosmoshub-sign-key" + +[providers.hashicorp.adapter] +vault_addr = "http://127.0.0.1:8200" + +## Validator Configuration + +# [[validator]] +# chain_id = "cosmoshub-3" +# addr = "tcp://deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@example1.example.com:26658" +# secret_key = "/home/soleinik/work/rust/tmkms/secrets/kms-identity.key" +# protocol_version = "legacy" +# reconnect = true