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 18fac461..9968ceb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,29 @@ dependencies = [ "cpufeatures 0.2.5", ] +[[package]] +name = "aes-gcm" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +dependencies = [ + "aead 0.5.1", + "aes", + "cipher 0.4.3", + "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 = "ahash" version = "0.7.6" @@ -160,6 +183,16 @@ 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 = "async-trait" version = "0.1.64" @@ -171,6 +204,17 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -187,7 +231,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide", + "miniz_oxide 0.6.2", "object", "rustc-demangle", ] @@ -550,6 +594,23 @@ dependencies = [ "owo-colors", ] +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[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.1" @@ -621,6 +682,25 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +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.4.9" @@ -738,13 +818,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.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ - "const-oid", + "const-oid 0.9.1", "zeroize", ] @@ -774,7 +865,7 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", + "der 0.6.1", "elliptic-curve", "rfc6979", "signature", @@ -816,13 +907,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ "base16ct", - "crypto-bigint", - "der", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.6", "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", "sec1", "subtle", @@ -901,6 +992,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide 0.5.4", +] + [[package]] name = "flex-error" version = "0.4.4" @@ -1079,6 +1180,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.27.1" @@ -1164,6 +1275,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.2.6" @@ -1528,6 +1648,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "ledger" @@ -1551,6 +1674,12 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + [[package]] name = "libusb1-sys" version = "0.6.4" @@ -1636,6 +1765,15 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.6.2" @@ -1657,6 +1795,24 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "mockito" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401edc088069634afaa5f4a29617b36dba683c0c16fe4435a86debad23fa2f1a" +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.11" @@ -1698,6 +1854,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566d173b2f9406afbc5510a90925d5a2cd80cae4605631f1212303df265de011" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -1719,6 +1892,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1726,6 +1910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1886,6 +2071,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +[[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" @@ -1930,14 +2124,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.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", ] [[package]] @@ -1954,7 +2170,19 @@ checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ "cpufeatures 0.2.5", "opaque-debug", - "universal-hash", + "universal-hash 0.4.1", +] + +[[package]] +name = "polyval" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures 0.2.5", + "opaque-debug", + "universal-hash 0.5.0", ] [[package]] @@ -2227,7 +2455,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac 0.12.1", "zeroize", ] @@ -2304,6 +2532,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "rsa" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +dependencies = [ + "byteorder", + "digest 0.10.6", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8 0.8.0", + "rand_core 0.6.4", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "rusb" version = "0.9.1" @@ -2498,9 +2746,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct", - "der", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", "subtle", "zeroize", ] @@ -2598,6 +2846,18 @@ dependencies = [ "syn", ] +[[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.5" @@ -2674,6 +2934,12 @@ dependencies = [ "syn", ] +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + [[package]] name = "simple-hyper-client" version = "0.1.0" @@ -2729,6 +2995,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.6.0" @@ -2736,7 +3012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", ] [[package]] @@ -3056,6 +3332,9 @@ version = "0.12.2" dependencies = [ "abscissa_core", "abscissa_tokio", + "aes-gcm", + "aes-kw", + "base64 0.13.1", "byteorder", "bytes 0.5.6", "bytes 1.4.0", @@ -3072,6 +3351,7 @@ dependencies = [ "hyper-rustls 0.23.2", "k256", "ledger", + "mockito", "once_cell", "prost", "prost-amino", @@ -3080,6 +3360,7 @@ dependencies = [ "rand 0.7.3", "rand_core 0.6.4", "rpassword", + "rsa", "sdkms", "serde", "serde_json", @@ -3095,6 +3376,7 @@ dependencies = [ "tendermint-proto", "tendermint-rpc", "thiserror", + "ureq", "url 2.3.1", "uuid 1.3.0", "wait-timeout", @@ -3320,12 +3602,41 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "ureq" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" +dependencies = [ + "base64 0.13.1", + "chunked_transfer", + "flate2", + "log", + "once_cell", + "rustls 0.20.8", + "serde", + "serde_json", + "url 2.3.1", + "webpki 0.22.0", + "webpki-roots 0.22.6", +] + [[package]] name = "url" version = "1.7.2" diff --git a/Cargo.toml b/Cargo.toml index 29781772..21513005 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,10 +61,20 @@ wait-timeout = "0.2" yubihsm = { version = "0.41", features = ["secp256k1", "setup", "usb"], optional = true } zeroize = "1" + +# HashiCorp deps +ureq = { version = "~2.5", 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} + [dev-dependencies] abscissa_core = { version = "0.7", features = ["testing"] } byteorder = "1" rand = "0.7" +mockito = "0.31.0" [features] softsign = [] @@ -72,6 +82,8 @@ tx-signer = ["abscissa_tokio", "hyper", "hyper-rustls", "stdtx", "tendermint-rpc 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..b4a212a2 --- /dev/null +++ b/README.hashicorp.md @@ -0,0 +1,167 @@ +# 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]] +chain_id = "<...chain id...>" +api_endpoint= "https://<...host...>:8200" +access_token="<...token...>" +pk_key_name="<...ed25519 signing key...>" +``` + +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). +``` diff --git a/src/commands.rs b/src/commands.rs index cff0c176..4b60b474 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, } } @@ -74,6 +85,8 @@ impl Configurable for KmsCommand { KmsCommand::Yubihsm(yubihsm) => yubihsm.config_path(), #[cfg(feature = "ledger")] KmsCommand::Ledger(ledger) => ledger.config_path(), + #[cfg(feature = "hashicorp")] + KmsCommand::Hashicorp(hashicorp) => hashicorp.config_path(), _ => return None, }; diff --git a/src/commands/hashicorp.rs b/src/commands/hashicorp.rs new file mode 100644 index 00000000..2345decc --- /dev/null +++ b/src/commands/hashicorp.rs @@ -0,0 +1,37 @@ +//! `tmkms hashicorp` CLI (sub)commands + +mod test; +mod upload; + +pub use self::test::TestCommand; +pub use self::upload::UploadCommand; + +use abscissa_core::{Command, Runnable}; +use clap::Subcommand; +use std::path::PathBuf; + +/// `hashicorp` subcommand +#[derive(Command, Debug, Runnable, Subcommand)] +pub enum HashicorpCommand { + /// perform a signing test + Test(TestCommand), + + /// upload priv/pub key + Upload(UploadCommand), +} + +impl HashicorpCommand { + pub(super) fn config_path(&self) -> Option<&PathBuf> { + match self { + HashicorpCommand::Test(init) => init.config.as_ref(), + HashicorpCommand::Upload(init) => init.config.as_ref(), + } + } + + pub(super) fn verbose(&self) -> bool { + match self { + HashicorpCommand::Test(test) => test.verbose, + HashicorpCommand::Upload(test) => test.verbose, + } + } +} diff --git a/src/commands/hashicorp/test.rs b/src/commands/hashicorp/test.rs new file mode 100644 index 00000000..f36e7d69 --- /dev/null +++ b/src/commands/hashicorp/test.rs @@ -0,0 +1,75 @@ +//! Test the Hashicorp is working by performing signatures successively + +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, + + /// Ed25519 signing key ID in Hashicorp Vault + #[clap(help = "Vault's transit secret engine signing key")] + pk_name: String, + + ///test message + #[clap(help = "message to sign")] + test_messsage: String, +} + +impl Runnable for TestCommand { + /// Perform a signing test using the current TMKMS configuration + fn run(&self) { + if self.pk_name.is_empty() { + status_err!("pk_name cannot be empty!"); + process::exit(1); + } + + let config = APP.config(); + + let config = if let Some(c) = config + .providers + .hashicorp + .iter() + .find(|c| c.pk_name == self.pk_name) + { + c + } else { + status_err!("pk_name is not configured in provided \"tmkms.toml\"!"); + process::exit(1); + }; + + let started_at = Instant::now(); + + let app = crate::keyring::providers::hashicorp::client::TendermintValidatorApp::connect( + &config.api_endpoint, + &config.access_token, + &self.pk_name, + ) + .unwrap_or_else(|e| panic!("Unable to connect to Vault {} {}", config.api_endpoint, 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..3ae68817 --- /dev/null +++ b/src/commands/hashicorp/upload.rs @@ -0,0 +1,296 @@ +//! Test the Hashicorp is working by performing signatures successively + +use crate::{config::provider::hashicorp::HashiCorpConfig, prelude::*}; +use abscissa_core::{Command, Runnable}; +use aes_kw; +use clap::Parser; +use serde::Serialize; +use std::{path::PathBuf, process}; + +use crate::keyring::providers::hashicorp::{client, error}; +use rsa::{pkcs8::DecodePublicKey, PaddingScheme, PublicKey, RsaPublicKey}; + +///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")] + pk_name: String, + + /// base64 encoded key to upload + #[clap(long = "payload")] + pub payload: String, +} +///Import Secret Key Request +#[derive(Debug, Serialize)] +struct ImportRequest { + #[serde(default = "ed25519")] + r#type: String, + + ciphertext: String, +} + +impl Runnable for UploadCommand { + /// Perform a import using the current TMKMS configuration + fn run(&self) { + if self.pk_name.is_empty() { + status_err!("pk_name cannot be empty!"); + process::exit(1); + } + + let config = APP.config(); + + //finding key in config will point to correct Vault's URL + let config = if let Some(c) = config + .providers + .hashicorp + .iter() + .find(|c| c.pk_name == self.pk_name) + { + c + } else { + let cfg_path = if let Some(path) = self.config.as_ref() { + path.clone() + } else { + PathBuf::from("./tmkms.toml") + }; + status_err!( + "pk_name is not configured in provided \"{}\"!", + cfg_path.as_path().to_str().unwrap() + ); + process::exit(1); + }; + + self.upload(config); + } +} + +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 + + //root token or token with enough admin rights + let vault_token = std::env::var("VAULT_TOKEN") + .expect("root token \"VAULT_TOKEN\" is not set (confg token is NOT used)!"); + + let ed25519_input_key = input_key(&self.payload) + .expect("secret: error converting \"key-to-upload\"[ed25519] with PKCS8 wrapping"); + + //create app instance + let app = client::TendermintValidatorApp::connect( + &config.api_endpoint, + &vault_token, + &self.pk_name, + ) + .unwrap_or_else(|_| panic!("Unable to connect to Vault at {}", config.api_endpoint)); + + 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.pk_name, + client::CreateKeyType::Ed25519, + &base64::encode(wrapped_aes), + ) + .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_dalek::Keypair::from_bytes(&bytes)?.secret + } else { + ed25519_dalek::SecretKey::from_bytes(&bytes)? + }; + + let mut secret_key: Vec = secret_key.to_bytes().into_iter().collect::>(); + + //HashiCorp Vault Transit engine expects PKCS8 + if secret_key.len() == ed25519_dalek::SECRET_KEY_LENGTH { + 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_dalek::SECRET_KEY_LENGTH + PKCS8_HEADER.len()); + + Ok(secret_key) +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use super::*; + + #[test] + fn test_input_key_32bit_ok() { + let bytes = ed25519_dalek::SecretKey::generate(&mut rand::thread_rng()).to_bytes(); + assert_eq!(bytes.len(), ed25519_dalek::SECRET_KEY_LENGTH); + + let secret = base64::encode(bytes); + + //under test + let bytes = input_key(&secret).unwrap(); + + assert_eq!( + bytes.len(), + ed25519_dalek::SECRET_KEY_LENGTH + PKCS8_HEADER.len() + ); + } + + #[test] + fn test_input_key_48bit_ok() { + let mut secret = PKCS8_HEADER.into_iter().cloned().collect::>(); + + let bytes = ed25519_dalek::SecretKey::generate(&mut rand::thread_rng()).to_bytes(); + assert_eq!(bytes.len(), ed25519_dalek::SECRET_KEY_LENGTH); + + secret.extend_from_slice(&bytes); + + let secret = base64::encode(bytes); + //under test + let bytes = input_key(&secret).unwrap(); + + assert_eq!( + bytes.len(), + ed25519_dalek::SECRET_KEY_LENGTH + PKCS8_HEADER.len() + ); + } + #[test] + fn test_input_key_64bit_ok() { + let mut secret = PKCS8_HEADER.into_iter().cloned().collect::>(); + + let bytes = ed25519_dalek::SecretKey::generate(&mut rand::thread_rng()).to_bytes(); + assert_eq!(bytes.len(), ed25519_dalek::SECRET_KEY_LENGTH); + + secret.extend_from_slice(&bytes); + + let secret = base64::encode(bytes); + //under test + let bytes = input_key(&secret).unwrap(); + + assert_eq!( + bytes.len(), + ed25519_dalek::SECRET_KEY_LENGTH + PKCS8_HEADER.len() + ); + } + + const PK_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, + pk_name: PK_NAME.into(), + config: None, + payload: ED25519.into(), + }; + + let config = HashiCorpConfig { + access_token: "crazy-long-string".into(), + api_endpoint: format!("http://{}", server_address()), + pk_name: PK_NAME.into(), + chain_id: tendermint::chain::Id::try_from(CHAIN_ID).unwrap(), + }; + + std::env::set_var("VAULT_TOKEN", VAULT_TOKEN); + + //init + let lookup_self = mock("GET", "/v1/auth/token/lookup-self") + .match_header("X-Vault-Token", VAULT_TOKEN) + .with_body(TOKEN_DATA) + .create(); + + //upload + 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", PK_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/init/config_builder.rs b/src/commands/init/config_builder.rs index 766ca4ad..000e89ae 100644 --- a/src/commands/init/config_builder.rs +++ b/src/commands/init/config_builder.rs @@ -86,6 +86,9 @@ impl ConfigBuilder { #[cfg(feature = "fortanixdsm")] self.add_fortanixdsm_provider_config(); + + #[cfg(feature = "hashicorp")] + self.add_hashicorp_provider_config(); } /// Add `[[validator]]` configurations @@ -179,6 +182,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..8e7cb6e0 --- /dev/null +++ b/src/commands/init/templates/keyring/hashicorp.toml @@ -0,0 +1,9 @@ +[[providers.hashicorp]] +#ChainId this provider is configured for +chain_id = "$CHAIN_ID" +#Vault's api url - VAULT_ADDR +api_endpoint= "http://127.0.0.1:8200" +#Vault's access token - vault token create -policy= +access_token="hvs.CAESINi91lCOFj-_dOGiUfpdZUPKk93LD8YyHz-qZcYLVwH_Gh4KHGh2cy5kdXV1T2tpcXliakFFblU1SUpqanczYjU" +#Vault's transit secret engine key - vault write transit/keys/ type=ed25519 +pk_name="cosmoshub-sign-key" 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..26866868 --- /dev/null +++ b/src/config/provider/hashicorp.rs @@ -0,0 +1,21 @@ +//! Configuration for HashiCorp Vault + +use crate::chain; +use serde::Deserialize; + +#[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +/// Hashicorp Vault signer configuration +pub struct HashiCorpConfig { + /// Chains this signing key is authorized to be used from + pub chain_id: chain::Id, + + /// HashiCorp Vault API endpoint, e.g. https://127.0.0.1:8200 + pub api_endpoint: String, + + /// Access token for authenticating to HashiCorp Vault + pub access_token: String, + + /// Vault's key name with ed25519 pub+priv key + pub pk_name: String, +} diff --git a/src/keyring.rs b/src/keyring.rs index bce3c865..3a9c4913 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -190,5 +190,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..e750a270 --- /dev/null +++ b/src/keyring/providers/hashicorp.rs @@ -0,0 +1,80 @@ +//! HashiCorp Vault provider +pub(crate) mod client; +pub(crate) mod error; +pub(crate) mod signer; + +use crate::{ + chain, + config::provider::hashicorp::HashiCorpConfig, + 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(()); + } + + let mut chains = Vec::::new(); + + for config in configs { + //misconfiguration check + if chains.contains(&config.chain_id.to_string()) { + fail!( + ConfigError, + format!("already configured! chain id:{}", config.chain_id) + ) + } else { + chains.push(config.chain_id.to_string()) + } + + let mut app = client::TendermintValidatorApp::connect( + &config.api_endpoint, + &config.access_token, + &config.pk_name, + ) + .unwrap_or_else(|_| { + panic!( + "Failed to authenticate to Vault for chain id:{}", + config.chain_id + ) + }); + + let public_key = app.public_key().unwrap_or_else(|_| { + panic!("Failed to get public key for chain id:{}", config.chain_id,) + }); + + let public_key = ed25519::PublicKey::from_bytes(&public_key).unwrap_or_else(|_| { + panic!( + "invalid Ed25519 public key for chain id:{}", + config.chain_id + ) + }); + + let provider = Ed25519HashiCorpAppSigner::new(app); + + chain_registry.add_consensus_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..de21c8d8 --- /dev/null +++ b/src/keyring/providers/hashicorp/client.rs @@ -0,0 +1,538 @@ +use abscissa_core::prelude::*; +use std::collections::{BTreeMap, HashMap}; + +use super::error::Error; + +use std::time::Duration; +use ureq::Agent; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +pub const CONSENUS_KEY_TYPE: &str = "ed25519"; +const VAULT_TOKEN: &str = "X-Vault-Token"; + +pub(crate) struct TendermintValidatorApp { + agent: Agent, + api_endpoint: String, + token: String, + key_name: String, + public_key_value: Option<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>, +} + +// TODO(tarcieri): check this is actually sound?! :-) +#[allow(unsafe_code)] +unsafe impl Send for TendermintValidatorApp {} + +///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)] +struct SignRequest { + input: String, //Base64 encoded +} + +///Sign Response Struct +#[derive(Debug, Deserialize)] +struct SignResponse { + signature: String, //Base64 encoded +} + +#[derive(Debug, Serialize)] +pub(crate) struct ImportRequest { + pub r#type: String, + pub ciphertext: String, + pub hash_function: String, +} + +#[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"), + } + } +} + +impl TendermintValidatorApp { + pub fn connect(api_endpoint: &str, token: &str, key_name: &str) -> Result { + //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 agent: Agent = 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") + )) + .build(); + + let app = TendermintValidatorApp { + agent, + api_endpoint: api_endpoint.to_owned(), + token: token.to_owned(), + key_name: key_name.to_owned(), + public_key_value: None, + }; + + debug!("Initialized with Vault host at {}", api_endpoint); + app.hand_shake()?; + + Ok(app) + } + + fn hand_shake(&self) -> Result<(), Error> { + let _ = self + .agent + .get(&format!("{}/v1/auth/token/lookup-self", self.api_endpoint)) + .set(VAULT_TOKEN, &self.token) + .call() + .map_err(|e| { + super::error::Error::Combined( + "Is \"access_token\" value correct?".into(), + Box::new(e.into()), + ) + })?; + Ok(()) + } + + //vault read transit/keys/cosmoshub-sign-key + //GET http://0.0.0.0:8200/v1/transit/keys/cosmoshub-sign-key + /// Get public key + pub fn public_key(&mut self) -> Result<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH], Error> { + if let Some(v) = self.public_key_value { + debug!("using cached public key {}...", self.key_name); + return Ok(v); + } + + debug!("fetching public key for {}...", self.key_name); + + ///Response struct + #[derive(Debug, Deserialize)] + struct PublicKeyResponse { + keys: BTreeMap>, + } + + //TODO - explore "latest" + let data = if let Some(data) = self + .agent + .get(&format!( + "{}/v1/transit/keys/{}", + self.api_endpoint, self.key_name + )) + .set(VAULT_TOKEN, &self.token) + .call()? + .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 vetion:{}", 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:{}", + self.key_name, CONSENUS_KEY_TYPE, key_type + ))); + } + } else { + return Err(Error::InvalidPubKey(format!( + "Public key \"{}\": expected key type:{}, unable to determine type", + self.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 {}={}...", self.key_name, pubk); + + let pubk = base64::decode(pubk)?; + + debug!( + "Public key: base64 decoded {}, size:{}", + self.key_name, + pubk.len() + ); + + let mut array = [0u8; ed25519_dalek::PUBLIC_KEY_LENGTH]; + array.copy_from_slice(&pubk[..ed25519_dalek::PUBLIC_KEY_LENGTH]); + + //cache it... + self.public_key_value = Some(array); + debug!("Public key: value cached {}", self.key_name,); + + Ok(array) + } + + //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, message: &[u8]) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH], 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 data = if let Some(data) = self + .agent + .post(&format!( + "{}/v1/transit/sign/{}", + self.api_endpoint, self.key_name + )) + .set(VAULT_TOKEN, &self.token) + .send_json(body)? + .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_dalek::SIGNATURE_LENGTH]; + array.copy_from_slice(&signature[..ed25519_dalek::SIGNATURE_LENGTH]); + Ok(array) + } + + ///fetch RSA wraping key from Vault/Transit. Returned key will be a 4096-bit RSA public key. + pub fn wrapping_key_pem(&self) -> Result { + debug!("getting wraping key..."); + #[derive(Debug, Deserialize)] + struct PublicKeyResponse { + public_key: String, + } + + let data = if let Some(data) = self + .agent + .get(&format!("{}/v1/transit/wrapping_key", self.api_endpoint)) + .set(VAULT_TOKEN, &self.token) + .call()? + .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, + ) -> Result<(), Error> { + let body = ImportRequest { + r#type: key_type.to_string(), + ciphertext: ciphertext.into(), + hash_function: "SHA256".into(), + }; + + let _ = self + .agent + .post(&format!( + "{}/v1/transit/keys/{}/import", + self.api_endpoint, key_name + )) + .set(VAULT_TOKEN, &self.token) + .send_json(body)?; + + Ok(()) + } +} + +#[cfg(feature = "hashicorp")] +#[cfg(test)] +mod tests { + use super::*; + 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( + &format!("http://{}", server_address()), + TEST_TOKEN, + TEST_KEY_NAME, + ); + + 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( + &format!("http://{}", server_address()), + TEST_TOKEN, + TEST_KEY_NAME, + ) + .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 vaule + 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( + &format!("http://{}", server_address()), + TEST_TOKEN, + TEST_KEY_NAME, + ) + .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] + 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( + &format!("http://{}", server_address()), + TEST_TOKEN, + TEST_KEY_NAME, + ) + .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..bf0ea6e9 --- /dev/null +++ b/src/keyring/providers/hashicorp/error.rs @@ -0,0 +1,63 @@ +//! 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), + + #[error("Help:{0}, Error:{1} ")] + Combined(String, Box), +} + +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/lib.rs b/src/lib.rs index d4abc5d5..152ec4ad 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 amino_types; diff --git a/tests/cli/init.rs b/tests/cli/init.rs index 6f22887f..566264dc 100644 --- a/tests/cli/init.rs +++ b/tests/cli/init.rs @@ -1,16 +1,18 @@ //! Integration tests for the `init` subcommand -use crate::cli; -use abscissa_core::Config; -use std::{ffi::OsStr, fs}; -use tmkms::{commands::init::networks::Network, config::KmsConfig}; - -#[test] -fn test_command() { - let parent_dir = tempfile::tempdir().unwrap(); - - let output_dir = parent_dir.path().join("tmkms"); - assert!(!output_dir.exists()); +#[cfg(feature = "softsign")] +mod softsign_init_test { + use crate::cli; + use abscissa_core::Config; + use std::{ffi::OsStr, fs}; + use tmkms::{commands::init::networks::Network, config::KmsConfig}; + + #[test] + fn test_command() { + let parent_dir = tempfile::tempdir().unwrap(); + + let output_dir = parent_dir.path().join("tmkms"); + assert!(!output_dir.exists()); // Network names to test with let networks = Network::all() diff --git a/tests/integration.rs b/tests/integration.rs index d6e3064e..8f365671 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,58 +1,72 @@ //! KMS integration test -use std::{ - fs, - io::{self, Cursor, Read, Write}, - net::{TcpListener, TcpStream}, - os::unix::net::{UnixListener, UnixStream}, - process::{Child, Command}, -}; - -use abscissa_core::prelude::warn; -use chrono::{DateTime, Utc}; -use ed25519_dalek::{self as ed25519, Verifier}; -use rand::Rng; -use tempfile::NamedTempFile; - -use prost_amino::Message; -use tendermint_p2p::secret_connection::{self, SecretConnection}; - -use tmkms::{ - amino_types::{self, *}, - config::validator::ProtocolVersion, - connection::unix::UnixConnection, -}; - -/// Integration tests for the KMS command-line interface mod cli; - /// Path to the KMS executable const KMS_EXE_PATH: &str = "target/debug/tmkms"; -/// Path to the example validator signing key -const SIGNING_KEY_PATH: &str = "tests/support/signing.key"; +#[cfg(feature = "softsign")] +mod softsign_integration_tests { + use std::{ + fs, + io::{self, Cursor, Read, Write}, + net::{TcpListener, TcpStream}, + os::unix::net::{UnixListener, UnixStream}, + process::{Child, Command}, + }; -enum KmsSocket { - /// TCP socket type - TCP(TcpStream), + use abscissa_core::prelude::warn; + use chrono::{DateTime, Utc}; + use ed25519_dalek::{self as ed25519, Verifier}; + use rand::Rng; + use tempfile::NamedTempFile; - /// UNIX socket type - UNIX(UnixStream), -} + use prost_amino::Message; + use tendermint_p2p::secret_connection::{self, SecretConnection}; -enum KmsConnection { - /// Secret connection type - Tcp(SecretConnection), + use tmkms::{ + amino_types::{self, *}, + config::validator::ProtocolVersion, + connection::unix::UnixConnection, + }; - /// UNIX connection type - Unix(UnixConnection), -} + // /// Integration tests for the KMS command-line interface + // use super::cli; -impl io::Write for KmsConnection { - fn write(&mut self, data: &[u8]) -> Result { - match *self { - KmsConnection::Tcp(ref mut conn) => conn.write(data), - KmsConnection::Unix(ref mut conn) => conn.write(data), + // /// Path to the KMS executable + // const KMS_EXE_PATH: &str = "target/debug/tmkms"; + + /// Path to the example validator signing key + const SIGNING_KEY_PATH: &str = "tests/support/signing.key"; + + enum KmsSocket { + /// TCP socket type + TCP(TcpStream), + + /// UNIX socket type + UNIX(UnixStream), + } + + enum KmsConnection { + /// Secret connection type + Tcp(SecretConnection), + + /// UNIX connection type + Unix(UnixConnection), + } + + impl io::Write for KmsConnection { + fn write(&mut self, data: &[u8]) -> Result { + match *self { + KmsConnection::Tcp(ref mut conn) => conn.write(data), + KmsConnection::Unix(ref mut conn) => conn.write(data), + } + } + + fn flush(&mut self) -> Result<(), io::Error> { + match *self { + KmsConnection::Tcp(ref mut conn) => conn.flush(), + KmsConnection::Unix(ref mut conn) => conn.flush(), + } } } @@ -125,13 +139,62 @@ impl KmsProcess { } } - /// Create a config file for a TCP KMS and return its path - fn create_tcp_config(port: u16) -> NamedTempFile { - let mut config_file = NamedTempFile::new().unwrap(); - let pub_key = test_ed25519_keypair().public; - let peer_id = secret_connection::PublicKey::from(pub_key).peer_id(); + impl KmsProcess { + /// Spawn the KMS process and wait for an incoming TCP connection + pub fn create_tcp() -> 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); + + // Listen on a random port + let listener = TcpListener::bind(format!("{}:{}", "127.0.0.1", port)).unwrap(); + + let args = &["start", "-c", config.path().to_str().unwrap()]; + let process = Command::new(super::KMS_EXE_PATH) + .args(args) + .spawn() + .unwrap(); + + let (socket, _) = listener.accept().unwrap(); + Self { + process: process, + socket: KmsSocket::TCP(socket), + } + } + + /// Spawn the KMS process and connect to the Unix listener + pub fn create_unix() -> 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-{}{:06}.sock", letter, number); + let config = KmsProcess::create_unix_config(&socket_path); + + // Start listening for connections via the Unix socket + let listener = UnixListener::bind(socket_path).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(super::KMS_EXE_PATH) + .args(args) + .spawn() + .unwrap(); + + let (socket, _) = listener.accept().unwrap(); + Self { + process: process, + socket: KmsSocket::UNIX(socket), + } + } + + /// Create a config file for a TCP KMS and return its path + fn create_tcp_config(port: u16) -> NamedTempFile { + let mut config_file = NamedTempFile::new().unwrap(); + let pub_key = test_ed25519_keypair().public; + let peer_id = secret_connection::PublicKey::from(pub_key).peer_id(); - writeln!( + writeln!( config_file, r#" [[chain]] @@ -155,13 +218,13 @@ impl KmsProcess { ) .unwrap(); - config_file - } + config_file + } - /// Create a config file for a UNIX KMS and return its path - fn create_unix_config(socket_path: &str) -> NamedTempFile { - let mut config_file = NamedTempFile::new().unwrap(); - writeln!( + /// Create a config file for a UNIX KMS and return its path + fn create_unix_config(socket_path: &str) -> NamedTempFile { + let mut config_file = NamedTempFile::new().unwrap(); + writeln!( config_file, r#" [[chain]] @@ -182,363 +245,367 @@ impl KmsProcess { ) .unwrap(); - config_file - } + config_file + } - /// Get a connection from the socket - pub fn create_connection(&self) -> KmsConnection { - match self.socket { - KmsSocket::TCP(ref sock) => { - // we use the same key for both sides: - let identity_keypair = test_ed25519_keypair(); - - // Here we reply to the kms with a "remote" ephermal key, auth signature etc: - let socket_cp = sock.try_clone().unwrap(); - - KmsConnection::Tcp( - SecretConnection::new( - socket_cp, - identity_keypair, - secret_connection::Version::Legacy, + /// Get a connection from the socket + pub fn create_connection(&self) -> KmsConnection { + match self.socket { + KmsSocket::TCP(ref sock) => { + // we use the same key for both sides: + let identity_keypair = test_ed25519_keypair(); + + // Here we reply to the kms with a "remote" ephermal key, auth signature etc: + let socket_cp = sock.try_clone().unwrap(); + + KmsConnection::Tcp( + SecretConnection::new( + socket_cp, + identity_keypair, + secret_connection::Version::Legacy, + ) + .unwrap(), ) - .unwrap(), - ) - } + } - KmsSocket::UNIX(ref sock) => { - let socket_cp = sock.try_clone().unwrap(); + KmsSocket::UNIX(ref sock) => { + let socket_cp = sock.try_clone().unwrap(); - KmsConnection::Unix(UnixConnection::new(socket_cp)) + KmsConnection::Unix(UnixConnection::new(socket_cp)) + } } } } -} -/// A struct to hold protocol integration tests contexts -struct ProtocolTester { - tcp_device: KmsProcess, - tcp_connection: KmsConnection, - unix_device: KmsProcess, - unix_connection: KmsConnection, -} + /// A struct to hold protocol integration tests contexts + struct ProtocolTester { + tcp_device: KmsProcess, + tcp_connection: KmsConnection, + unix_device: KmsProcess, + unix_connection: KmsConnection, + } -impl ProtocolTester { - pub fn apply(functor: F) - where - F: FnOnce(ProtocolTester), - { - let tcp_device = KmsProcess::create_tcp(); - let tcp_connection = tcp_device.create_connection(); - let unix_device = KmsProcess::create_unix(); - let unix_connection = unix_device.create_connection(); - - functor(Self { - tcp_device, - tcp_connection, - unix_device, - unix_connection, - }); + impl ProtocolTester { + pub fn apply(functor: F) + where + F: FnOnce(ProtocolTester), + { + let tcp_device = KmsProcess::create_tcp(); + let tcp_connection = tcp_device.create_connection(); + let unix_device = KmsProcess::create_unix(); + let unix_connection = unix_device.create_connection(); + + functor(Self { + tcp_device, + tcp_connection, + unix_device, + unix_connection, + }); + } } -} -impl Drop for ProtocolTester { - fn drop(&mut self) { - self.tcp_device.process.kill().unwrap(); - self.unix_device.process.kill().unwrap(); + impl Drop for ProtocolTester { + fn drop(&mut self) { + self.tcp_device.process.kill().unwrap(); + self.unix_device.process.kill().unwrap(); - match fs::remove_file("test_chain_id_priv_validator_state.json") { - Err(ref e) if e.kind() != io::ErrorKind::NotFound => { - panic!("{}", e); + match fs::remove_file("test_chain_id_priv_validator_state.json") { + Err(ref e) if e.kind() != io::ErrorKind::NotFound => { + panic!("{}", e); + } + _ => (), } - _ => (), } } -} -impl io::Write for ProtocolTester { - fn write(&mut self, data: &[u8]) -> Result { - let unix_sz = self.unix_connection.write(data)?; - let tcp_sz = self.tcp_connection.write(data)?; + impl io::Write for ProtocolTester { + fn write(&mut self, data: &[u8]) -> Result { + let unix_sz = self.unix_connection.write(data)?; + let tcp_sz = self.tcp_connection.write(data)?; - // Assert caller sanity - assert!(unix_sz == tcp_sz); - Ok(unix_sz) - } + // Assert caller sanity + assert!(unix_sz == tcp_sz); + Ok(unix_sz) + } - fn flush(&mut self) -> Result<(), io::Error> { - self.unix_connection.flush()?; - self.tcp_connection.flush()?; - Ok(()) + fn flush(&mut self) -> Result<(), io::Error> { + self.unix_connection.flush()?; + self.tcp_connection.flush()?; + Ok(()) + } } -} -impl io::Read for ProtocolTester { - fn read(&mut self, data: &mut [u8]) -> Result { - let mut unix_buf = vec![0u8; data.len()]; + impl io::Read for ProtocolTester { + fn read(&mut self, data: &mut [u8]) -> Result { + let mut unix_buf = vec![0u8; data.len()]; - self.tcp_connection.read(data)?; - let unix_sz = self.unix_connection.read(&mut unix_buf)?; + self.tcp_connection.read(data)?; + let unix_sz = self.unix_connection.read(&mut unix_buf)?; - // Assert handler sanity - if unix_buf != data { - warn!("binary protocol differs between TCP and UNIX sockets"); - } + // Assert handler sanity + if unix_buf != data { + warn!("binary protocol differs between TCP and UNIX sockets"); + } - Ok(unix_sz) + Ok(unix_sz) + } } -} -/// Get the Ed25519 signing keypair used by the tests -fn test_ed25519_keypair() -> ed25519::Keypair { - tmkms::key_utils::load_base64_ed25519_key(SIGNING_KEY_PATH).unwrap() -} - -/// Extract the actual length of an amino message -pub fn extract_actual_len(buf: &[u8]) -> Result { - let mut buff = Cursor::new(buf); - let actual_len = prost_amino::encoding::decode_varint(&mut buff)?; - if actual_len == 0 { - return Ok(1); + /// Get the Ed25519 signing keypair used by the tests + fn test_ed25519_keypair() -> ed25519::Keypair { + tmkms::key_utils::load_base64_ed25519_key(SIGNING_KEY_PATH).unwrap() } - Ok(actual_len + (prost_amino::encoding::encoded_len_varint(actual_len) as u64)) -} -#[test] -fn test_handle_and_sign_proposal() { - let chain_id = "test_chain_id"; - let pub_key = test_ed25519_keypair().public; - - let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); - let t = TimeMsg { - seconds: dt.timestamp(), - nanos: dt.timestamp_subsec_nanos() as i32, - }; + /// Extract the actual length of an amino message + pub fn extract_actual_len(buf: &[u8]) -> Result { + let mut buff = Cursor::new(buf); + let actual_len = prost_amino::encoding::decode_varint(&mut buff)?; + if actual_len == 0 { + return Ok(1); + } + Ok(actual_len + (prost_amino::encoding::encoded_len_varint(actual_len) as u64)) + } - ProtocolTester::apply(|mut pt| { - let proposal = amino_types::proposal::Proposal { - msg_type: amino_types::SignedMsgType::Proposal.to_u32(), - height: 12345, - round: 1, - timestamp: Some(t), - pol_round: -1, - block_id: None, - signature: vec![], - }; + #[test] + fn test_handle_and_sign_proposal() { + let chain_id = "test_chain_id"; + let pub_key = test_ed25519_keypair().public; - let spr = amino_types::proposal::SignProposalRequest { - proposal: Some(proposal), + let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); + let t = TimeMsg { + seconds: dt.timestamp(), + nanos: dt.timestamp_subsec_nanos() as i32, }; - let mut buf = vec![]; - spr.encode(&mut buf).unwrap(); - pt.write_all(&buf).unwrap(); - - // receive response: - let mut resp_buf = vec![0u8; 1024]; - pt.read(&mut resp_buf).unwrap(); - - let actual_len = extract_actual_len(&resp_buf).unwrap(); - let mut resp = vec![0u8; actual_len as usize]; - resp.copy_from_slice(&mut resp_buf[..(actual_len as usize)]); - - let p_req = proposal::SignedProposalResponse::decode(resp.as_ref()) - .expect("decoding proposal failed"); - let mut sign_bytes: Vec = vec![]; - spr.sign_bytes( - chain_id.parse().unwrap(), - ProtocolVersion::Legacy, - &mut sign_bytes, - ) - .unwrap(); - - let prop: amino_types::proposal::Proposal = p_req - .proposal - .expect("proposal should be embedded but none was found"); - - let signature = ed25519::Signature::try_from(prop.signature.as_slice()).unwrap(); - let msg: &[u8] = sign_bytes.as_slice(); - - assert!(pub_key.verify(msg, &signature).is_ok()); - }); -} - -#[test] -fn test_handle_and_sign_vote() { - let chain_id = "test_chain_id"; - let pub_key = test_ed25519_keypair().public; - - let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); - let t = TimeMsg { - seconds: dt.timestamp(), - nanos: dt.timestamp_subsec_nanos() as i32, - }; + ProtocolTester::apply(|mut pt| { + let proposal = amino_types::proposal::Proposal { + msg_type: amino_types::SignedMsgType::Proposal.to_u32(), + height: 12345, + round: 1, + timestamp: Some(t), + pol_round: -1, + block_id: None, + signature: vec![], + }; + + let spr = amino_types::proposal::SignProposalRequest { + proposal: Some(proposal), + }; + + let mut buf = vec![]; + spr.encode(&mut buf).unwrap(); + pt.write_all(&buf).unwrap(); + + // receive response: + let mut resp_buf = vec![0u8; 1024]; + pt.read(&mut resp_buf).unwrap(); + + let actual_len = extract_actual_len(&resp_buf).unwrap(); + let mut resp = vec![0u8; actual_len as usize]; + resp.copy_from_slice(&mut resp_buf[..(actual_len as usize)]); + + let p_req = proposal::SignedProposalResponse::decode(resp.as_ref()) + .expect("decoding proposal failed"); + let mut sign_bytes: Vec = vec![]; + spr.sign_bytes( + chain_id.parse().unwrap(), + ProtocolVersion::Legacy, + &mut sign_bytes, + ) + .unwrap(); + + let prop: amino_types::proposal::Proposal = p_req + .proposal + .expect("proposal should be embedded but none was found"); + + let signature = ed25519::Signature::try_from(prop.signature.as_slice()).unwrap(); + let msg: &[u8] = sign_bytes.as_slice(); + + assert!(pub_key.verify(msg, &signature).is_ok()); + }); + } - ProtocolTester::apply(|mut pt| { - let vote_msg = amino_types::vote::Vote { - vote_type: 0x01, - height: 12345, - round: 2, - timestamp: Some(t), - block_id: Some(BlockId { - hash: b"some hash00000000000000000000000".to_vec(), - parts_header: Some(PartsSetHeader { - total: 1000000, - hash: b"parts_hash0000000000000000000000".to_vec(), - }), - }), - validator_address: vec![ - 0xa3, 0xb2, 0xcc, 0xdd, 0x71, 0x86, 0xf1, 0x68, 0x5f, 0x21, 0xf2, 0x48, 0x2a, 0xf4, - 0xfb, 0x34, 0x46, 0xa8, 0x4b, 0x35, - ], - validator_index: 56789, - signature: vec![], - }; + #[test] + fn test_handle_and_sign_vote() { + let chain_id = "test_chain_id"; + let pub_key = test_ed25519_keypair().public; - let svr = amino_types::vote::SignVoteRequest { - vote: Some(vote_msg), + let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); + let t = TimeMsg { + seconds: dt.timestamp(), + nanos: dt.timestamp_subsec_nanos() as i32, }; - let mut buf = vec![]; - svr.encode(&mut buf).unwrap(); - pt.write_all(&buf).unwrap(); - - // receive response: - let mut resp_buf = vec![0u8; 1024]; - pt.read(&mut resp_buf).unwrap(); - - let actual_len = extract_actual_len(&resp_buf).unwrap(); - let mut resp = vec![0u8; actual_len as usize]; - resp.copy_from_slice(&resp_buf[..actual_len as usize]); - - let v_resp = vote::SignedVoteResponse::decode(resp.as_ref()).expect("decoding vote failed"); - let mut sign_bytes: Vec = vec![]; - svr.sign_bytes( - chain_id.parse().unwrap(), - ProtocolVersion::Legacy, - &mut sign_bytes, - ) - .unwrap(); - - let vote_msg: amino_types::vote::Vote = v_resp - .vote - .expect("vote should be embedded int the response but none was found"); - - let sig: Vec = vote_msg.signature; - assert_ne!(sig.len(), 0); - - let signature = ed25519::Signature::try_from(sig.as_slice()).unwrap(); - let msg: &[u8] = sign_bytes.as_slice(); - assert!(pub_key.verify(msg, &signature).is_ok()); - }); -} - -#[test] -#[should_panic] -fn test_exceed_max_height() { - let chain_id = "test_chain_id"; - let pub_key = test_ed25519_keypair().public; - - let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); - let t = TimeMsg { - seconds: dt.timestamp(), - nanos: dt.timestamp_subsec_nanos() as i32, - }; - - ProtocolTester::apply(|mut pt| { - let vote_msg = amino_types::vote::Vote { - vote_type: 0x01, - height: 500001, - round: 2, - timestamp: Some(t), - block_id: Some(BlockId { - hash: b"some hash00000000000000000000000".to_vec(), - parts_header: Some(PartsSetHeader { - total: 1000000, - hash: b"parts_hash0000000000000000000000".to_vec(), + ProtocolTester::apply(|mut pt| { + let vote_msg = amino_types::vote::Vote { + vote_type: 0x01, + height: 12345, + round: 2, + timestamp: Some(t), + block_id: Some(BlockId { + hash: b"some hash00000000000000000000000".to_vec(), + parts_header: Some(PartsSetHeader { + total: 1000000, + hash: b"parts_hash0000000000000000000000".to_vec(), + }), }), - }), - validator_address: vec![ - 0xa3, 0xb2, 0xcc, 0xdd, 0x71, 0x86, 0xf1, 0x68, 0x5f, 0x21, 0xf2, 0x48, 0x2a, 0xf4, - 0xfb, 0x34, 0x46, 0xa8, 0x4b, 0x35, - ], - validator_index: 56789, - signature: vec![], - }; - - let svr = amino_types::vote::SignVoteRequest { - vote: Some(vote_msg), - }; - let mut buf = vec![]; - svr.encode(&mut buf).unwrap(); - pt.write_all(&buf).unwrap(); - - // receive response: - let mut resp_buf = vec![0u8; 1024]; - pt.read(&mut resp_buf).unwrap(); - - let actual_len = extract_actual_len(&resp_buf).unwrap(); - let mut resp = vec![0u8; actual_len as usize]; - resp.copy_from_slice(&resp_buf[..actual_len as usize]); - - let v_resp = vote::SignedVoteResponse::decode(resp.as_ref()).expect("decoding vote failed"); - let mut sign_bytes: Vec = vec![]; - svr.sign_bytes( - chain_id.parse().unwrap(), - ProtocolVersion::Legacy, - &mut sign_bytes, - ) - .unwrap(); - - let vote_msg: amino_types::vote::Vote = v_resp - .vote - .expect("vote should be embedded int the response but none was found"); + validator_address: vec![ + 0xa3, 0xb2, 0xcc, 0xdd, 0x71, 0x86, 0xf1, 0x68, 0x5f, 0x21, 0xf2, 0x48, 0x2a, + 0xf4, 0xfb, 0x34, 0x46, 0xa8, 0x4b, 0x35, + ], + validator_index: 56789, + signature: vec![], + }; + + let svr = amino_types::vote::SignVoteRequest { + vote: Some(vote_msg), + }; + let mut buf = vec![]; + svr.encode(&mut buf).unwrap(); + pt.write_all(&buf).unwrap(); + + // receive response: + let mut resp_buf = vec![0u8; 1024]; + pt.read(&mut resp_buf).unwrap(); + + let actual_len = extract_actual_len(&resp_buf).unwrap(); + let mut resp = vec![0u8; actual_len as usize]; + resp.copy_from_slice(&resp_buf[..actual_len as usize]); + + let v_resp = + vote::SignedVoteResponse::decode(resp.as_ref()).expect("decoding vote failed"); + let mut sign_bytes: Vec = vec![]; + svr.sign_bytes( + chain_id.parse().unwrap(), + ProtocolVersion::Legacy, + &mut sign_bytes, + ) + .unwrap(); + + let vote_msg: amino_types::vote::Vote = v_resp + .vote + .expect("vote should be embedded int the response but none was found"); + + let sig: Vec = vote_msg.signature; + assert_ne!(sig.len(), 0); + + let signature = ed25519::Signature::try_from(sig.as_slice()).unwrap(); + let msg: &[u8] = sign_bytes.as_slice(); + + assert!(pub_key.verify(msg, &signature).is_ok()); + }); + } - let sig: Vec = vote_msg.signature; - assert_ne!(sig.len(), 0); + #[test] + #[should_panic] + fn test_exceed_max_height() { + let chain_id = "test_chain_id"; + let pub_key = test_ed25519_keypair().public; - let signature = ed25519::Signature::try_from(sig.as_slice()).unwrap(); - let msg: &[u8] = sign_bytes.as_slice(); + let dt = "2018-02-11T07:09:22.765Z".parse::>().unwrap(); + let t = TimeMsg { + seconds: dt.timestamp(), + nanos: dt.timestamp_subsec_nanos() as i32, + }; - assert!(pub_key.verify(msg, &signature).is_ok()); - }); -} + ProtocolTester::apply(|mut pt| { + let vote_msg = amino_types::vote::Vote { + vote_type: 0x01, + height: 500001, + round: 2, + timestamp: Some(t), + block_id: Some(BlockId { + hash: b"some hash00000000000000000000000".to_vec(), + parts_header: Some(PartsSetHeader { + total: 1000000, + hash: b"parts_hash0000000000000000000000".to_vec(), + }), + }), + validator_address: vec![ + 0xa3, 0xb2, 0xcc, 0xdd, 0x71, 0x86, 0xf1, 0x68, 0x5f, 0x21, 0xf2, 0x48, 0x2a, + 0xf4, 0xfb, 0x34, 0x46, 0xa8, 0x4b, 0x35, + ], + validator_index: 56789, + signature: vec![], + }; + + let svr = amino_types::vote::SignVoteRequest { + vote: Some(vote_msg), + }; + let mut buf = vec![]; + svr.encode(&mut buf).unwrap(); + pt.write_all(&buf).unwrap(); + + // receive response: + let mut resp_buf = vec![0u8; 1024]; + pt.read(&mut resp_buf).unwrap(); + + let actual_len = extract_actual_len(&resp_buf).unwrap(); + let mut resp = vec![0u8; actual_len as usize]; + resp.copy_from_slice(&resp_buf[..actual_len as usize]); + + let v_resp = + vote::SignedVoteResponse::decode(resp.as_ref()).expect("decoding vote failed"); + let mut sign_bytes: Vec = vec![]; + svr.sign_bytes( + chain_id.parse().unwrap(), + ProtocolVersion::Legacy, + &mut sign_bytes, + ) + .unwrap(); + + let vote_msg: amino_types::vote::Vote = v_resp + .vote + .expect("vote should be embedded int the response but none was found"); + + let sig: Vec = vote_msg.signature; + assert_ne!(sig.len(), 0); + + let signature = ed25519::Signature::try_from(sig.as_slice()).unwrap(); + let msg: &[u8] = sign_bytes.as_slice(); + + assert!(pub_key.verify(msg, &signature).is_ok()); + }); + } -#[test] -fn test_handle_and_sign_get_publickey() { - ProtocolTester::apply(|mut pt| { - let mut buf = vec![]; + #[test] + fn test_handle_and_sign_get_publickey() { + ProtocolTester::apply(|mut pt| { + let mut buf = vec![]; - PubKeyRequest {}.encode(&mut buf).unwrap(); + PubKeyRequest {}.encode(&mut buf).unwrap(); - pt.write_all(&buf).unwrap(); + pt.write_all(&buf).unwrap(); - // receive response: - let mut resp_buf = vec![0u8; 1024]; - pt.read(&mut resp_buf).unwrap(); + // receive response: + let mut resp_buf = vec![0u8; 1024]; + pt.read(&mut resp_buf).unwrap(); - let actual_len = extract_actual_len(&resp_buf).unwrap(); - let mut resp = vec![0u8; actual_len as usize]; - resp.copy_from_slice(&resp_buf[..actual_len as usize]); + let actual_len = extract_actual_len(&resp_buf).unwrap(); + let mut resp = vec![0u8; actual_len as usize]; + resp.copy_from_slice(&resp_buf[..actual_len as usize]); - let pk_resp = PubKeyResponse::decode(resp.as_ref()).expect("decoding public key failed"); - assert_ne!(pk_resp.pub_key_ed25519.len(), 0); - }); -} + let pk_resp = + PubKeyResponse::decode(resp.as_ref()).expect("decoding public key failed"); + assert_ne!(pk_resp.pub_key_ed25519.len(), 0); + }); + } -#[test] -fn test_handle_and_sign_ping_pong() { - ProtocolTester::apply(|mut pt| { - let mut buf = vec![]; - PingRequest {}.encode(&mut buf).unwrap(); - pt.write_all(&buf).unwrap(); - - // receive response: - let mut resp_buf = vec![0u8; 1024]; - pt.read(&mut resp_buf).unwrap(); - - let actual_len = extract_actual_len(&resp_buf).unwrap(); - let mut resp = vec![0u8; actual_len as usize]; - resp.copy_from_slice(&resp_buf[..actual_len as usize]); - PingResponse::decode(resp.as_ref()).expect("decoding ping response failed"); - }); + #[test] + fn test_handle_and_sign_ping_pong() { + ProtocolTester::apply(|mut pt| { + let mut buf = vec![]; + PingRequest {}.encode(&mut buf).unwrap(); + pt.write_all(&buf).unwrap(); + + // receive response: + let mut resp_buf = vec![0u8; 1024]; + pt.read(&mut resp_buf).unwrap(); + + let actual_len = extract_actual_len(&resp_buf).unwrap(); + let mut resp = vec![0u8; actual_len as usize]; + resp.copy_from_slice(&resp_buf[..actual_len as usize]); + PingResponse::decode(resp.as_ref()).expect("decoding ping response failed"); + }); + } } diff --git a/tmkms-hashicorp.toml b/tmkms-hashicorp.toml new file mode 100644 index 00000000..40ac8e2d --- /dev/null +++ b/tmkms-hashicorp.toml @@ -0,0 +1,41 @@ +# 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]] +chain_id = "cosmoshub-4" +api_endpoint= "http://127.0.0.1:8200" +access_token="hvs.CAESIBwDAKQh2JKuxibeHOV-jmc2T68loAKLNWO2_QX1l7L3Gh4KHGh2cy5iYnRJRVVLdHBqTVMwUGdLZFNGWjNCZkc" +pk_name="cosmoshub-sign-key" + +[[providers.hashicorp]] +chain_id = "cosmoshub-4" +api_endpoint= "http://127.0.0.1:8200" +access_token="hvs.CAESIBwDAKQh2JKuxibeHOV-jmc2T68loAKLNWO2_QX1l7L3Gh4KHGh2cy5iYnRJRVVLdHBqTVMwUGdLZFNGWjNCZkc" +pk_name="cosmoshub-sign-key" + + +## 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 + + +