├── .cargo └── audit.toml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── security_audit.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.fortanixdsm.md ├── README.md ├── README.yubihsm.md ├── src ├── application.rs ├── bin │ └── tmkms │ │ └── main.rs ├── chain.rs ├── chain │ ├── guard.rs │ ├── registry.rs │ ├── state.rs │ └── state │ │ ├── error.rs │ │ └── hook.rs ├── client.rs ├── commands.rs ├── commands │ ├── init.rs │ ├── init │ │ ├── config_builder.rs │ │ ├── networks.rs │ │ └── templates │ │ │ ├── keyring │ │ │ ├── fortanixdsm.toml │ │ │ ├── ledgertm.toml │ │ │ ├── softsign_account.toml │ │ │ ├── softsign_consensus.toml │ │ │ ├── yubihsm.toml │ │ │ └── yubihsm_server.toml │ │ │ ├── networks │ │ │ ├── columbus.toml │ │ │ ├── cosmoshub.toml │ │ │ ├── irishub.toml │ │ │ ├── osmosis.toml │ │ │ ├── persistence.toml │ │ │ └── sentinelhub.toml │ │ │ ├── schema │ │ │ ├── cosmos-sdk.toml │ │ │ ├── iris.toml │ │ │ ├── osmosis.toml │ │ │ ├── persistence.toml │ │ │ ├── sentinelhub.toml │ │ │ └── terra.toml │ │ │ └── validator.toml │ ├── ledger.rs │ ├── softsign.rs │ ├── softsign │ │ ├── import.rs │ │ └── keygen.rs │ ├── start.rs │ ├── version.rs │ ├── yubihsm.rs │ └── yubihsm │ │ ├── detect.rs │ │ ├── keys.rs │ │ ├── keys │ │ ├── export.rs │ │ ├── generate.rs │ │ ├── import.rs │ │ └── list.rs │ │ ├── setup.rs │ │ └── test.rs ├── config.rs ├── config │ ├── chain.rs │ ├── chain │ │ └── hook.rs │ ├── provider.rs │ ├── provider │ │ ├── fortanixdsm.rs │ │ ├── ledgertm.rs │ │ ├── softsign.rs │ │ └── yubihsm.rs │ └── validator.rs ├── connection.rs ├── connection │ ├── tcp.rs │ └── unix.rs ├── error.rs ├── key_utils.rs ├── keyring.rs ├── keyring │ ├── ecdsa.rs │ ├── ed25519.rs │ ├── ed25519 │ │ ├── signing_key.rs │ │ └── verifying_key.rs │ ├── format.rs │ ├── providers.rs │ ├── providers │ │ ├── fortanixdsm.rs │ │ ├── ledgertm.rs │ │ ├── ledgertm │ │ │ ├── client.rs │ │ │ ├── error.rs │ │ │ └── signer.rs │ │ ├── softsign.rs │ │ └── yubihsm.rs │ └── signature.rs ├── lib.rs ├── prelude.rs ├── privval.rs ├── rpc.rs ├── session.rs └── yubihsm.rs ├── tests ├── cli │ ├── init.rs │ ├── mod.rs │ ├── version.rs │ ├── yubihsm.rs │ └── yubihsm │ │ ├── detect.rs │ │ ├── keys.rs │ │ └── keys │ │ ├── export.rs │ │ ├── generate.rs │ │ ├── import.rs │ │ └── list.rs ├── integration.rs └── support │ ├── buffer-underflow-proposal.bin │ ├── gen-validator-integration-cfg.sh │ ├── kms_yubihsm_mock.toml │ ├── priv_validator_mock.json │ ├── run-harness-tests.sh │ ├── secret_connection.key │ ├── signing_ed25519.key │ └── signing_secp256k1.key └── tmkms.toml.example /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/RustSec/cargo-audit/blob/master/audit.toml.example 2 | 3 | [advisories] 4 | ignore = [ 5 | "RUSTSEC-2019-0036", # failure: type confusion if __private_get_type_id__ is overridden 6 | "RUSTSEC-2020-0036", # failure is officially deprecated/unmaintained 7 | "RUSTSEC-2024-0421", 8 | "RUSTSEC-2024-0436", 9 | ] 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: main 7 | 8 | env: 9 | CARGO_INCREMENTAL: 0 10 | RUSTFLAGS: "-Dwarnings" 11 | 12 | jobs: 13 | check: 14 | name: Check 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v1 19 | 20 | - name: Cache cargo registry 21 | uses: actions/cache@v4 22 | with: 23 | path: ~/.cargo/registry 24 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} 25 | 26 | - name: Cache cargo index 27 | uses: actions/cache@v4 28 | with: 29 | path: ~/.cargo/git 30 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }} 31 | 32 | - name: Install stable toolchain 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: stable 36 | override: true 37 | 38 | - name: Install libudev-dev 39 | run: sudo apt-get update && sudo apt-get install libudev-dev 40 | 41 | - name: Run cargo check 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: check 45 | args: --all-features 46 | 47 | build: 48 | name: Build 49 | strategy: 50 | matrix: 51 | toolchain: 52 | - stable 53 | - 1.81.0 # MSRV 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout sources 57 | uses: actions/checkout@v1 58 | 59 | - name: Cache cargo registry 60 | uses: actions/cache@v4 61 | with: 62 | path: ~/.cargo/registry 63 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} 64 | 65 | - name: Cache cargo index 66 | uses: actions/cache@v4 67 | with: 68 | path: ~/.cargo/git 69 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }} 70 | 71 | - name: Cache cargo build 72 | uses: actions/cache@v4 73 | with: 74 | path: target 75 | key: ${{ runner.os }}-rust-${{ matrix.toolchain }}-cargo-build-target-${{ hashFiles('Cargo.lock') }} 76 | 77 | - name: Install toolchain 78 | uses: actions-rs/toolchain@v1 79 | with: 80 | toolchain: ${{ matrix.toolchain }} 81 | override: true 82 | 83 | - name: Install libudev-dev 84 | run: sudo apt-get update && sudo apt-get install libudev-dev 85 | 86 | - run: cargo build --no-default-features --features softsign --release 87 | - run: cargo build --features=yubihsm --release 88 | - run: cargo build --features=yubihsm-server --release 89 | - run: cargo build --features=ledger --release 90 | - run: cargo build --features=yubihsm-server,ledger,softsign --release 91 | 92 | test: 93 | name: Test Suite 94 | strategy: 95 | matrix: 96 | toolchain: 97 | - stable 98 | - 1.81.0 # MSRV 99 | runs-on: ubuntu-latest 100 | steps: 101 | - name: Checkout sources 102 | uses: actions/checkout@v1 103 | 104 | - name: Cache cargo registry 105 | uses: actions/cache@v4 106 | with: 107 | path: ~/.cargo/registry 108 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} 109 | 110 | - name: Cache cargo index 111 | uses: actions/cache@v4 112 | with: 113 | path: ~/.cargo/git 114 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }} 115 | 116 | - name: Cache cargo build 117 | uses: actions/cache@v4 118 | with: 119 | path: target 120 | key: ${{ runner.os }}-rust-${{ matrix.toolchain }}-cargo-build-target-${{ hashFiles('Cargo.lock') }} 121 | 122 | - name: Install toolchain 123 | uses: actions-rs/toolchain@v1 124 | with: 125 | toolchain: ${{ matrix.toolchain }} 126 | override: true 127 | 128 | - name: Install libudev-dev 129 | run: sudo apt-get update && sudo apt-get install libudev-dev 130 | 131 | - name: Run cargo test 132 | uses: actions-rs/cargo@v1 133 | with: 134 | command: test 135 | args: --all-features -- --test-threads 1 136 | 137 | validate: 138 | name: Validate against test harness 139 | runs-on: ubuntu-latest 140 | steps: 141 | - name: Checkout sources 142 | uses: actions/checkout@v1 143 | 144 | - name: Cache cargo registry 145 | uses: actions/cache@v4 146 | with: 147 | path: ~/.cargo/registry 148 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} 149 | 150 | - name: Cache cargo index 151 | uses: actions/cache@v4 152 | with: 153 | path: ~/.cargo/git 154 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }} 155 | 156 | - name: Cache cargo build 157 | uses: actions/cache@v4 158 | with: 159 | path: target 160 | key: ${{ runner.os }}-rust-${{ matrix.toolchain }}-cargo-build-target-${{ hashFiles('Cargo.lock') }} 161 | 162 | - name: Install toolchain 163 | uses: actions-rs/toolchain@v1 164 | with: 165 | toolchain: stable 166 | override: true 167 | 168 | - name: Run cargo build 169 | uses: actions-rs/cargo@v1 170 | with: 171 | command: build 172 | args: --features=softsign --release 173 | 174 | # TODO(tarcieri): install test harness components. See build failure here: 175 | # 176 | # - name: Run test harness 177 | # env: 178 | # TMKMS_BIN: ./target/debug/tmkms 179 | # run: sh tests/support/run-harness-tests.sh 180 | 181 | fmt: 182 | name: Rustfmt 183 | runs-on: ubuntu-latest 184 | steps: 185 | - name: Checkout sources 186 | uses: actions/checkout@v1 187 | 188 | - name: Install stable toolchain 189 | uses: actions-rs/toolchain@v1 190 | with: 191 | toolchain: stable 192 | override: true 193 | 194 | - name: Install rustfmt 195 | run: rustup component add rustfmt 196 | 197 | - name: Run cargo fmt 198 | uses: actions-rs/cargo@v1 199 | with: 200 | command: fmt 201 | args: --all -- --check 202 | 203 | clippy: 204 | name: Clippy 205 | runs-on: ubuntu-latest 206 | steps: 207 | - name: Checkout sources 208 | uses: actions/checkout@v1 209 | 210 | - name: Cache cargo registry 211 | uses: actions/cache@v4 212 | with: 213 | path: ~/.cargo/registry 214 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} 215 | 216 | - name: Cache cargo index 217 | uses: actions/cache@v4 218 | with: 219 | path: ~/.cargo/git 220 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }} 221 | 222 | - name: Install stable toolchain 223 | uses: actions-rs/toolchain@v1 224 | with: 225 | toolchain: 1.81.0 # MSRV 226 | override: true 227 | 228 | - name: Install libudev-dev 229 | run: sudo apt-get update && sudo apt-get install libudev-dev 230 | 231 | - name: Install clippy 232 | run: rustup component add clippy 233 | 234 | - name: Run cargo clippy 235 | uses: actions-rs/cargo@v1 236 | with: 237 | command: clippy 238 | args: --all-features 239 | -------------------------------------------------------------------------------- /.github/workflows/security_audit.yml: -------------------------------------------------------------------------------- 1 | name: Security Audit 2 | on: 3 | pull_request: 4 | paths: 5 | - .github/workflows/security_audit.yml 6 | - '**/Cargo.lock' 7 | push: 8 | branches: master 9 | paths: '**/Cargo.lock' 10 | schedule: 11 | - cron: "0 0 * * *" 12 | 13 | jobs: 14 | security_audit: 15 | name: Security Audit Workspace 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Cache cargo bin 20 | uses: actions/cache@v4 21 | with: 22 | path: ~/.cargo/bin 23 | key: ${{ runner.os }}-cargo-audit-v0.20-ubuntu-v24.04 24 | - uses: rustsec/audit-check@v2 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | tmkms.toml 3 | **/*.rs.bk 4 | **/*priv_validator_state.json 5 | 6 | # Ignore VIM swap files 7 | *.swp 8 | 9 | \.idea/ 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [team@iqlusion.io]. 64 | 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | [team@iqlusion.io]: mailto:team@iqlusion.io 135 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a [code of conduct], please follow it in all your interactions with the project. 7 | 8 | ## Licensing 9 | 10 | Unless you explicitly state otherwise, any contribution intentionally submitted 11 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 12 | licensed as above, without any additional terms or conditions. 13 | 14 | [code of conduct]: ./CODE_OF_CONDUCT.md 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmkms" 3 | description = """ 4 | Tendermint Key Management System: provides isolated, optionally HSM-backed 5 | signing key management for Tendermint applications including validators, 6 | oracles, IBC relayers, and other transaction signing applications 7 | """ 8 | version = "0.14.0" 9 | authors = ["Tony Arcieri ", "Ismail Khoffi "] 10 | license = "Apache-2.0" 11 | repository = "https://github.com/iqlusioninc/tmkms/" 12 | readme = "README.md" 13 | categories = ["cryptography::cryptocurrencies"] 14 | keywords = ["cosmos", "ed25519", "kms", "key-management", "yubihsm"] 15 | edition = "2021" 16 | rust-version = "1.74" 17 | 18 | [dependencies] 19 | abscissa_core = "0.8" 20 | bytes = "1" 21 | chrono = "0.4" 22 | clap = "4" 23 | cosmrs = "0.22" 24 | ed25519 = "2" 25 | ed25519-consensus = "2" 26 | elliptic-curve = { version = "0.13", features = ["pkcs8"], optional = true } 27 | eyre = "0.6" 28 | getrandom = "0.2" 29 | hkd32 = { version = "0.7", default-features = false, features = ["mnemonic"] } 30 | hkdf = "0.12" 31 | k256 = { version = "0.13", features = ["ecdsa", "sha256"] } 32 | ledger = { version = "0.2", optional = true } 33 | once_cell = "1.5" 34 | prost = "0.13" 35 | prost-derive = "0.13" 36 | rand_core = { version = "0.6", features = ["std"] } 37 | rpassword = { version = "7", optional = true } 38 | sdkms = { version = "0.5", optional = true } 39 | serde = { version = "1", features = ["serde_derive"] } 40 | serde_json = "1" 41 | sha2 = "0.10" 42 | signature = { version = "2", features = ["std"] } 43 | subtle = "2" 44 | subtle-encoding = { version = "0.5", features = ["bech32-preview"] } 45 | tempfile = "3" 46 | tendermint = { version = "0.40", features = ["secp256k1"] } 47 | tendermint-config = "0.40" 48 | tendermint-p2p = "0.40" 49 | tendermint-proto = "0.40" 50 | thiserror = "1" 51 | url = { version = "2.2.2", features = ["serde"], optional = true } 52 | uuid = { version = "1", features = ["serde"], optional = true } 53 | wait-timeout = "0.2" 54 | yubihsm = { version = "0.42", features = ["secp256k1", "setup", "usb"], optional = true } 55 | zeroize = "1" 56 | 57 | [dev-dependencies] 58 | abscissa_core = { version = "0.8", features = ["testing"] } 59 | byteorder = "1" 60 | rand = "0.8" 61 | 62 | [features] 63 | softsign = [] 64 | yubihsm-mock = ["yubihsm/mockhsm"] 65 | yubihsm-server = ["yubihsm/http-server", "rpassword"] 66 | fortanixdsm = ["elliptic-curve", "sdkms", "url", "uuid"] 67 | 68 | # Enable integer overflow checks in release builds for security reasons 69 | [profile.release] 70 | overflow-checks = true 71 | 72 | [package.metadata.docs.rs] 73 | all-features = true 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################### 2 | # Test harness for remote signer from Tendermint 3 | 4 | # Configure the version of Tendermint here against which you want to run 5 | # integration tests 6 | ARG TENDERMINT_VERSION=latest 7 | 8 | FROM tendermint/tm-signer-harness:${TENDERMINT_VERSION} AS harness 9 | 10 | USER root 11 | 12 | RUN mkdir -p /harness 13 | 14 | # We need this script to generate configuration for the KMS 15 | COPY tests/support/gen-validator-integration-cfg.sh /harness/ 16 | 17 | # Generate the base configuration data for the Tendermint validator for use 18 | # during integration testing. This will generate the data, by default, in the 19 | # /tendermint directory. 20 | RUN tendermint init --home=/harness && \ 21 | tm-signer-harness extract_key --tmhome=/harness --output=/harness/signing.key && \ 22 | cd /harness && \ 23 | chmod +x gen-validator-integration-cfg.sh && \ 24 | TMHOME=/harness sh ./gen-validator-integration-cfg.sh 25 | 26 | ################################################### 27 | # Tendermint KMS Dockerfile 28 | 29 | FROM centos:7 AS build 30 | 31 | # Install/update RPMs 32 | RUN yum update -y && \ 33 | yum groupinstall -y "Development Tools" && \ 34 | yum install -y \ 35 | centos-release-scl \ 36 | cmake \ 37 | epel-release \ 38 | libudev-devel \ 39 | libusbx-devel \ 40 | openssl-devel \ 41 | sudo && \ 42 | yum install -y --enablerepo=epel libsodium-devel && \ 43 | yum install -y --enablerepo=centos-sclo-rh llvm-toolset-7 && \ 44 | yum clean all && \ 45 | rm -rf /var/cache/yum 46 | 47 | # Set environment variables to enable SCL packages (llvm-toolset-7) 48 | ENV LD_LIBRARY_PATH=/opt/rh/llvm-toolset-7/root/usr/lib64 49 | ENV PATH "/opt/rh/llvm-toolset-7/root/usr/bin:/opt/rh/llvm-toolset-7/root/usr/sbin:$PATH" 50 | ENV PKG_CONFIG_PATH=/opt/rh/llvm-toolset-7/root/usr/lib64/pkgconfig 51 | ENV X_SCLS llvm-toolset-7 52 | 53 | # Create "developer" user 54 | RUN useradd developer && \ 55 | echo 'developer ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/developer 56 | 57 | # Switch to the "developer" user 58 | USER developer 59 | WORKDIR /home/developer 60 | 61 | # Include cargo in the path 62 | ENV PATH "$PATH:/home/developer/.cargo/bin" 63 | 64 | # Install rustup 65 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y && \ 66 | rustup update && \ 67 | rustup component add rustfmt && \ 68 | rustup component add clippy && \ 69 | cargo install cargo-audit 70 | 71 | # Configure Rust environment variables 72 | ENV RUSTFLAGS "-Ctarget-feature=+aes,+ssse3" 73 | ENV RUST_BACKTRACE full 74 | 75 | ################################################### 76 | # Remote validator integration testing 77 | 78 | # We need the generated harness and Tendermint configuration 79 | COPY --from=harness /harness /harness 80 | 81 | # We need the test harness binary 82 | COPY --from=harness /usr/bin/tm-signer-harness /usr/bin/tm-signer-harness 83 | 84 | # We need a secret connection key 85 | COPY tests/support/secret_connection.key /harness/ 86 | 87 | USER root 88 | # Ensure the /harness folder has the right owner 89 | RUN chown -R developer /harness 90 | USER developer 91 | 92 | -------------------------------------------------------------------------------- /README.fortanixdsm.md: -------------------------------------------------------------------------------- 1 | # Fortanix DSM + Tendermint KMS 2 | 3 | Fortanix Data Security Manager (DSM) provides integrated data security with encryption, multicloud key management, tokenization, and other capabilities from one platform. 4 | 5 | This document describes how to configure Fortanix DSM for production use with Tendermint KMS. 6 | 7 | ## Compiling `tmkms` with Fortanix DSM support 8 | 9 | Refer the main README.md for compiling `tmkms` 10 | from source code. You will need the prerequisities mentioned as indicated above. 11 | 12 | There are two ways to install `tmkms` with Fortanix DSM, you need to pass the `--features=fortanixdsm` parameter to cargo. 13 | 14 | ### Compiling from source code (via git) 15 | 16 | `tmkms` can be compiled directly from the git repository source code using the 17 | following method. 18 | 19 | ``` 20 | $ git clone https://github.com/iqlusioninc/tmkms.git && cd tmkms 21 | [...] 22 | $ cargo build --release --features=fortanixdsm 23 | ``` 24 | 25 | If successful, this will produce a `tmkms` executable located at 26 | `./target/release/tmkms` 27 | 28 | ### Installing with the `cargo install` command 29 | 30 | With Rust (1.40+) installed, you can install tmkms with the following: 31 | 32 | ``` 33 | cargo install tmkms --features=fortanixdsm 34 | ``` 35 | 36 | Or to install a specific version (recommended): 37 | 38 | ``` 39 | cargo install tmkms --features=fortanixdsm --version=0.4.0 40 | ``` 41 | 42 | This command installs `tmkms` directly from packages hosted on Rust's 43 | [crates.io] service. Package authenticity is verified via the 44 | [crates.io index] (itself a git repository) and by SHA-256 digests of 45 | released artifacts. 46 | 47 | However, if newer dependencies are available, it may use newer versions 48 | besides the ones which are "locked" in the source code repository. We 49 | cannot verify those dependencies do not contain malicious code. If you would 50 | like to ensure the dependencies in use are identical to the main repository, 51 | please build from source code instead. 52 | 53 | ## Production Fortanix DSM setup 54 | 55 | `tmkms` contains support for Fortanix DSM backend, which enables tmkms to access the secure keys on DSM. This requires creation of the keys on the DSM which can be done by referring to this [guide](https://support.fortanix.com/hc/en-us/articles/360038354592-User-s-Guide-Fortanix-Data-Security-Manager-Key-Lifecycle-Management). Creating, enabling and marking the key for signing and export should enable tmkms to use the keys on DSM. 56 | 57 | ### Configuring `tmkms` for initial setup 58 | 59 | In order to perform setup, `tmkms` needs a configuration file which 60 | contains the authentication details needed to authenticate to the DSM with an API key. 61 | 62 | This configuration should be placed in a file called: `tmkms.toml`. 63 | 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. 64 | 65 | example: 66 | 67 | ```toml 68 | [[providers.fortanixdsm]] 69 | api_endpoint = "https://sdkms.fortanix.com" 70 | api_key = "Nzk5MDQ3ZGUtN2Q2NS00OTRjLTgzMDMtNjQwMTlhYzdmOGUzOlF1SU93ZXJsOFU4VUdEWEdQMmx1dFJOVjlvMTRSd3lhNnVDNVNhVkpZOVhzYVgyc0pOVGRQVGJ0RjZJdmVLMy00X05iTEhxMkowamF3UGVPaXJEWEd3" 71 | signing_keys = [ 72 | { chain_ids = ["$CHAIN_ID"], type = "account", key_id = "72e9ed9e-9eb4-46bd-a135-e78ed9bfd611" }, 73 | { chain_ids = ["$CHAIN_ID"], type = "consensus", key_name = "My Key" }, 74 | ] 75 | ``` 76 | You can get the api key from the app that holds the security object(key) in DSM. Key can be identified by either using the key-id or the key name, which are available in the details of the security object created on DSM. If you already have the key, you can import the key on DSM following the same DSM user guide mentioned above. 77 | 78 | ### Generating keys on DSM 79 | 1. Create a security group on DSM, example 'TMKMS group'. 80 | 2. Create a APP under the same security group on DSM, example 'TMKMS'. Select Authentication method to be 'API Key' and copy the API key for use in config fie (tmkms.toml). 81 | 82 | 3. Create a security Object under the same group in DSM, so that the API key for the app can be used to access the key under the same group. The type of key must be `EC CurveEd25519` for consensus key and `Secp256k1` for account key. Proceed with creation of these keys on DSM and the required key ID has to be passed in the config file, this can be obtained from the details on the security object section on DSM. 83 | 4. To import an existing tendermint key use the following script to convert a tendermint key to Fortanix DSM accepted key format. 84 | ``` 85 | #!/bin/bash 86 | # Usage: tendermint-ed25519.sh 87 | 88 | gokey=$(jq -r .priv_key.value $1 | base64 -d| xxd -p -c 64) 89 | echo 302e 0201 0030 0506 032b 6570 0422 0420 "${gokey:0:64}" | xxd -p -r > $2 90 | echo 302a 3005 0603 2b65 7003 2100 "${gokey:64}" | xxd -p -r > $3 91 | ``` 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tendermint KMS 🔐 2 | 3 | [![Crate][crate-image]][crate-link] 4 | [![Build Status][build-image]][build-link] 5 | [![Apache 2.0 Licensed][license-image]][license-link] 6 | ![MSRV][rustc-image] 7 | 8 | Key Management System for [Tendermint] applications such as 9 | [Cosmos Validators]. 10 | 11 | Provides isolated, optionally HSM-backed signing key management for Tendermint 12 | applications including validators, oracles, IBC relayers, and other transaction 13 | signing applications. 14 | 15 | ## About 16 | 17 | This repository contains `tmkms`, a key management service intended to be deployed 18 | in conjunction with [Tendermint] applications (ideally on separate physical hosts) 19 | which provides the following: 20 | 21 | - **High-availability** access to validator signing keys 22 | - **Double-signing** prevention even in the event the validator process is compromised 23 | - **Hardware security module** storage for validator keys which can survive host compromise 24 | 25 | ## Status 26 | 27 | Tendermint KMS is currently *beta quality*. It has undergone one security audit 28 | with only one low-severity finding. 29 | 30 | ### Double Signing / High Availability 31 | 32 | Tendermint KMS implements *beta quality* double signing detection. 33 | It has undergone some testing, however we do not (yet) recommend using the KMS 34 | in conjunction with multiple simultaneously active validators on the same 35 | network for prolonged periods of time. 36 | 37 | In particular, there is presently **no double signing defense** in the case 38 | that multiple KMS instances are running simultaneously and connecting to 39 | multiple validators on the same network. 40 | 41 | ## Signing Providers 42 | 43 | You **MUST** select one or more signing provider(s) when compiling the KMS, 44 | passed as the argument to the `--features` flag (see below for more 45 | instructions on how to build Tendermint KMS). 46 | 47 | The following signing backend providers are presently supported: 48 | 49 | #### Hardware Security Modules (recommended) 50 | - [FortanixDSM](./README.fortanixdsm.md) (gated under the `fortanixdsm` cargo feature. See [README.fortanixdsm.md](./README.fortanixdsm.md) 51 | - [YubiHSM2] (gated under the `yubihsm` cargo feature. See [README.yubihsm.md][yubihsm2] for more info) 52 | - [Ledger] (gated under the `ledger` cargo feature) 53 | 54 | #### Software-Only (not recommended) 55 | 56 | - `softsign` backend which uses [ed25519-dalek] 57 | 58 | ## Supported Platforms 59 | 60 | `tmkms` should build on any [supported Rust platform] which is also supported 61 | by [libusb], however there are some platforms which meet those criteria which 62 | are unsuitable for cryptography purposes due to lack of constant-time CPU 63 | instructions. Below are some of the available tier 1, 2, and 3 Rust platforms 64 | which meet our minimum criteria for KMS use. 65 | 66 | NOTE: `tmkms` is presently tested on Linux/x86_64. We don't otherwise guarantee 67 | support for any of the platforms below, but they theoretically meet the necessary 68 | prerequisites for support. 69 | 70 | ### Operating Systems 71 | 72 | - Linux (recommended) 73 | - FreeBSD 74 | - NetBSD 75 | - OpenBSD 76 | - macOS 77 | 78 | ### CPU Architectures 79 | 80 | - `x86_64` (recommended) 81 | - `arm` (32-bit ARM) 82 | - `aarch64` (64-bit ARM) 83 | - `riscv32` (32-bit RISC-V) 84 | - `riscv64` (64-bit RISC-V) 85 | 86 | ## Installation 87 | 88 | You will need the following prerequisites: 89 | 90 | - **Rust** (stable; **1.72+**): https://rustup.rs/ 91 | - **C compiler**: e.g. gcc, clang 92 | - **pkg-config** 93 | - **libusb** (1.0+). Install instructions for common platforms: 94 | - Debian/Ubuntu: `apt install libusb-1.0-0-dev` 95 | - RedHat/CentOS: `yum install libusb1-devel` 96 | - macOS (Homebrew): `brew install libusb` 97 | 98 | NOTE (x86_64 only): Configure `RUSTFLAGS` environment variable: 99 | `export RUSTFLAGS=-Ctarget-feature=+aes,+ssse3` 100 | 101 | There are two ways to install `tmkms`: either compiling the source code after 102 | cloning it from git, or using Rust's `cargo install` command. 103 | 104 | ### Compiling from source code (via git) 105 | 106 | `tmkms` can be compiled directly from the git repository source code using the 107 | following method. 108 | 109 | The following example adds `--features=yubihsm` to enable YubiHSM 2 support. 110 | 111 | ``` 112 | $ git clone https://github.com/iqlusioninc/tmkms.git && cd tmkms 113 | [...] 114 | $ cargo build --release --features=yubihsm 115 | ``` 116 | 117 | Alternatively, substitute `--features=ledger` to enable Ledger support. 118 | 119 | If successful, this will produce a `tmkms` executable located at 120 | `./target/release/tmkms` 121 | 122 | ### Installing with the `cargo install` command 123 | 124 | With Rust (1.56+) installed, you can install tmkms with the following: 125 | 126 | ``` 127 | cargo install tmkms --features=yubihsm 128 | ``` 129 | 130 | Or to install a specific version (recommended): 131 | 132 | ``` 133 | cargo install tmkms --features=yubihsm --version=0.4.0 134 | ``` 135 | 136 | Alternatively, substitute `--features=ledger` to enable Ledger support. 137 | 138 | ## Configuration: `tmkms init` 139 | 140 | The `tmkms init` command can be used to generate a directory containing 141 | the configuration files needed to run the KMS. Run the following: 142 | 143 | ``` 144 | $ tmkms init /path/to/kms/home 145 | ``` 146 | 147 | This will output a `tmkms.toml` file, a `kms-identity.key` (used to authenticate 148 | the KMS to the validator), and create `secrets` and `state` subdirectories. 149 | 150 | Please look through `tmkms.toml` after it's generated, as various sections 151 | will require some customization. 152 | 153 | The `tmkms init` command also accepts a `-n` or `--networks` argument which can 154 | be used to specify certain well-known Tendermint chains to initialize: 155 | 156 | ``` 157 | $ tmkms init -n cosmoshub,irishub,columbus /path/to/kms/home 158 | ``` 159 | 160 | ## Running: `tmkms start` 161 | 162 | After creading the configuration, start `tmkms` with the following: 163 | 164 | ``` 165 | $ tmkms start 166 | ``` 167 | 168 | This will read the configuration from the `tmkms.toml` file in the current 169 | working directory. 170 | 171 | To explicitly specify the path to the configuration, use the `-c` flag: 172 | 173 | ``` 174 | $ tmkms start -c /path/to/tmkms.toml 175 | ``` 176 | 177 | ## Development 178 | 179 | The following are instructions for setting up a development environment. 180 | They assume you've already followed steps 1 & 2 from the Installation 181 | section above. 182 | 183 | - Install **rustfmt**: `rustup component add rustfmt` 184 | - Install **clippy**: `rustup component add clippy` 185 | 186 | Alternatively, you can build a Docker image from the [Dockerfile] in the top 187 | level of the repository, which is what is used to run tests in CI. 188 | 189 | Before opening a pull request, please run the checks below: 190 | 191 | ### Testing 192 | 193 | Run the test suite with: 194 | 195 | ``` 196 | cargo test --all-features -- --test-threads 1 197 | ``` 198 | 199 | ### Format checking (rustfmt) 200 | 201 | Make sure your code is well-formatted by running: 202 | 203 | ``` 204 | cargo fmt 205 | ``` 206 | 207 | ### Lint (clippy) 208 | 209 | Lint your code (i.e. check it for common issues) with: 210 | 211 | ``` 212 | cargo clippy 213 | ``` 214 | 215 | ## License 216 | 217 | Licensed under the Apache License, Version 2.0 (the "License"); 218 | you may not use this file except in compliance with the License. 219 | You may obtain a copy of the License at 220 | 221 | https://www.apache.org/licenses/LICENSE-2.0 222 | 223 | Unless required by applicable law or agreed to in writing, software 224 | distributed under the License is distributed on an "AS IS" BASIS, 225 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 226 | See the License for the specific language governing permissions and 227 | limitations under the License. 228 | 229 | [//]: # (badges) 230 | 231 | [crate-image]: https://img.shields.io/crates/v/tmkms.svg 232 | [crate-link]: https://crates.io/crates/tmkms 233 | [build-image]: https://github.com/iqlusioninc/tmkms/workflows/CI/badge.svg?branch=main&event=push 234 | [build-link]: https://github.com/iqlusioninc/tmkms/actions 235 | [license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg 236 | [license-link]: https://github.com/iqlusioninc/tmkms/blob/main/LICENSE 237 | [rustc-image]: https://img.shields.io/badge/rustc-1.74+-blue.svg 238 | 239 | [//]: # (general links) 240 | 241 | [Tendermint]: https://tendermint.com/ 242 | [Cosmos Validators]: https://hub.cosmos.network/main/validators/validator-faq 243 | [YubiHSM2]: https://github.com/iqlusioninc/tmkms/blob/main/README.yubihsm.md 244 | [Ledger]: https://www.ledger.com/ 245 | [ed25519-dalek]: https://github.com/dalek-cryptography/ed25519-dalek 246 | [supported Rust platform]: https://forge.rust-lang.org/platform-support.html 247 | [libusb]: https://libusb.info/ 248 | [Dockerfile]: https://github.com/iqlusioninc/tmkms/blob/main/Dockerfile 249 | -------------------------------------------------------------------------------- /src/application.rs: -------------------------------------------------------------------------------- 1 | //! Abscissa `Application` for the KMS 2 | 3 | use crate::{commands::KmsCommand, config::KmsConfig}; 4 | use abscissa_core::{ 5 | application::{self, AppCell}, 6 | config::{self, CfgCell}, 7 | trace, Application, FrameworkError, StandardPaths, 8 | }; 9 | 10 | /// Application state 11 | pub static APP: AppCell = AppCell::new(); 12 | 13 | /// The `tmkms` application 14 | #[derive(Debug, Default)] 15 | pub struct KmsApplication { 16 | /// Application configuration. 17 | config: CfgCell, 18 | 19 | /// Application state. 20 | state: application::State, 21 | } 22 | 23 | impl Application for KmsApplication { 24 | /// Entrypoint command for this application. 25 | type Cmd = KmsCommand; 26 | 27 | /// Application configuration. 28 | type Cfg = KmsConfig; 29 | 30 | /// Paths to resources within the application. 31 | type Paths = StandardPaths; 32 | 33 | /// Accessor for application configuration. 34 | fn config(&self) -> config::Reader { 35 | self.config.read() 36 | } 37 | 38 | /// Borrow the application state immutably. 39 | fn state(&self) -> &application::State { 40 | &self.state 41 | } 42 | 43 | /// Register all components used by this application. 44 | /// 45 | /// If you would like to add additional components to your application 46 | /// beyond the default ones provided by the framework, this is the place 47 | /// to do so. 48 | fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { 49 | let components = self.framework_components(command)?; 50 | let mut component_registry = self.state.components_mut(); 51 | component_registry.register(components) 52 | } 53 | 54 | /// Post-configuration lifecycle callback. 55 | /// 56 | /// Called regardless of whether config is loaded to indicate this is the 57 | /// time in app lifecycle when configuration would be loaded if 58 | /// possible. 59 | fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> { 60 | let mut component_registry = self.state.components_mut(); 61 | component_registry.after_config(&config)?; 62 | self.config.set_once(config); 63 | Ok(()) 64 | } 65 | 66 | /// Get tracing configuration from command-line options 67 | fn tracing_config(&self, command: &KmsCommand) -> trace::Config { 68 | if command.verbose() { 69 | trace::Config::verbose() 70 | } else { 71 | trace::Config::default() 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/bin/tmkms/main.rs: -------------------------------------------------------------------------------- 1 | //! Main entry point for the `tmkms` executable 2 | 3 | use tmkms::application::APP; 4 | 5 | /// Boot the `tmkms` application 6 | fn main() { 7 | abscissa_core::boot(&APP); 8 | } 9 | -------------------------------------------------------------------------------- /src/chain.rs: -------------------------------------------------------------------------------- 1 | //! Information about particular Tendermint blockchain networks 2 | 3 | mod guard; 4 | mod registry; 5 | pub mod state; 6 | 7 | pub use self::{ 8 | guard::Guard, 9 | registry::{GlobalRegistry, Registry, REGISTRY}, 10 | state::State, 11 | }; 12 | use crate::{ 13 | config::{chain::ChainConfig, KmsConfig}, 14 | error::Error, 15 | keyring::{self, KeyRing}, 16 | prelude::*, 17 | }; 18 | use std::{path::PathBuf, sync::Mutex}; 19 | pub use tendermint::chain::Id; 20 | 21 | /// Information about a particular Tendermint blockchain network 22 | pub struct Chain { 23 | /// ID of a particular chain 24 | pub id: Id, 25 | 26 | /// Should extensions for this chain be signed? 27 | pub sign_extensions: bool, 28 | 29 | /// Signing keyring for this chain 30 | pub keyring: KeyRing, 31 | 32 | /// State from the last block signed for this chain 33 | pub state: Mutex, 34 | } 35 | 36 | impl Chain { 37 | /// Attempt to create a `Chain` state from the given configuration 38 | pub fn from_config(config: &ChainConfig) -> Result { 39 | let state_file = match config.state_file { 40 | Some(ref path) => path.to_owned(), 41 | None => PathBuf::from(&format!("{}_priv_validator_state.json", config.id)), 42 | }; 43 | 44 | let mut state = State::load_state(state_file)?; 45 | 46 | if let Some(ref hook) = config.state_hook { 47 | match state::hook::run(hook) { 48 | Ok(hook_output) => state.update_from_hook_output(hook_output)?, 49 | Err(e) => { 50 | if hook.fail_closed { 51 | return Err(e); 52 | } else { 53 | // fail open: note the error to the log and proceed anyway 54 | error!("error invoking state hook for chain {}: {}", config.id, e); 55 | } 56 | } 57 | } 58 | } 59 | 60 | Ok(Self { 61 | id: config.id.clone(), 62 | sign_extensions: config.sign_extensions, 63 | keyring: KeyRing::new(config.key_format.clone()), 64 | state: Mutex::new(state), 65 | }) 66 | } 67 | } 68 | 69 | /// Initialize the chain registry from the configuration file 70 | pub fn load_config(config: &KmsConfig) -> Result<(), Error> { 71 | for config in &config.chain { 72 | REGISTRY.register(Chain::from_config(config)?)?; 73 | } 74 | 75 | let mut registry = REGISTRY.0.write().unwrap(); 76 | keyring::load_config(&mut registry, &config.providers) 77 | } 78 | -------------------------------------------------------------------------------- /src/chain/guard.rs: -------------------------------------------------------------------------------- 1 | use super::{Chain, Id, Registry}; 2 | use std::sync::RwLockReadGuard; 3 | 4 | /// Wrapper for a `RwLockReadGuard<'static, Registry>`, allowing access to 5 | /// global information about particular Tendermint networks / "chains" 6 | pub struct Guard<'lock>(RwLockReadGuard<'lock, Registry>); 7 | 8 | impl<'lock> From> for Guard<'lock> { 9 | fn from(guard: RwLockReadGuard<'lock, Registry>) -> Guard<'lock> { 10 | Guard(guard) 11 | } 12 | } 13 | 14 | impl Guard<'_> { 15 | /// Get information about a particular chain ID (if registered) 16 | pub fn get_chain(&self, chain_id: &Id) -> Option<&Chain> { 17 | self.0.get_chain(chain_id) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/chain/registry.rs: -------------------------------------------------------------------------------- 1 | //! Registry of information about known Tendermint blockchain networks 2 | 3 | use super::{Chain, Guard, Id}; 4 | use crate::{ 5 | error::{Error, ErrorKind::*}, 6 | keyring, 7 | prelude::*, 8 | Map, 9 | }; 10 | use once_cell::sync::Lazy; 11 | use std::sync::RwLock; 12 | 13 | /// State of Tendermint blockchain networks 14 | pub static REGISTRY: Lazy = Lazy::new(GlobalRegistry::default); 15 | 16 | /// Registry of blockchain networks known to the KMS 17 | #[derive(Default)] 18 | pub struct Registry(Map); 19 | 20 | impl Registry { 21 | /// Add an account key to a keyring for a chain stored in the registry 22 | pub fn add_account_key( 23 | &mut self, 24 | chain_id: &Id, 25 | signer: keyring::ecdsa::Signer, 26 | ) -> Result<(), Error> { 27 | let chain = self.0.get_mut(chain_id).ok_or_else(|| { 28 | format_err!( 29 | InvalidKey, 30 | "can't add ECDSA signer {} to unregistered chain: {}", 31 | signer.provider(), 32 | chain_id 33 | ) 34 | })?; 35 | 36 | chain.keyring.add_ecdsa(signer) 37 | } 38 | 39 | /// Add a consensus key to a keyring for a chain stored in the registry 40 | pub fn add_consensus_key( 41 | &mut self, 42 | chain_id: &Id, 43 | signer: keyring::ed25519::Signer, 44 | ) -> Result<(), Error> { 45 | let chain = self.0.get_mut(chain_id).ok_or_else(|| { 46 | format_err!( 47 | InvalidKey, 48 | "can't add Ed25519 signer {} to unregistered chain: {}", 49 | signer.provider(), 50 | chain_id 51 | ) 52 | })?; 53 | 54 | chain.keyring.add_ed25519(signer) 55 | } 56 | 57 | /// Register a `Chain` with the registry 58 | pub fn register_chain(&mut self, chain: Chain) -> Result<(), Error> { 59 | let chain_id = chain.id.clone(); 60 | 61 | if self.0.insert(chain_id.clone(), chain).is_none() { 62 | Ok(()) 63 | } else { 64 | // TODO(tarcieri): handle updating the set of registered chains 65 | fail!(ConfigError, "chain ID already registered: {}", chain_id); 66 | } 67 | } 68 | 69 | /// Get information about a particular chain ID (if registered) 70 | pub fn get_chain(&self, chain_id: &Id) -> Option<&Chain> { 71 | self.0.get(chain_id) 72 | } 73 | } 74 | 75 | /// Global registry of blockchain networks known to the KMS 76 | // NOTE: The `RwLock` is a bit of futureproofing as this data structure is for the 77 | // most part "immutable". New chains should be registered at boot time. 78 | // The only case in which this structure may change is in the event of 79 | // runtime configuration reloading, so the `RwLock` is included as 80 | // futureproofing for such a feature. 81 | // 82 | // See: 83 | #[derive(Default)] 84 | pub struct GlobalRegistry(pub(super) RwLock); 85 | 86 | impl GlobalRegistry { 87 | /// Acquire a read-only (concurrent) lock to the internal chain registry 88 | pub fn get(&self) -> Guard<'_> { 89 | // TODO(tarcieri): better handle `PoisonError` here? 90 | self.0.read().unwrap().into() 91 | } 92 | 93 | /// Register a chain with the registry 94 | pub fn register(&self, chain: Chain) -> Result<(), Error> { 95 | // TODO(tarcieri): better handle `PoisonError` here? 96 | let mut registry = self.0.write().unwrap(); 97 | registry.register_chain(chain) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/chain/state/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types regarding chain state (i.e. double signing) 2 | 3 | use abscissa_core::error::{BoxError, Context}; 4 | use std::fmt::{self, Display}; 5 | use thiserror::Error; 6 | 7 | /// Error type 8 | #[derive(Debug)] 9 | pub struct StateError(pub(crate) Box>); 10 | 11 | /// Kinds of errors 12 | #[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] 13 | pub enum StateErrorKind { 14 | /// Height regressed 15 | #[error("height regression")] 16 | HeightRegression, 17 | 18 | /// Step regressed 19 | #[error("step regression")] 20 | StepRegression, 21 | 22 | /// Round regressed 23 | #[error("round regression")] 24 | RoundRegression, 25 | 26 | /// Double sign detected 27 | #[error("double sign detected")] 28 | DoubleSign, 29 | 30 | /// Error syncing state to disk 31 | #[error("error syncing state to disk")] 32 | SyncError, 33 | } 34 | 35 | impl StateErrorKind { 36 | /// Create an error context from this error 37 | pub fn context(self, source: impl Into) -> Context { 38 | Context::new(self, Some(source.into())) 39 | } 40 | } 41 | 42 | impl StateError { 43 | /// Get the kind of error 44 | pub fn kind(&self) -> StateErrorKind { 45 | *self.0.kind() 46 | } 47 | } 48 | 49 | impl From for StateError { 50 | fn from(kind: StateErrorKind) -> Self { 51 | Context::new(kind, None).into() 52 | } 53 | } 54 | 55 | impl From> for StateError { 56 | fn from(context: Context) -> Self { 57 | StateError(Box::new(context)) 58 | } 59 | } 60 | 61 | impl Display for StateError { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | self.0.fmt(f) 64 | } 65 | } 66 | 67 | impl std::error::Error for StateError { 68 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 69 | self.0.source() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/chain/state/hook.rs: -------------------------------------------------------------------------------- 1 | //! State hook support: obtain `ConsensusState` from an external source 2 | 3 | use crate::{ 4 | config::chain::HookConfig, 5 | error::{Error, ErrorKind::HookError}, 6 | prelude::*, 7 | }; 8 | use serde::Deserialize; 9 | use std::{process::Command, time::Duration}; 10 | use tendermint::block; 11 | use wait_timeout::ChildExt; 12 | 13 | /// Default timeout to use when a user one is unspecified 14 | const DEFAULT_TIMEOUT_SECS: u64 = 1; 15 | 16 | /// Sanity limit on how far the block height from the hook can diverge from the 17 | /// last known state 18 | pub const BLOCK_HEIGHT_SANITY_LIMIT: u64 = 9000; 19 | 20 | /// Run the given hook command to obtain the last signing state 21 | pub fn run(config: &HookConfig) -> Result { 22 | let mut child = Command::new(&config.cmd[0]) 23 | .args(&config.cmd[1..]) 24 | .spawn()?; 25 | let timeout = Duration::from_secs(config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS)); 26 | 27 | match child.wait_timeout(timeout)? { 28 | Some(status) => { 29 | if status.success() { 30 | if let Some(stdout) = child.stdout { 31 | Ok(serde_json::from_reader(stdout)?) 32 | } else { 33 | fail!(HookError, "couldn't consume stdout from child"); 34 | } 35 | } else { 36 | fail!(HookError, "subcommand returned status {:?}", status.code()) 37 | } 38 | } 39 | None => { 40 | // timeout 41 | child.kill()?; 42 | child.wait()?; 43 | fail!(HookError, "subcommand timed out after {:?}", timeout) 44 | } 45 | } 46 | } 47 | 48 | /// JSON output from the hook command (parsed with serde) 49 | #[derive(Debug, Deserialize)] 50 | pub struct Output { 51 | /// Latest block height 52 | pub latest_block_height: block::Height, 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use crate::config::chain::HookConfig; 58 | 59 | #[test] 60 | fn hook_test() { 61 | // TODO(tarcieri): write real tests for the hook subsystem 62 | let _ = super::run(&HookConfig { 63 | cmd: ["todo", "real", "example"] 64 | .iter() 65 | .map(|str| str.into()) 66 | .collect(), 67 | timeout_secs: Some(0), 68 | fail_closed: true, 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | //! The KMS makes outbound connections to the validator, and is technically a 2 | //! client, however once connected it accepts incoming RPCs, and otherwise 3 | //! acts as a service. 4 | //! 5 | //! To dance around the fact the KMS isn't actually a service, we refer to it 6 | //! as a "Key Management System". 7 | 8 | use crate::{ 9 | chain, 10 | config::ValidatorConfig, 11 | error::{Error, ErrorKind}, 12 | prelude::*, 13 | session::Session, 14 | }; 15 | use std::{panic, process::exit, thread, time::Duration}; 16 | 17 | /// Join handle type used by our clients 18 | type JoinHandle = thread::JoinHandle>; 19 | 20 | /// How long to wait after a crash before respawning (in seconds) 21 | pub const RESPAWN_DELAY: u64 = 1; 22 | 23 | /// Client connections: wraps a thread which makes a connection to a particular 24 | /// validator node and then receives RPCs. 25 | /// 26 | /// The `Client` type does not deal with network I/O, that is handled inside of 27 | /// the `Session`. Instead, the `Client` type manages threading and respawning 28 | /// sessions in the event of errors. 29 | pub struct Client { 30 | /// Name of the client thread 31 | name: String, 32 | 33 | /// Handle to the client thread 34 | handle: JoinHandle, 35 | } 36 | 37 | impl Client { 38 | /// Spawn a new client, returning a handle so it can be joined 39 | pub fn spawn(config: ValidatorConfig) -> Self { 40 | register_chain(&config.chain_id); 41 | 42 | let name = format!("{}@{}", &config.chain_id, &config.addr); 43 | 44 | let handle = thread::Builder::new() 45 | .name(name.clone()) 46 | .spawn(move || main_loop(config)) 47 | .unwrap_or_else(|e| { 48 | status_err!("error spawning thread: {}", e); 49 | exit(1); 50 | }); 51 | 52 | Self { name, handle } 53 | } 54 | 55 | /// Get the name of this client 56 | pub fn name(&self) -> &str { 57 | &self.name 58 | } 59 | 60 | /// Wait for a running client to finish 61 | pub fn join(self) -> Result<(), Error> { 62 | self.handle.join().unwrap() 63 | } 64 | } 65 | 66 | /// Main loop for all clients. Handles reconnecting in the event of an error 67 | fn main_loop(config: ValidatorConfig) -> Result<(), Error> { 68 | while let Err(e) = run_client(config.clone()) { 69 | // `PoisonError` is unrecoverable 70 | if *e.kind() == ErrorKind::PoisonError { 71 | error!("[{}@{}] FATAL -- {}", &config.chain_id, &config.addr, e); 72 | return Err(e); 73 | } else { 74 | error!("[{}@{}] {}", &config.chain_id, &config.addr, e); 75 | } 76 | 77 | if config.reconnect { 78 | // TODO: configurable respawn delay 79 | thread::sleep(Duration::from_secs(RESPAWN_DELAY)); 80 | } else { 81 | return Err(e); 82 | } 83 | } 84 | 85 | Ok(()) 86 | } 87 | 88 | /// Ensure chain with given ID is properly registered 89 | pub fn register_chain(chain_id: &chain::Id) { 90 | let registry = chain::REGISTRY.get(); 91 | 92 | debug!("registering chain: {}", chain_id); 93 | registry.get_chain(chain_id).unwrap_or_else(|| { 94 | status_err!( 95 | "unregistered chain: {} (add it to tmkms.toml's [[chain]] section)", 96 | chain_id 97 | ); 98 | exit(1); 99 | }); 100 | } 101 | 102 | /// Open a new session and run the session loop 103 | pub fn run_client(config: ValidatorConfig) -> Result<(), Error> { 104 | panic::catch_unwind(move || Session::open(config)?.request_loop()) 105 | .unwrap_or_else(|e| Err(Error::from_panic(e))) 106 | } 107 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | //! Subcommands of the `tmkms` command-line application 2 | 3 | pub mod init; 4 | #[cfg(feature = "ledger")] 5 | pub mod ledger; 6 | #[cfg(feature = "softsign")] 7 | pub mod softsign; 8 | pub mod start; 9 | pub mod version; 10 | #[cfg(feature = "yubihsm")] 11 | pub mod yubihsm; 12 | 13 | #[cfg(feature = "ledger")] 14 | pub use self::ledger::LedgerCommand; 15 | #[cfg(feature = "softsign")] 16 | pub use self::softsign::SoftsignCommand; 17 | #[cfg(feature = "yubihsm")] 18 | pub use self::yubihsm::YubihsmCommand; 19 | 20 | pub use self::{init::InitCommand, start::StartCommand, version::VersionCommand}; 21 | 22 | use crate::config::{KmsConfig, CONFIG_ENV_VAR, CONFIG_FILE_NAME}; 23 | use abscissa_core::{Command, Configurable, Runnable}; 24 | use clap::Parser; 25 | use std::{env, path::PathBuf}; 26 | 27 | /// Subcommands of the KMS command-line application 28 | #[derive(Command, Debug, Parser, Runnable)] 29 | pub enum KmsCommand { 30 | /// initialize KMS configuration 31 | Init(InitCommand), 32 | 33 | /// subcommands for Ledger 34 | #[cfg(feature = "ledger")] 35 | #[clap(subcommand)] 36 | Ledger(LedgerCommand), 37 | 38 | /// subcommands for software signer 39 | #[cfg(feature = "softsign")] 40 | #[clap(subcommand)] 41 | Softsign(SoftsignCommand), 42 | 43 | /// start the KMS application" 44 | Start(StartCommand), 45 | 46 | /// display the version 47 | Version(VersionCommand), 48 | 49 | /// subcommands for YubiHSM2 50 | #[cfg(feature = "yubihsm")] 51 | #[clap(subcommand)] 52 | Yubihsm(YubihsmCommand), 53 | } 54 | 55 | impl KmsCommand { 56 | /// Are we configured for verbose logging? 57 | pub fn verbose(&self) -> bool { 58 | match self { 59 | KmsCommand::Start(run) => run.verbose, 60 | #[cfg(feature = "yubihsm")] 61 | KmsCommand::Yubihsm(yubihsm) => yubihsm.verbose(), 62 | _ => false, 63 | } 64 | } 65 | } 66 | 67 | impl Configurable for KmsCommand { 68 | /// Get the path to the configuration file, either from selected subcommand 69 | /// or the default 70 | fn config_path(&self) -> Option { 71 | let config = match self { 72 | KmsCommand::Start(start) => start.config.as_ref(), 73 | #[cfg(feature = "yubihsm")] 74 | KmsCommand::Yubihsm(yubihsm) => yubihsm.config_path(), 75 | #[cfg(feature = "ledger")] 76 | KmsCommand::Ledger(ledger) => ledger.config_path(), 77 | _ => return None, 78 | }; 79 | 80 | let path = config 81 | .cloned() 82 | .or_else(|| env::var(CONFIG_ENV_VAR).ok().map(PathBuf::from)) 83 | .unwrap_or_else(|| PathBuf::from(CONFIG_FILE_NAME)); 84 | 85 | Some(path) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/commands/init.rs: -------------------------------------------------------------------------------- 1 | //! `init` subcommand 2 | 3 | pub mod config_builder; 4 | pub mod networks; 5 | 6 | use self::{config_builder::ConfigBuilder, networks::Network}; 7 | use crate::{config::CONFIG_FILE_NAME, key_utils, prelude::*}; 8 | use abscissa_core::Command; 9 | use clap::Parser; 10 | use std::{ 11 | fs, 12 | os::unix::fs::PermissionsExt, 13 | path::{Path, PathBuf}, 14 | process, 15 | }; 16 | 17 | /// Subdirectories to create within the parent directory 18 | pub const SUBDIRECTORIES: &[&str] = &["schema", "secrets", "state"]; 19 | 20 | /// Filesystem permissions to set on the secrets directory 21 | pub const SECRETS_DIR_PERMISSIONS: u32 = 0o700; 22 | 23 | /// Default name of the Secret Connection key 24 | pub const SECRET_CONNECTION_KEY: &str = "kms-identity.key"; 25 | 26 | /// Abort the operation, printing a formatted message and exiting the process 27 | /// with a status code of 1 (i.e. error) 28 | macro_rules! abort { 29 | ($fmt:expr, $($arg:tt)+) => { 30 | status_err!(format!($fmt, $($arg)+)); 31 | process::exit(1); 32 | }; 33 | } 34 | 35 | /// `init` subcommand 36 | #[derive(Command, Debug, Parser)] 37 | pub struct InitCommand { 38 | /// Tendermint networks to configure (comma separated) 39 | #[clap(short = 'n', long = "networks")] 40 | networks: Option, 41 | 42 | /// path where config files should be generated 43 | output_paths: Vec, 44 | } 45 | 46 | impl Runnable for InitCommand { 47 | fn run(&self) { 48 | if self.output_paths.len() != 1 { 49 | eprintln!("Usage: tmkms init [-f] KMS_HOME_PATH"); 50 | process::exit(1); 51 | } 52 | 53 | // Parse specified networks to initialize 54 | let mut networks = vec![]; 55 | match &self.networks { 56 | Some(chain_ids) => { 57 | for chain_id in chain_ids.split(',') { 58 | networks.push(Network::parse(chain_id)); 59 | } 60 | } 61 | None => { 62 | networks.push(Network::CosmosHub); 63 | } 64 | } 65 | 66 | let kms_home = { 67 | let output_path = &self.output_paths[0]; 68 | 69 | // Create KMS home directory 70 | if !output_path.exists() { 71 | status_ok!("Creating", "{}", output_path.display()); 72 | 73 | fs::create_dir_all(output_path).unwrap_or_else(|e| { 74 | abort!("couldn't create `{}`: {}", output_path.display(), e); 75 | }); 76 | } 77 | 78 | fs::canonicalize(output_path).unwrap_or_else(|e| { 79 | abort!("couldn't canonicalize `{}`: {}", output_path.display(), e); 80 | }) 81 | }; 82 | 83 | // Create subdirectories within the KMS home directory 84 | for subdir in SUBDIRECTORIES { 85 | let subdir_path = kms_home.join(subdir); 86 | 87 | fs::create_dir_all(&subdir_path).unwrap_or_else(|e| { 88 | abort!("couldn't create `{}`: {}", subdir_path.display(), e); 89 | }); 90 | } 91 | 92 | // Restrict filesystem permissions to the `secrets` subdirectory 93 | let secrets_dir = kms_home.join("secrets"); 94 | 95 | set_permissions(&secrets_dir, SECRETS_DIR_PERMISSIONS); 96 | 97 | let config_path = kms_home.join(CONFIG_FILE_NAME); 98 | let config_toml = ConfigBuilder::new(&kms_home, &networks).generate(); 99 | 100 | fs::write(&config_path, config_toml).unwrap_or_else(|e| { 101 | abort!("couldn't write `{}`: {}", config_path.display(), e); 102 | }); 103 | 104 | status_ok!("Generated", "KMS configuration: {}", config_path.display()); 105 | 106 | let secret_connection_key = secrets_dir.join(SECRET_CONNECTION_KEY); 107 | key_utils::generate_key(&secret_connection_key).unwrap_or_else(|e| { 108 | abort!( 109 | "couldn't generate `{}`: {}", 110 | secret_connection_key.display(), 111 | e 112 | ); 113 | }); 114 | 115 | status_ok!( 116 | "Generated", 117 | "Secret Connection key: {}", 118 | secret_connection_key.display() 119 | ); 120 | 121 | // TODO(tarcieri): generate consensus and account keys when using softsign 122 | } 123 | } 124 | 125 | /// Set Unix permissions on the given path. 126 | /// 127 | /// On error, prints a message and exits the process with status 1 (error) 128 | fn set_permissions(path: impl AsRef, mode: u32) { 129 | fs::set_permissions(path.as_ref(), fs::Permissions::from_mode(mode)).unwrap_or_else(|e| { 130 | abort!( 131 | "couldn't set permissions on `{}`: {}", 132 | path.as_ref().display(), 133 | e 134 | ); 135 | }); 136 | } 137 | -------------------------------------------------------------------------------- /src/commands/init/config_builder.rs: -------------------------------------------------------------------------------- 1 | //! Configuration file builder 2 | 3 | use super::networks::Network; 4 | use std::{ 5 | fmt::{self, Display}, 6 | path::Path, 7 | }; 8 | 9 | /// Header to place at the top of `tmkms.toml` 10 | pub const KMS_CONFIG_HEADER: &str = "# Tendermint KMS configuration file"; 11 | 12 | /// Configuration file builder 13 | pub struct ConfigBuilder { 14 | /// Path to the KMS home directory (as a string) 15 | kms_home: String, 16 | 17 | /// Networks to include in configuration 18 | networks: Vec, 19 | 20 | /// Contents of the configuration file in-progress 21 | contents: String, 22 | } 23 | 24 | impl ConfigBuilder { 25 | /// Create config builder in the default state 26 | pub fn new(kms_home: impl AsRef, networks: &[Network]) -> Self { 27 | let mut result = Self { 28 | // We need to template the KMS homedir into a config file so we have 29 | // to convert it into a string 30 | kms_home: kms_home.as_ref().display().to_string(), 31 | networks: networks.to_vec(), 32 | contents: String::new(), 33 | }; 34 | 35 | result.add_str(KMS_CONFIG_HEADER); 36 | result.add_str("\n\n"); 37 | 38 | result 39 | } 40 | 41 | /// Generate configuration, returning a serialized TOML string 42 | pub fn generate(mut self) -> String { 43 | self.add_chain_config(); 44 | self.add_provider_config(); 45 | self.add_validator_config(); 46 | 47 | self.contents 48 | } 49 | 50 | /// Add a comment describing a particular section 51 | fn add_section_comment(&mut self, section: &str) { 52 | self.add_str(format!("## {section}\n\n")); 53 | } 54 | 55 | /// Add `[[chain]]` configurations 56 | fn add_chain_config(&mut self) { 57 | self.add_section_comment("Chain Configuration"); 58 | 59 | for network in &self.networks.clone() { 60 | self.add_template(match network { 61 | Network::Columbus => include_str!("templates/networks/columbus.toml"), 62 | Network::CosmosHub => include_str!("templates/networks/cosmoshub.toml"), 63 | Network::IrisHub => include_str!("templates/networks/irishub.toml"), 64 | Network::SentinelHub => include_str!("templates/networks/sentinelhub.toml"), 65 | Network::Osmosis => include_str!("templates/networks/osmosis.toml"), 66 | Network::Persistence => include_str!("templates/networks/persistence.toml"), 67 | }); 68 | } 69 | } 70 | 71 | /// Add `[[provider]]` configuration (customized for enabled signing providers) 72 | fn add_provider_config(&mut self) { 73 | self.add_section_comment("Signing Provider Configuration"); 74 | 75 | #[cfg(feature = "yubihsm")] 76 | self.add_yubihsm_provider_config(); 77 | 78 | #[cfg(feature = "ledger")] 79 | self.add_ledgertm_provider_config(); 80 | 81 | #[cfg(feature = "softsign")] 82 | self.add_softsign_provider_config(); 83 | 84 | #[cfg(feature = "fortanixdsm")] 85 | self.add_fortanixdsm_provider_config(); 86 | } 87 | 88 | /// Add `[[validator]]` configurations 89 | fn add_validator_config(&mut self) { 90 | self.add_section_comment("Validator Configuration"); 91 | self.add_template_with_chain_id(include_str!("templates/validator.toml")); 92 | } 93 | 94 | /// Add `[[provider.yubihsm]]` configuration 95 | #[cfg(feature = "yubihsm")] 96 | fn add_yubihsm_provider_config(&mut self) { 97 | self.add_str("### YubiHSM2 Provider Configuration\n\n"); 98 | 99 | self.add_str(format_template( 100 | include_str!("templates/keyring/yubihsm.toml"), 101 | &[("$KMS_HOME", self.kms_home.as_ref())], 102 | )); 103 | 104 | self.add_str("\nkeys = [\n"); 105 | 106 | let mut key_id = 1; 107 | let key_types = ["consensus"]; 108 | 109 | for network in self.networks.clone() { 110 | for key_type in key_types { 111 | self.add_str(format!( 112 | " {{ key = {}, type = \"{}\", chain_ids = [\"{}\"] }}, \n", 113 | key_id, 114 | key_type, 115 | network.chain_id(), 116 | )); 117 | 118 | key_id += 1; 119 | } 120 | } 121 | 122 | self.add_str("]\n"); 123 | 124 | #[cfg(feature = "yubihsm-server")] 125 | self.add_str(include_str!("templates/keyring/yubihsm_server.toml")); 126 | 127 | self.add_str("\n"); 128 | } 129 | 130 | /// Add `[[provdier.ledgertm]]` configuration 131 | #[cfg(feature = "ledger")] 132 | fn add_ledgertm_provider_config(&mut self) { 133 | self.add_str("### Ledger Provider Configuration\n\n"); 134 | self.add_template_with_chain_id(include_str!("templates/keyring/ledgertm.toml")); 135 | } 136 | 137 | /// Add `[[provider.softsign]]` configuration 138 | #[cfg(feature = "softsign")] 139 | fn add_softsign_provider_config(&mut self) { 140 | self.add_str("### Software-based Signer Configuration\n\n"); 141 | self.add_template_with_chain_id(include_str!("templates/keyring/softsign_consensus.toml")); 142 | } 143 | 144 | /// Add `[[provider.fortanixdsm]]` configuration 145 | #[cfg(feature = "fortanixdsm")] 146 | fn add_fortanixdsm_provider_config(&mut self) { 147 | self.add_str("### Fortanix DSM Signer Configuration\n\n"); 148 | self.add_template_with_chain_id(include_str!("templates/keyring/fortanixdsm.toml")); 149 | } 150 | 151 | /// Append a template to the config file, substituting `$KMS_HOME` 152 | fn add_template(&mut self, template: &str) { 153 | self.add_str(format_template( 154 | template, 155 | &[("$KMS_HOME", self.kms_home.as_ref())], 156 | )); 157 | 158 | self.add_str("\n\n"); 159 | } 160 | 161 | /// Append a template to the config file, substituting `$KMS_HOME` and `$CHAIN_ID` 162 | fn add_template_with_chain_id(&mut self, template: &str) { 163 | for network in self.networks.clone() { 164 | self.add_str(format_template( 165 | template, 166 | &[ 167 | ("$KMS_HOME", self.kms_home.as_ref()), 168 | ("$CHAIN_ID", network.chain_id()), 169 | ], 170 | )); 171 | 172 | self.add_str("\n\n"); 173 | } 174 | } 175 | 176 | /// Add a string to `self.contents` 177 | fn add_str(&mut self, str: impl AsRef) { 178 | self.contents.push_str(str.as_ref()) 179 | } 180 | } 181 | 182 | impl Display for ConfigBuilder { 183 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 184 | f.write_str(&self.contents) 185 | } 186 | } 187 | 188 | /// Apply the given set of substitutions and trim newlines 189 | fn format_template(template: &str, substitutions: &[(&str, &str)]) -> String { 190 | substitutions.iter().fold( 191 | template.trim_end().to_owned(), 192 | |string, (name, replacement)| string.replace(name, replacement), 193 | ) 194 | } 195 | -------------------------------------------------------------------------------- /src/commands/init/networks.rs: -------------------------------------------------------------------------------- 1 | //! Tendermint KMS configuration file networks 2 | 3 | use crate::prelude::*; 4 | use std::{ 5 | fmt::{self, Display}, 6 | process, 7 | }; 8 | 9 | /// Tendermint networks we have config networks for 10 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 11 | pub enum Network { 12 | /// Terra `columbus` chain 13 | Columbus, 14 | 15 | /// Cosmos `cosmoshub` chain 16 | CosmosHub, 17 | 18 | /// Iris `irishub` chain 19 | IrisHub, 20 | 21 | /// Sentinel `sentinelhub` chain 22 | SentinelHub, 23 | 24 | /// Osmosis `osmosis` chain 25 | Osmosis, 26 | 27 | /// Persistence `core` chain 28 | Persistence, 29 | } 30 | 31 | impl Display for Network { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | f.write_str(match self { 34 | Network::Columbus => "columbus", 35 | Network::CosmosHub => "cosmoshub", 36 | Network::IrisHub => "irishub", 37 | Network::SentinelHub => "sentinelhub", 38 | Network::Osmosis => "osmosis", 39 | Network::Persistence => "core", 40 | }) 41 | } 42 | } 43 | 44 | impl Network { 45 | /// Get a slice containing all known networks 46 | pub fn all() -> &'static [Network] { 47 | &[ 48 | Network::Columbus, 49 | Network::CosmosHub, 50 | Network::IrisHub, 51 | Network::SentinelHub, 52 | Network::Osmosis, 53 | Network::Persistence, 54 | ] 55 | } 56 | 57 | /// Parse a network name from the chain ID prefix 58 | pub fn parse(s: &str) -> Self { 59 | match s { 60 | "columbus" => Network::Columbus, 61 | "cosmoshub" => Network::CosmosHub, 62 | "irishub" => Network::IrisHub, 63 | "sentinelhub" => Network::SentinelHub, 64 | "osmosis" => Network::Osmosis, 65 | "core" => Network::Persistence, 66 | other => { 67 | status_err!("unknown Tendermint network: `{}`", other); 68 | eprintln!("\nRegistered networks:"); 69 | 70 | for network in Self::all() { 71 | eprintln!("- {network}"); 72 | } 73 | 74 | process::exit(1); 75 | } 76 | } 77 | } 78 | 79 | /// Get the current production chain ID for this network 80 | pub fn chain_id(&self) -> &str { 81 | match self { 82 | Network::Columbus => "columbus-3", 83 | Network::CosmosHub => "cosmoshub-3", 84 | Network::IrisHub => "irishub", 85 | Network::SentinelHub => "sentinelhub-2", 86 | Network::Osmosis => "osmosis-1", 87 | Network::Persistence => "core-1", 88 | } 89 | } 90 | 91 | /// Get the schema file for this network 92 | pub fn schema_file(&self) -> &str { 93 | match self { 94 | Network::Columbus => "terra.toml", 95 | Network::CosmosHub => "cosmos-sdk.toml", 96 | Network::IrisHub => "iris.toml", 97 | Network::SentinelHub => "sentinelhub.toml", 98 | Network::Osmosis => "osmosis.toml", 99 | Network::Persistence => "persistence.toml", 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/commands/init/templates/keyring/fortanixdsm.toml: -------------------------------------------------------------------------------- 1 | [[providers.fortanixdsm]] 2 | api_endpoint = "https://sdkms.fortanix.com" 3 | api_key = "Nzk5MDQ3ZGUtN2Q2NS00OTRjLTgzMDMtNjQwMTlhYzdmOGUzOlF1SU93ZXJsOFU4VUdEWEdQMmx1dFJOVjlvMTRSd3lhNnVDNVNhVkpZOVhzYVgyc0pOVGRQVGJ0RjZJdmVLMy00X05iTEhxMkowamF3UGVPaXJEWEd3" 4 | signing_keys = [ 5 | { chain_ids = ["$CHAIN_ID"], type = "account", key_id = "72e9ed9e-9eb4-46bd-a135-e78ed9bfd611" }, 6 | { chain_ids = ["$CHAIN_ID"], type = "consensus", key_name = "My Key" }, 7 | ] 8 | -------------------------------------------------------------------------------- /src/commands/init/templates/keyring/ledgertm.toml: -------------------------------------------------------------------------------- 1 | [[providers.ledgertm]] 2 | chain_ids = ["cosmoshub-1"] 3 | -------------------------------------------------------------------------------- /src/commands/init/templates/keyring/softsign_account.toml: -------------------------------------------------------------------------------- 1 | [[providers.softsign]] 2 | chain_ids = ["$CHAIN_ID"] 3 | key_type = "account" 4 | path = "$KMS_HOME/secrets/$CHAIN_ID-account.key" 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/keyring/softsign_consensus.toml: -------------------------------------------------------------------------------- 1 | [[providers.softsign]] 2 | chain_ids = ["$CHAIN_ID"] 3 | key_type = "consensus" 4 | path = "$KMS_HOME/secrets/$CHAIN_ID-consensus.key" 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/keyring/yubihsm.toml: -------------------------------------------------------------------------------- 1 | [[providers.yubihsm]] 2 | adapter = { type = "usb" } 3 | auth = { key = 1, password_file = "$KMS_HOME/secrets/yubihsm-password.txt" } 4 | #serial_number = "0123456789" # serial number of a specific YubiHSM to connect to (optional) 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/keyring/yubihsm_server.toml: -------------------------------------------------------------------------------- 1 | #connector_server = { laddr = "tcp://127.0.0.1:12345", cli = { auth_key = 2 } } 2 | -------------------------------------------------------------------------------- /src/commands/init/templates/networks/columbus.toml: -------------------------------------------------------------------------------- 1 | ### Terra Columbus Network 2 | 3 | [[chain]] 4 | id = "columbus-3" 5 | key_format = { type = "bech32", account_key_prefix = "terra", consensus_key_prefix = "terravalconspub" } 6 | state_file = "$KMS_HOME/state/columbus-3-consensus.json" 7 | -------------------------------------------------------------------------------- /src/commands/init/templates/networks/cosmoshub.toml: -------------------------------------------------------------------------------- 1 | ### Cosmos Hub Network 2 | 3 | [[chain]] 4 | id = "cosmoshub-3" 5 | key_format = { type = "bech32", account_key_prefix = "cosmospub", consensus_key_prefix = "cosmosvalconspub" } 6 | state_file = "$KMS_HOME/state/cosmoshub-3-consensus.json" 7 | -------------------------------------------------------------------------------- /src/commands/init/templates/networks/irishub.toml: -------------------------------------------------------------------------------- 1 | ### Iris Hub Network 2 | 3 | [[chain]] 4 | id = "irishub" 5 | key_format = { type = "bech32", account_key_prefix = "iap", consensus_key_prefix = "icp" } 6 | state_file = "$KMS_HOME/state/irishub-consensus.json" 7 | -------------------------------------------------------------------------------- /src/commands/init/templates/networks/osmosis.toml: -------------------------------------------------------------------------------- 1 | ### Osmosis Network 2 | 3 | [[chain]] 4 | id = "osmosis-1" 5 | key_format = { type = "bech32", account_key_prefix = "osmopub", consensus_key_prefix = "osmovalconspub" } 6 | state_file = "$KMS_HOME/state/osmosis-1-consensus.json" -------------------------------------------------------------------------------- /src/commands/init/templates/networks/persistence.toml: -------------------------------------------------------------------------------- 1 | ### Persistence Network 2 | 3 | [[chain]] 4 | id = "core-1" 5 | key_format = { type = "bech32", account_key_prefix = "persistencepub", consensus_key_prefix = "persistencevalconspub" } 6 | state_file = "$KMS_HOME/state/core-1-consensus.json" -------------------------------------------------------------------------------- /src/commands/init/templates/networks/sentinelhub.toml: -------------------------------------------------------------------------------- 1 | ### Sentinel Network 2 | 3 | [[chain]] 4 | id = "sentinelhub-2" 5 | key_format = { type = "bech32", account_key_prefix = "sentpub", consensus_key_prefix = "sentvalconspub" } 6 | state_file = "$KMS_HOME/state/sentinelhub-2-consensus.json" 7 | -------------------------------------------------------------------------------- /src/commands/init/templates/schema/cosmos-sdk.toml: -------------------------------------------------------------------------------- 1 | # TODO 2 | # 3 | # If you're interested in this, please open an issue at: 4 | # 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/schema/iris.toml: -------------------------------------------------------------------------------- 1 | # TODO 2 | # 3 | # If you're interested in this, please open an issue at: 4 | # 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/schema/osmosis.toml: -------------------------------------------------------------------------------- 1 | # TODO 2 | # 3 | # If you're interested in this, please open an issue at: 4 | # 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/schema/persistence.toml: -------------------------------------------------------------------------------- 1 | # TODO 2 | # 3 | # If you're interested in this, please open an issue at: 4 | # 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/schema/sentinelhub.toml: -------------------------------------------------------------------------------- 1 | # TODO 2 | # 3 | # If you're interested in this, please open an issue at: 4 | # 5 | -------------------------------------------------------------------------------- /src/commands/init/templates/schema/terra.toml: -------------------------------------------------------------------------------- 1 | # Terra stablecoin project schema 2 | # 3 | 4 | namespace = "core/StdTx" 5 | acc_prefix = "terra" 6 | val_prefix = "terravaloper" 7 | 8 | # 9 | # Oracle vote transactions 10 | # 11 | # 12 | 13 | # MsgExchangeRatePrevote 14 | # 15 | [[definition]] 16 | type_name = "oracle/MsgExchangeRatePrevote" 17 | fields = [ 18 | { name = "hash", type = "string" }, 19 | { name = "denom", type = "string" }, 20 | { name = "feeder", type = "sdk.AccAddress" }, 21 | { name = "validator", type = "sdk.ValAddress" }, 22 | ] 23 | 24 | # MsgExchangeRateVote 25 | # 26 | [[definition]] 27 | type_name = "oracle/MsgExchangeRateVote" 28 | fields = [ 29 | { name = "exchange_rate", type = "sdk.Dec"}, 30 | { name = "salt", type = "string" }, 31 | { name = "denom", type = "string" }, 32 | { name = "feeder", type = "sdk.AccAddress" }, 33 | { name = "validator", type = "sdk.ValAddress" }, 34 | ] 35 | -------------------------------------------------------------------------------- /src/commands/init/templates/validator.toml: -------------------------------------------------------------------------------- 1 | [[validator]] 2 | chain_id = "$CHAIN_ID" 3 | addr = "tcp://deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@example1.example.com:26658" 4 | secret_key = "$KMS_HOME/secrets/kms-identity.key" 5 | protocol_version = "v0.34" 6 | reconnect = true 7 | -------------------------------------------------------------------------------- /src/commands/ledger.rs: -------------------------------------------------------------------------------- 1 | //! `tmkms ledger` CLI (sub)commands 2 | 3 | use crate::{ 4 | chain, 5 | prelude::*, 6 | privval::{SignableMsg, SignedMsgType}, 7 | }; 8 | use abscissa_core::{Command, Runnable}; 9 | use clap::{Parser, Subcommand}; 10 | use std::{path::PathBuf, process}; 11 | use tendermint::Vote; 12 | use tendermint_proto as proto; 13 | 14 | /// `ledger` subcommand 15 | #[derive(Command, Debug, Runnable, Subcommand)] 16 | pub enum LedgerCommand { 17 | /// initialise the height/round/step 18 | Init(InitCommand), 19 | } 20 | 21 | impl LedgerCommand { 22 | pub(super) fn config_path(&self) -> Option<&PathBuf> { 23 | match self { 24 | LedgerCommand::Init(init) => init.config.as_ref(), 25 | } 26 | } 27 | } 28 | 29 | /// `ledger init` subcommand 30 | #[derive(Command, Debug, Parser)] 31 | pub struct InitCommand { 32 | /// config file path 33 | #[clap(short = 'c', long = "config")] 34 | pub config: Option, 35 | 36 | /// block height 37 | #[clap(short = 'H', long = "height")] 38 | pub height: Option, 39 | 40 | /// block round 41 | #[clap(short = 'r', long = "round")] 42 | pub round: Option, 43 | } 44 | 45 | impl Runnable for InitCommand { 46 | fn run(&self) { 47 | let config = APP.config(); 48 | 49 | chain::load_config(&config).unwrap_or_else(|e| { 50 | status_err!("error loading configuration: {}", e); 51 | process::exit(1); 52 | }); 53 | 54 | let chain_id = config.validator[0].chain_id.clone(); 55 | let registry = chain::REGISTRY.get(); 56 | let chain = registry.get_chain(&chain_id).unwrap(); 57 | 58 | let vote = proto::types::Vote { 59 | height: self.height.unwrap(), 60 | round: self.round.unwrap() as i32, 61 | r#type: SignedMsgType::Proposal.into(), 62 | ..Default::default() 63 | }; 64 | println!("{vote:?}"); 65 | let sign_vote_req = SignableMsg::from(Vote::try_from(vote).unwrap()); 66 | let to_sign = sign_vote_req 67 | .canonical_bytes(config.validator[0].chain_id.clone()) 68 | .unwrap(); 69 | 70 | let _sig = chain.keyring.sign(None, &to_sign).unwrap(); 71 | 72 | println!( 73 | "Successfully called the init command with height {}, and round {}", 74 | self.height.unwrap(), 75 | self.round.unwrap() 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/softsign.rs: -------------------------------------------------------------------------------- 1 | //! `tmkms softsign` CLI (sub)commands 2 | 3 | mod import; 4 | mod keygen; 5 | 6 | use self::{import::ImportCommand, keygen::KeygenCommand}; 7 | use abscissa_core::{Command, Runnable}; 8 | use clap::Subcommand; 9 | 10 | /// The `softsign` subcommand 11 | #[derive(Command, Debug, Runnable, Subcommand)] 12 | pub enum SoftsignCommand { 13 | /// generate a software signing key 14 | Keygen(KeygenCommand), 15 | 16 | /// convert existing private key to base64 format 17 | Import(ImportCommand), 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/softsign/import.rs: -------------------------------------------------------------------------------- 1 | //! `tmkms softsign import` command 2 | 3 | use crate::{config::provider::softsign::KeyFormat, key_utils, prelude::*}; 4 | use abscissa_core::Command; 5 | use clap::Parser; 6 | use std::{path::PathBuf, process}; 7 | use tendermint::PrivateKey; 8 | use tendermint_config::PrivValidatorKey; 9 | 10 | /// `import` command: import a `priv_validator.json` formatted key and convert 11 | /// it into the raw format used by the softsign backend (by default) 12 | #[derive(Command, Debug, Default, Parser)] 13 | pub struct ImportCommand { 14 | /// key format to import: 'json' or 'raw' (default 'json') 15 | #[clap(short = 'f')] 16 | format: Option, 17 | 18 | /// [INPUT] and [OUTPUT] paths for key generation 19 | paths: Vec, 20 | } 21 | 22 | impl Runnable for ImportCommand { 23 | /// Import a `priv_validator.json` 24 | fn run(&self) { 25 | if self.paths.len() != 2 { 26 | status_err!("expected 2 arguments, got {}", self.paths.len()); 27 | eprintln!("\nUsage: tmkms softsign import [priv_validator.json] [output.key]"); 28 | process::exit(1); 29 | } 30 | 31 | let input_path = &self.paths[0]; 32 | let output_path = &self.paths[1]; 33 | 34 | let format = self 35 | .format 36 | .as_ref() 37 | .map(|f| { 38 | f.parse::().unwrap_or_else(|e| { 39 | status_err!("{} (must be 'json' or 'raw')", e); 40 | process::exit(1); 41 | }) 42 | }) 43 | .unwrap_or(KeyFormat::Json); 44 | 45 | if format != KeyFormat::Json { 46 | status_err!("invalid format: {:?} (must be 'json')", format); 47 | process::exit(1); 48 | } 49 | 50 | let secret_key = PrivValidatorKey::load_json_file(input_path) 51 | .unwrap_or_else(|e| { 52 | status_err!("couldn't load {}: {}", input_path.display(), e); 53 | process::exit(1); 54 | }) 55 | .priv_key; 56 | 57 | match secret_key { 58 | PrivateKey::Ed25519(sk) => { 59 | key_utils::write_base64_secret(output_path, sk.as_bytes()).unwrap_or_else(|e| { 60 | status_err!("{}", e); 61 | process::exit(1); 62 | }); 63 | info!("Imported Ed25519 private key to {}", output_path.display()); 64 | } 65 | PrivateKey::Secp256k1(sk) => { 66 | key_utils::write_base64_secret(output_path, &sk.to_bytes()).unwrap_or_else(|e| { 67 | status_err!("{}", e); 68 | process::exit(1); 69 | }); 70 | info!( 71 | "Imported Secp256k1 private key to {}", 72 | output_path.display() 73 | ); 74 | } 75 | _ => unreachable!("unsupported priv_validator.json algorithm"), 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/softsign/keygen.rs: -------------------------------------------------------------------------------- 1 | //! `tmkms softsign keygen` subcommand 2 | 3 | use crate::{key_utils, keyring::ed25519, prelude::*}; 4 | use abscissa_core::Command; 5 | use clap::Parser; 6 | use k256::ecdsa; 7 | use rand_core::{OsRng, RngCore}; 8 | use std::{path::Path, path::PathBuf, process}; 9 | 10 | /// Default type of key to generate 11 | pub const DEFAULT_KEY_TYPE: &str = "consensus"; 12 | 13 | /// `keygen` command 14 | #[derive(Command, Debug, Default, Parser)] 15 | pub struct KeygenCommand { 16 | /// type of key: 'account' or 'consensus' (default 'consensus') 17 | #[clap(short = 't', long = "type")] 18 | key_type: Option, 19 | 20 | /// path where generated key should be created 21 | output_paths: Vec, 22 | } 23 | 24 | impl Runnable for KeygenCommand { 25 | /// Generate an Ed25519 secret key for use with a software provider (i.e. ed25519-dalek) 26 | fn run(&self) { 27 | if self.output_paths.len() != 1 { 28 | eprintln!("Usage: tmkms softsign keygen [-t account,consensus] PATH"); 29 | process::exit(1); 30 | } 31 | 32 | let output_path = &self.output_paths[0]; 33 | 34 | match self 35 | .key_type 36 | .as_ref() 37 | .map(AsRef::as_ref) 38 | .unwrap_or(DEFAULT_KEY_TYPE) 39 | { 40 | "account" => generate_secp256k1_key(output_path), 41 | "consensus" => generate_ed25519_key(output_path), 42 | other => { 43 | status_err!( 44 | "unknown key type: {} (must be 'account' or 'consensus')", 45 | other 46 | ); 47 | process::exit(1); 48 | } 49 | } 50 | } 51 | } 52 | 53 | /// Randomly generate a Base64-encoded secp256k1 key and store it at the given path 54 | fn generate_secp256k1_key(output_path: &Path) { 55 | let signing_key = ecdsa::SigningKey::random(&mut OsRng); 56 | 57 | key_utils::write_base64_secret(output_path, &signing_key.to_bytes()).unwrap_or_else(|e| { 58 | status_err!("{}", e); 59 | process::exit(1); 60 | }); 61 | 62 | status_ok!( 63 | "Generated", 64 | "account (secp256k1) private key at: {}", 65 | output_path.display() 66 | ); 67 | } 68 | 69 | /// Randomly generate a Base64-encoded Ed25519 key and store it at the given path 70 | fn generate_ed25519_key(output_path: &Path) { 71 | let mut sk_bytes = [0u8; 32]; 72 | OsRng.fill_bytes(&mut sk_bytes); 73 | let sk = ed25519::SigningKey::from(sk_bytes); 74 | 75 | key_utils::write_base64_secret(output_path, sk.as_bytes()).unwrap_or_else(|e| { 76 | status_err!("{}", e); 77 | process::exit(1); 78 | }); 79 | 80 | status_ok!( 81 | "Generated", 82 | "consensus (Ed25519) private key at: {}", 83 | output_path.display() 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/commands/start.rs: -------------------------------------------------------------------------------- 1 | //! Start the KMS 2 | 3 | use crate::{chain, client::Client, prelude::*}; 4 | use abscissa_core::Command; 5 | use clap::Parser; 6 | use std::{path::PathBuf, process}; 7 | 8 | /// The `start` command 9 | #[derive(Command, Debug, Default, Parser)] 10 | pub struct StartCommand { 11 | /// path to tmkms.toml 12 | #[clap(short = 'c', long = "config")] 13 | pub config: Option, 14 | 15 | /// enable verbose debug logging 16 | #[clap(short = 'v', long = "verbose")] 17 | pub verbose: bool, 18 | } 19 | 20 | impl Runnable for StartCommand { 21 | /// Run the KMS 22 | fn run(&self) { 23 | info!( 24 | "{} {} starting up...", 25 | env!("CARGO_PKG_NAME"), 26 | env!("CARGO_PKG_VERSION") 27 | ); 28 | 29 | run_app(self.spawn_clients()); 30 | } 31 | } 32 | 33 | impl StartCommand { 34 | /// Spawn clients from the app's configuration 35 | fn spawn_clients(&self) -> Vec { 36 | let config = APP.config(); 37 | 38 | chain::load_config(&config).unwrap_or_else(|e| { 39 | status_err!("error loading configuration: {}", e); 40 | process::exit(1); 41 | }); 42 | 43 | // Spawn the validator client threads 44 | config 45 | .validator 46 | .iter() 47 | .cloned() 48 | .map(Client::spawn) 49 | .collect() 50 | } 51 | } 52 | 53 | /// Run the application. 54 | fn run_app(validator_clients: Vec) { 55 | blocking_wait(validator_clients); 56 | } 57 | 58 | /// Wait for clients to shut down using synchronous thread joins 59 | fn blocking_wait(validator_clients: Vec) { 60 | // Wait for all of the validator client threads to exit 61 | debug!("Main thread waiting on clients..."); 62 | 63 | let mut success = true; 64 | 65 | for client in validator_clients { 66 | let name = client.name().to_owned(); 67 | 68 | if let Err(e) = client.join() { 69 | status_err!("client '{}' exited with error: {}", name, e); 70 | success = false; 71 | } 72 | } 73 | 74 | if success { 75 | info!("Shutdown completed successfully"); 76 | } else { 77 | warn!("Shutdown completed with errors"); 78 | process::exit(1); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/version.rs: -------------------------------------------------------------------------------- 1 | //! Provide the version 2 | 3 | use crate::prelude::*; 4 | use abscissa_core::Command; 5 | use clap::Parser; 6 | use std::{option_env, process}; 7 | 8 | /// The `version` command 9 | #[derive(Command, Debug, Default, Parser)] 10 | pub struct VersionCommand {} 11 | 12 | impl Runnable for VersionCommand { 13 | /// Run the KMS 14 | fn run(&self) { 15 | println!("{}", option_env!("CARGO_PKG_VERSION").unwrap_or("unknown")); 16 | process::exit(0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/yubihsm.rs: -------------------------------------------------------------------------------- 1 | //! `tmkms yubihsm` CLI (sub)commands 2 | 3 | mod detect; 4 | mod keys; 5 | mod setup; 6 | mod test; 7 | 8 | pub use self::{detect::DetectCommand, keys::KeysCommand, setup::SetupCommand, test::TestCommand}; 9 | use abscissa_core::{Command, Runnable}; 10 | use clap::Subcommand; 11 | use std::path::PathBuf; 12 | 13 | /// The `yubihsm` subcommand 14 | #[derive(Command, Debug, Runnable, Subcommand)] 15 | pub enum YubihsmCommand { 16 | /// detect all YubiHSM2 devices connected via USB 17 | Detect(DetectCommand), 18 | 19 | /// key management subcommands 20 | #[clap(subcommand)] 21 | Keys(KeysCommand), 22 | 23 | /// initial device setup and configuration 24 | Setup(SetupCommand), 25 | 26 | /// perform a signing test 27 | Test(TestCommand), 28 | } 29 | 30 | impl YubihsmCommand { 31 | pub(super) fn config_path(&self) -> Option<&PathBuf> { 32 | // Mark that we're invoking a `tmkms yubihsm` command 33 | crate::yubihsm::mark_cli_command(); 34 | 35 | match self { 36 | YubihsmCommand::Keys(keys) => keys.config_path(), 37 | YubihsmCommand::Setup(setup) => setup.config.as_ref(), 38 | YubihsmCommand::Test(test) => test.config.as_ref(), 39 | _ => None, 40 | } 41 | } 42 | 43 | pub(super) fn verbose(&self) -> bool { 44 | match self { 45 | YubihsmCommand::Detect(detect) => detect.verbose, 46 | YubihsmCommand::Setup(setup) => setup.verbose, 47 | YubihsmCommand::Test(test) => test.verbose, 48 | _ => false, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/yubihsm/detect.rs: -------------------------------------------------------------------------------- 1 | //! Detect YubiHSM2s connected via USB 2 | 3 | use crate::prelude::*; 4 | use abscissa_core::Command; 5 | use clap::Parser; 6 | use std::process; 7 | use yubihsm::connector::usb::Devices; 8 | 9 | /// The `yubihsm detect` subcommand 10 | #[derive(Command, Debug, Default, Parser)] 11 | pub struct DetectCommand { 12 | /// path to tmkms.toml 13 | #[clap(short = 'c', long = "config")] 14 | pub config: Option, 15 | 16 | /// enable verbose debug logging 17 | #[clap(short = 'v', long = "verbose")] 18 | pub verbose: bool, 19 | } 20 | 21 | impl Runnable for DetectCommand { 22 | /// Detect all YubiHSM2 devices connected via USB 23 | fn run(&self) { 24 | let devices = Devices::detect(Default::default()).unwrap_or_else(|e| { 25 | status_err!("couldn't detect USB devices: {}", e); 26 | 27 | // TODO: handle exits via abscissa 28 | process::exit(1); 29 | }); 30 | 31 | if devices.is_empty() { 32 | status_err!("no YubiHSM2 devices detected!"); 33 | process::exit(1); 34 | } 35 | 36 | println!("Detected YubiHSM2 USB devices:"); 37 | 38 | for device in devices.iter() { 39 | println!( 40 | "- Serial #{} (bus {})", 41 | device.serial_number, 42 | device.bus_number(), 43 | ); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/yubihsm/keys.rs: -------------------------------------------------------------------------------- 1 | //! YubiHSM2 key management commands 2 | 3 | mod export; 4 | mod generate; 5 | mod import; 6 | mod list; 7 | 8 | use self::{ 9 | export::ExportCommand, generate::GenerateCommand, import::ImportCommand, list::ListCommand, 10 | }; 11 | use abscissa_core::{Command, Runnable}; 12 | use clap::Subcommand; 13 | use std::path::PathBuf; 14 | 15 | /// Default YubiHSM2 domain (internal partitioning) 16 | pub const DEFAULT_DOMAINS: yubihsm::Domain = yubihsm::Domain::DOM1; 17 | 18 | /// Default wrap key to use when exporting 19 | pub const DEFAULT_WRAP_KEY: yubihsm::object::Id = 1; 20 | 21 | /// The `yubihsm keys` subcommand 22 | #[derive(Command, Debug, Subcommand, Runnable)] 23 | pub enum KeysCommand { 24 | /// export an encrypted backup of a signing key inside the HSM device 25 | Export(ExportCommand), 26 | 27 | /// generate an Ed25519 signing key inside the HSM device 28 | Generate(GenerateCommand), 29 | 30 | /// import validator signing key for the 'yubihsm keys' subcommand 31 | Import(ImportCommand), 32 | 33 | /// list all suitable Ed25519 keys in the HSM 34 | List(ListCommand), 35 | } 36 | 37 | impl KeysCommand { 38 | /// Optional path to the configuration file 39 | pub(super) fn config_path(&self) -> Option<&PathBuf> { 40 | match self { 41 | KeysCommand::Export(export) => export.config.as_ref(), 42 | KeysCommand::Generate(generate) => generate.config.as_ref(), 43 | KeysCommand::List(list) => list.config.as_ref(), 44 | KeysCommand::Import(import) => import.config.as_ref(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/yubihsm/keys/export.rs: -------------------------------------------------------------------------------- 1 | //! Create encrypted backups of YubiHSM2 keys 2 | 3 | use super::DEFAULT_WRAP_KEY; 4 | use crate::{key_utils, prelude::*}; 5 | use abscissa_core::Command; 6 | use clap::Parser; 7 | use std::{path::PathBuf, process}; 8 | 9 | /// The `yubihsm keys export` subcommand: create encrypted backups of keys 10 | #[derive(Command, Debug, Default, Parser)] 11 | pub struct ExportCommand { 12 | /// path to tmkms.toml 13 | #[clap(short = 'c', long = "config")] 14 | pub config: Option, 15 | 16 | /// ID of key to export in encrypted form 17 | #[clap(short = 'i', long = "id")] 18 | pub key_id: u16, 19 | 20 | /// ID of the wrap key to encrypt the exported key under 21 | #[clap(short = 'w', long = "wrapkey")] 22 | pub wrap_key_id: Option, 23 | 24 | /// Path to write the resulting file to 25 | pub path: PathBuf, 26 | } 27 | 28 | impl Runnable for ExportCommand { 29 | fn run(&self) { 30 | let wrap_key_id = self.wrap_key_id.unwrap_or(DEFAULT_WRAP_KEY); 31 | 32 | let wrapped_bytes = crate::yubihsm::client() 33 | .export_wrapped( 34 | wrap_key_id, 35 | yubihsm::object::Type::AsymmetricKey, 36 | self.key_id, 37 | ) 38 | .unwrap_or_else(|e| { 39 | status_err!( 40 | "couldn't export key {} under wrap key {}: {}", 41 | self.key_id, 42 | wrap_key_id, 43 | e 44 | ); 45 | process::exit(1); 46 | }); 47 | 48 | key_utils::write_base64_secret(&self.path, &wrapped_bytes.into_vec()).unwrap_or_else(|e| { 49 | status_err!("{}", e); 50 | process::exit(1); 51 | }); 52 | 53 | status_ok!( 54 | "Exported", 55 | "key 0x{:04x} (encrypted under wrap key 0x{:04x}) to {}", 56 | self.key_id, 57 | wrap_key_id, 58 | self.path.display() 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/yubihsm/keys/generate.rs: -------------------------------------------------------------------------------- 1 | //! Generate a new key within the YubiHSM2 2 | 3 | use super::{DEFAULT_DOMAINS, DEFAULT_WRAP_KEY}; 4 | use crate::{config::provider::KeyType, key_utils, prelude::*}; 5 | use abscissa_core::Command; 6 | use chrono::{SecondsFormat, Utc}; 7 | use clap::Parser; 8 | use std::{ 9 | path::{Path, PathBuf}, 10 | process, 11 | }; 12 | use tendermint::PublicKey; 13 | 14 | /// The `yubihsm keys generate` subcommand 15 | #[derive(Command, Debug, Default, Parser)] 16 | pub struct GenerateCommand { 17 | /// path to tmkms.toml 18 | #[clap(short = 'c', long = "config")] 19 | pub config: Option, 20 | 21 | /// label for generated key 22 | #[clap(short = 'l', long = "label")] 23 | pub label: Option, 24 | 25 | /// bech32 prefix to display generated key with 26 | #[clap(short = 'p', long = "prefix")] 27 | pub bech32_prefix: Option, 28 | 29 | /// type of key to generate (default: ed25519) 30 | #[clap(short = 't')] 31 | pub key_type: Option, 32 | 33 | /// Mark this key as non-exportable 34 | #[clap(long = "non-exportable")] 35 | pub non_exportable: bool, 36 | 37 | /// path where encrypted backup should be written 38 | #[clap(short = 'b', long = "backup")] 39 | pub backup_file: Option, 40 | 41 | /// Key ID of the wrap key to use when creating a backup 42 | #[clap(short = 'w', long = "wrapkey")] 43 | pub wrap_key_id: Option, 44 | 45 | /// Key ID to generate 46 | pub key_ids: Vec, 47 | } 48 | 49 | impl GenerateCommand { 50 | /// Parse the key ID provided in the arguments 51 | pub fn parse_key_id(&self) -> u16 { 52 | if self.key_ids.len() != 1 { 53 | status_err!( 54 | "expected exactly 1 key ID to generate, got {}", 55 | self.key_ids.len() 56 | ); 57 | process::exit(1); 58 | } 59 | 60 | let key_id_str = &self.key_ids[0]; 61 | 62 | if let Some(s) = key_id_str.strip_prefix("0x") { 63 | u16::from_str_radix(s, 16).ok() 64 | } else { 65 | key_id_str.parse().ok() 66 | } 67 | .unwrap_or_else(|| { 68 | status_err!("couldn't parse key ID: {}", key_id_str); 69 | process::exit(1); 70 | }) 71 | } 72 | 73 | /// Parse the key type provided in the arguments 74 | pub fn parse_key_type(&self) -> KeyType { 75 | match self.key_type.as_ref().map(AsRef::as_ref) { 76 | Some("account") => KeyType::Account, 77 | Some("consensus") | None => KeyType::Consensus, // default 78 | Some(other) => { 79 | status_err!("invalid key type: {}", other); 80 | process::exit(1); 81 | } 82 | } 83 | } 84 | } 85 | 86 | impl Runnable for GenerateCommand { 87 | /// Generate an Ed25519 signing key inside a YubiHSM2 device 88 | fn run(&self) { 89 | let key_id = self.parse_key_id(); 90 | let key_type = self.parse_key_type(); 91 | 92 | let hsm = crate::yubihsm::client(); 93 | let mut capabilities = match key_type { 94 | KeyType::Account => yubihsm::Capability::SIGN_ECDSA, 95 | KeyType::Consensus => yubihsm::Capability::SIGN_EDDSA, 96 | }; 97 | 98 | // If the key isn't explicitly marked as non-exportable, allow it to be exported 99 | if !self.non_exportable { 100 | capabilities |= yubihsm::Capability::EXPORTABLE_UNDER_WRAP; 101 | } 102 | 103 | let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); 104 | let label = yubihsm::object::Label::from( 105 | match self.label { 106 | Some(ref l) => l.to_owned(), 107 | None => match self.bech32_prefix { 108 | Some(ref prefix) => format!("{prefix}:{timestamp}"), 109 | None => format!("{key_type}:{timestamp}"), 110 | }, 111 | } 112 | .as_ref(), 113 | ); 114 | 115 | let algorithm = match key_type { 116 | KeyType::Account => yubihsm::asymmetric::Algorithm::EcK256, 117 | KeyType::Consensus => yubihsm::asymmetric::Algorithm::Ed25519, 118 | }; 119 | 120 | if let Err(e) = hsm.generate_asymmetric_key( 121 | key_id, 122 | label, 123 | DEFAULT_DOMAINS, // TODO(tarcieri): customize domains 124 | capabilities, 125 | algorithm, 126 | ) { 127 | status_err!("couldn't generate key #{}: {}", key_id, e); 128 | process::exit(1); 129 | } 130 | 131 | match key_type { 132 | KeyType::Account => { 133 | // TODO(tarcieri): generate and show account ID (fingerprint) 134 | status_ok!("Generated", "account (secp256k1) key 0x{:04x}", key_id) 135 | } 136 | KeyType::Consensus => { 137 | // TODO(tarcieri): use KeyFormat (when available) to format Bech32 138 | let public_key = PublicKey::from_raw_ed25519( 139 | hsm.get_public_key(key_id) 140 | .unwrap_or_else(|e| { 141 | status_err!("couldn't get public key for key #{}: {}", key_id, e); 142 | process::exit(1); 143 | }) 144 | .as_ref(), 145 | ) 146 | .unwrap(); 147 | 148 | let public_key_string = match self.bech32_prefix { 149 | Some(ref prefix) => public_key.to_bech32(prefix), 150 | None => public_key.to_hex(), 151 | }; 152 | 153 | status_ok!( 154 | "Generated", 155 | "consensus (ed25519) key 0x{:04x}: {}", 156 | key_id, 157 | public_key_string 158 | ) 159 | } 160 | } 161 | 162 | if let Some(ref backup_file) = self.backup_file { 163 | create_encrypted_backup( 164 | &hsm, 165 | key_id, 166 | backup_file, 167 | self.wrap_key_id.unwrap_or(DEFAULT_WRAP_KEY), 168 | ); 169 | } 170 | } 171 | } 172 | 173 | /// Create an encrypted backup of this key under the given wrap key ID 174 | // TODO(tarcieri): unify this with the similar code in export? 175 | fn create_encrypted_backup( 176 | hsm: &yubihsm::Client, 177 | key_id: yubihsm::object::Id, 178 | backup_file_path: &Path, 179 | wrap_key_id: yubihsm::object::Id, 180 | ) { 181 | let wrapped_bytes = hsm 182 | .export_wrapped(wrap_key_id, yubihsm::object::Type::AsymmetricKey, key_id) 183 | .unwrap_or_else(|e| { 184 | status_err!( 185 | "couldn't export key {} under wrap key {}: {}", 186 | key_id, 187 | wrap_key_id, 188 | e 189 | ); 190 | process::exit(1); 191 | }); 192 | 193 | key_utils::write_base64_secret(backup_file_path, &wrapped_bytes.into_vec()).unwrap_or_else( 194 | |e| { 195 | status_err!("{}", e); 196 | process::exit(1); 197 | }, 198 | ); 199 | 200 | status_ok!( 201 | "Wrote", 202 | "backup of key {} (encrypted under wrap key {}) to {}", 203 | key_id, 204 | wrap_key_id, 205 | backup_file_path.display() 206 | ); 207 | } 208 | -------------------------------------------------------------------------------- /src/commands/yubihsm/keys/list.rs: -------------------------------------------------------------------------------- 1 | //! List keys inside the YubiHSM2 2 | 3 | use crate::{chain, keyring, prelude::*, Map}; 4 | use abscissa_core::Command; 5 | use clap::Parser; 6 | use k256::elliptic_curve::generic_array::GenericArray; 7 | use std::{path::PathBuf, process}; 8 | use tendermint::{PublicKey, TendermintKey}; 9 | 10 | /// The `yubihsm keys list` subcommand 11 | #[derive(Command, Debug, Default, Parser)] 12 | pub struct ListCommand { 13 | /// path to tmkms.toml 14 | #[clap(short = 'c', long = "config")] 15 | pub config: Option, 16 | } 17 | 18 | impl Runnable for ListCommand { 19 | /// List all suitable Ed25519 keys in the HSM 20 | fn run(&self) { 21 | let key_formatters = load_key_formatters(); 22 | let hsm = crate::yubihsm::client(); 23 | 24 | let serial_number = hsm 25 | .device_info() 26 | .unwrap_or_else(|e| { 27 | status_err!("couldn't get YubiHSM serial number: {}", e); 28 | process::exit(1); 29 | }) 30 | .serial_number; 31 | 32 | let objects = hsm.list_objects(&[]).unwrap_or_else(|e| { 33 | status_err!("couldn't list YubiHSM objects: {}", e); 34 | process::exit(1); 35 | }); 36 | 37 | let mut keys = objects 38 | .iter() 39 | .filter(|o| o.object_type == yubihsm::object::Type::AsymmetricKey) 40 | .collect::>(); 41 | 42 | keys.sort_by(|k1, k2| k1.object_id.cmp(&k2.object_id)); 43 | 44 | if keys.is_empty() { 45 | status_err!("no keys in this YubiHSM (#{})", serial_number); 46 | process::exit(0); 47 | } 48 | 49 | println!("Listing keys in YubiHSM #{serial_number}:"); 50 | 51 | for key in &keys { 52 | display_key_info(&hsm, key, &key_formatters); 53 | } 54 | } 55 | } 56 | 57 | /// Load information about configured YubiHSM keys 58 | fn load_key_formatters() -> Map { 59 | let chain_formatters = load_chain_formatters(); 60 | let cfg = crate::yubihsm::config(); 61 | let mut map = Map::new(); 62 | 63 | for key_config in &cfg.keys { 64 | // Only use a preferred formatting if there is one chain per key 65 | if key_config.chain_ids.len() == 1 { 66 | if let Some(formatter) = chain_formatters.get(&key_config.chain_ids[0]) { 67 | if map.insert(key_config.key, formatter.clone()).is_some() { 68 | status_err!("duplicate YubiHSM config for key: 0x{:04x}", key_config.key); 69 | process::exit(1); 70 | } 71 | } 72 | } 73 | } 74 | 75 | map 76 | } 77 | 78 | /// Load chain-specific key formatters from the configuration 79 | fn load_chain_formatters() -> Map { 80 | let cfg = APP.config(); 81 | let mut map = Map::new(); 82 | 83 | for chain in &cfg.chain { 84 | if map 85 | .insert(chain.id.clone(), chain.key_format.clone()) 86 | .is_some() 87 | { 88 | status_err!("duplicate chain config for '{}'", chain.id); 89 | process::exit(1); 90 | } 91 | } 92 | 93 | map 94 | } 95 | 96 | /// Display information about a key 97 | fn display_key_info( 98 | hsm: &yubihsm::Client, 99 | key: &yubihsm::object::Entry, 100 | key_formatters: &Map, 101 | ) { 102 | let key_info = hsm 103 | .get_object_info(key.object_id, yubihsm::object::Type::AsymmetricKey) 104 | .unwrap_or_else(|e| { 105 | status_err!( 106 | "couldn't get object info for asymmetric key #{}: {}", 107 | key.object_id, 108 | e 109 | ); 110 | process::exit(1); 111 | }); 112 | 113 | let public_key = hsm.get_public_key(key.object_id).unwrap_or_else(|e| { 114 | status_err!( 115 | "couldn't get public key for asymmetric key #{}: {}", 116 | key.object_id, 117 | e 118 | ); 119 | process::exit(1); 120 | }); 121 | 122 | let key_id = format!("- 0x{:04x}", key.object_id); 123 | 124 | let tendermint_key = match public_key.algorithm { 125 | yubihsm::asymmetric::Algorithm::EcK256 => { 126 | // The YubiHSM2 returns the uncompressed public key, so for 127 | // compatibility with Tendermint, we have to compress it first 128 | let compressed_pubkey = k256::EncodedPoint::from_untagged_bytes( 129 | GenericArray::from_slice(public_key.as_ref()), 130 | ) 131 | .compress(); 132 | 133 | TendermintKey::AccountKey( 134 | PublicKey::from_raw_secp256k1(compressed_pubkey.as_ref()).unwrap(), 135 | ) 136 | } 137 | yubihsm::asymmetric::Algorithm::Ed25519 => { 138 | let pk = PublicKey::from_raw_ed25519(public_key.as_ref()).unwrap(); 139 | TendermintKey::ConsensusKey(pk) 140 | } 141 | other => { 142 | status_attr_err!(key_id, "unsupported algorithm: {:?}", other); 143 | return; 144 | } 145 | }; 146 | 147 | let key_type = match tendermint_key { 148 | TendermintKey::AccountKey(_) => "acct", 149 | TendermintKey::ConsensusKey(_) => "cons", 150 | }; 151 | 152 | let key_serialized = match key_formatters.get(&key.object_id) { 153 | Some(key_formatter) => key_formatter.serialize(tendermint_key), 154 | None => match tendermint_key { 155 | TendermintKey::AccountKey(k) => k.to_hex(), 156 | TendermintKey::ConsensusKey(k) => k.to_hex(), 157 | }, 158 | }; 159 | 160 | status_attr_ok!(key_id, "[{}] {}", key_type, key_serialized); 161 | println!(" label: \"{}\"", &key_info.label); 162 | } 163 | -------------------------------------------------------------------------------- /src/commands/yubihsm/test.rs: -------------------------------------------------------------------------------- 1 | //! Test the YubiHSM2 is working by performing signatures successively 2 | 3 | use crate::prelude::*; 4 | use abscissa_core::Command; 5 | use clap::Parser; 6 | use std::{ 7 | path::PathBuf, 8 | process, thread, 9 | time::{Duration, Instant}, 10 | }; 11 | 12 | // TODO: figure out rough size of the proposal amino message for testing 13 | const TEST_MESSAGE: &[u8; 128] = &[0u8; 128]; 14 | 15 | /// The `yubihsm test` subcommand 16 | #[derive(Command, Debug, Default, Parser)] 17 | pub struct TestCommand { 18 | /// path to tmkms.toml 19 | #[clap(short = 'c', long = "config")] 20 | pub config: Option, 21 | 22 | /// enable verbose debug logging 23 | #[clap(short = 'v', long = "verbose")] 24 | pub verbose: bool, 25 | 26 | /// Ed25519 signing key ID in YubiHSM 27 | key_id: u16, 28 | } 29 | 30 | impl Runnable for TestCommand { 31 | /// Perform a signing test using the current HSM configuration 32 | fn run(&self) { 33 | if self.key_id == 0 { 34 | status_err!("no key ID given"); 35 | process::exit(1); 36 | } 37 | 38 | let hsm = crate::yubihsm::client(); 39 | 40 | loop { 41 | let started_at = Instant::now(); 42 | 43 | if let Err(e) = hsm.sign_ed25519(self.key_id, TEST_MESSAGE.as_ref()) { 44 | status_err!("signature operation failed: {}", e); 45 | thread::sleep(Duration::from_millis(250)); 46 | } else { 47 | status_ok!( 48 | "Success", 49 | "signed message using key ID #{} in {} ms", 50 | self.key_id, 51 | started_at.elapsed().as_millis() 52 | ); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration file structures (with serde-derived parser) 2 | 3 | pub mod chain; 4 | pub mod provider; 5 | pub mod validator; 6 | 7 | pub use self::validator::*; 8 | 9 | use self::{chain::ChainConfig, provider::ProviderConfig}; 10 | use serde::Deserialize; 11 | 12 | /// Environment variable containing path to config file 13 | pub const CONFIG_ENV_VAR: &str = "TMKMS_CONFIG_FILE"; 14 | 15 | /// Name of the KMS configuration file 16 | pub const CONFIG_FILE_NAME: &str = "tmkms.toml"; 17 | 18 | /// KMS configuration (i.e. TOML file parsed with serde) 19 | #[derive(Default, Deserialize, Debug)] 20 | #[serde(deny_unknown_fields)] 21 | pub struct KmsConfig { 22 | /// Chains the KMS is providing key management service for 23 | #[serde(default)] 24 | pub chain: Vec, 25 | 26 | /// Cryptographic signature provider configuration 27 | pub providers: ProviderConfig, 28 | 29 | /// Addresses of validator nodes 30 | #[serde(default)] 31 | pub validator: Vec, 32 | } 33 | -------------------------------------------------------------------------------- /src/config/chain.rs: -------------------------------------------------------------------------------- 1 | //! Chain configuration 2 | 3 | mod hook; 4 | 5 | pub use self::hook::HookConfig; 6 | use crate::{chain, keyring}; 7 | use serde::Deserialize; 8 | use std::path::PathBuf; 9 | 10 | /// Chain configuration 11 | #[derive(Deserialize, Debug)] 12 | #[serde(deny_unknown_fields)] 13 | pub struct ChainConfig { 14 | /// Chain ID of this Tendermint network/chain 15 | pub id: chain::Id, 16 | 17 | /// Key serialization format configuration for this chain 18 | pub key_format: keyring::Format, 19 | 20 | /// Should vote extensions on this chain be signed? (default: false) 21 | /// 22 | /// CometBFT v0.38 and newer supports an `ExtendedCommitSig` which requires computing an 23 | /// additional signature over an extension using the consensus key beyond simply signing a vote. 24 | /// 25 | /// Note: in the future this can be autodetected via the `signExtension` field on `SignVote`. 26 | /// See cometbft/cometbft#2439. 27 | #[serde(default)] 28 | pub sign_extensions: bool, 29 | 30 | /// Path to chain-specific `priv_validator_state.json` file 31 | pub state_file: Option, 32 | 33 | /// User-specified command to run to obtain the current block height for 34 | /// this chain. This will be executed at launch time to populate the 35 | /// initial block height if configured 36 | pub state_hook: Option, 37 | } 38 | -------------------------------------------------------------------------------- /src/config/chain/hook.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::ffi::OsString; 3 | 4 | /// Configuration for a particular hook to invoke 5 | #[derive(Default, Deserialize, Debug)] 6 | #[serde(deny_unknown_fields)] 7 | pub struct HookConfig { 8 | /// Command (with arguments) to invoke 9 | pub cmd: Vec, 10 | 11 | /// Timeout (in seconds) to wait when executing the command (default 5) 12 | pub timeout_secs: Option, 13 | 14 | /// Whether or not to fail open or closed if this command fails to execute. 15 | /// Failing closed will prevent the KMS from starting if this command fails. 16 | pub fail_closed: bool, 17 | } 18 | -------------------------------------------------------------------------------- /src/config/provider.rs: -------------------------------------------------------------------------------- 1 | //! Cryptographic service providers: signing backends 2 | 3 | #[cfg(feature = "fortanixdsm")] 4 | pub mod fortanixdsm; 5 | #[cfg(feature = "ledger")] 6 | pub mod ledgertm; 7 | #[cfg(feature = "softsign")] 8 | pub mod softsign; 9 | #[cfg(feature = "yubihsm")] 10 | pub mod yubihsm; 11 | 12 | #[cfg(feature = "fortanixdsm")] 13 | use self::fortanixdsm::FortanixDsmConfig; 14 | #[cfg(feature = "ledger")] 15 | use self::ledgertm::LedgerTendermintConfig; 16 | #[cfg(feature = "softsign")] 17 | use self::softsign::SoftsignConfig; 18 | #[cfg(feature = "yubihsm")] 19 | use self::yubihsm::YubihsmConfig; 20 | 21 | use serde::Deserialize; 22 | use std::fmt; 23 | 24 | /// Provider configuration 25 | #[derive(Default, Deserialize, Debug)] 26 | #[serde(deny_unknown_fields)] 27 | pub struct ProviderConfig { 28 | /// Software-backed signer 29 | #[cfg(feature = "softsign")] 30 | #[serde(default)] 31 | pub softsign: Vec, 32 | 33 | /// Map of yubihsm-connector labels to their configurations 34 | #[cfg(feature = "yubihsm")] 35 | #[serde(default)] 36 | pub yubihsm: Vec, 37 | 38 | /// Map of ledger-tm labels to their configurations 39 | #[cfg(feature = "ledger")] 40 | #[serde(default)] 41 | pub ledgertm: Vec, 42 | 43 | /// Fortanix DSM provider configurations 44 | #[cfg(feature = "fortanixdsm")] 45 | #[serde(default)] 46 | pub fortanixdsm: Vec, 47 | } 48 | 49 | /// Types of cryptographic keys 50 | // TODO(tarcieri): move this into a provider-agnostic module 51 | #[derive(Clone, Debug, Deserialize)] 52 | pub enum KeyType { 53 | /// Account keys 54 | #[serde(rename = "account")] 55 | Account, 56 | 57 | /// Consensus keys 58 | #[serde(rename = "consensus")] 59 | Consensus, 60 | } 61 | 62 | impl Default for KeyType { 63 | /// Backwards compat for existing configuration files 64 | fn default() -> Self { 65 | KeyType::Consensus 66 | } 67 | } 68 | 69 | impl fmt::Display for KeyType { 70 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 71 | match self { 72 | KeyType::Account => f.write_str("account"), 73 | KeyType::Consensus => f.write_str("consensus"), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/config/provider/fortanixdsm.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for the Fortanix DSM backend 2 | 3 | use super::KeyType; 4 | use crate::chain; 5 | use sdkms::api_model::SobjectDescriptor; 6 | use serde::Deserialize; 7 | use uuid::Uuid; 8 | 9 | /// The (optional) `[providers.fortanixdsm]` config section 10 | #[derive(Clone, Deserialize, Debug)] 11 | #[serde(deny_unknown_fields)] 12 | pub struct FortanixDsmConfig { 13 | /// Fortanix DSM API endpoint, e.g. https://amer.smartkey.io 14 | pub api_endpoint: String, 15 | 16 | /// API key for authenticating to DSM 17 | pub api_key: String, 18 | 19 | /// List of signing keys 20 | #[serde(default)] 21 | pub signing_keys: Vec, 22 | } 23 | 24 | /// Signing key configuration 25 | #[derive(Clone, Debug, Deserialize)] 26 | #[serde(deny_unknown_fields)] 27 | pub struct SigningKeyConfig { 28 | /// Chains this signing key is authorized to be used from 29 | pub chain_ids: Vec, 30 | 31 | /// Signing key descriptor 32 | #[serde(flatten)] 33 | pub key: KeyDescriptor, 34 | 35 | /// Type of key 36 | #[serde(default, rename = "type")] 37 | pub key_type: KeyType, 38 | } 39 | 40 | /// A key (i.e. security object) stored in Fortanix DSM 41 | #[derive(Clone, Debug, Deserialize)] 42 | #[serde(deny_unknown_fields, rename_all = "snake_case")] 43 | pub enum KeyDescriptor { 44 | /// Specify a DSM key by its unique id 45 | KeyId(Uuid), 46 | 47 | /// Specify a DSM key by its name 48 | KeyName(String), 49 | } 50 | 51 | impl From for SobjectDescriptor { 52 | fn from(x: KeyDescriptor) -> Self { 53 | match x { 54 | KeyDescriptor::KeyId(id) => SobjectDescriptor::Kid(id), 55 | KeyDescriptor::KeyName(name) => SobjectDescriptor::Name(name), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/config/provider/ledgertm.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for Ledger Tendermint signer 2 | 3 | use crate::chain; 4 | use serde::Deserialize; 5 | 6 | /// Ledger Tendermint signer configuration 7 | #[derive(Deserialize, Debug)] 8 | #[serde(deny_unknown_fields)] 9 | pub struct LedgerTendermintConfig { 10 | /// Chains this signing key is authorized to be used from 11 | pub chain_ids: Vec, 12 | } 13 | -------------------------------------------------------------------------------- /src/config/provider/softsign.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for software-backed signer (using ed25519-dalek) 2 | 3 | use super::KeyType; 4 | use crate::{ 5 | chain, 6 | error::{Error, ErrorKind::ConfigError}, 7 | prelude::*, 8 | }; 9 | use serde::Deserialize; 10 | use std::{ 11 | path::{Path, PathBuf}, 12 | str::FromStr, 13 | }; 14 | 15 | /// Software signer configuration 16 | #[derive(Deserialize, Debug)] 17 | #[serde(deny_unknown_fields)] 18 | pub struct SoftsignConfig { 19 | /// Chains this signing key is authorized to be used from 20 | pub chain_ids: Vec, 21 | 22 | /// Type of key (account vs consensus, default consensus) 23 | #[serde(default)] 24 | pub key_type: KeyType, 25 | 26 | /// Private key file format 27 | pub key_format: Option, 28 | 29 | /// Path to a file containing a cryptographic key 30 | // TODO: use `abscissa_core::Secret` to wrap this `PathBuf` 31 | pub path: SoftPrivateKey, 32 | } 33 | 34 | /// Software-backed private key (stored in a file) 35 | #[derive(Deserialize, Debug)] 36 | #[serde(deny_unknown_fields)] 37 | pub struct SoftPrivateKey(PathBuf); 38 | 39 | impl AsRef for SoftPrivateKey { 40 | /// Borrow this private key as a path 41 | fn as_ref(&self) -> &Path { 42 | self.0.as_ref() 43 | } 44 | } 45 | 46 | /// Private key format 47 | #[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq)] 48 | pub enum KeyFormat { 49 | /// Base64-encoded 50 | #[serde(rename = "base64")] 51 | #[default] 52 | Base64, 53 | 54 | /// JSON 55 | #[serde(rename = "json")] 56 | Json, 57 | } 58 | impl FromStr for KeyFormat { 59 | type Err = Error; 60 | 61 | fn from_str(s: &str) -> Result { 62 | let format = match s { 63 | "base64" => KeyFormat::Base64, 64 | "json" => KeyFormat::Json, 65 | other => fail!(ConfigError, "invalid key format: {}", other), 66 | }; 67 | 68 | Ok(format) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/config/provider/yubihsm.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for the `YubiHSM` backend 2 | 3 | use super::KeyType; 4 | use crate::{chain, prelude::*}; 5 | use serde::Deserialize; 6 | use std::{fmt, fs, path::PathBuf, process}; 7 | use tendermint_config::net; 8 | use yubihsm::Credentials; 9 | use zeroize::{Zeroize, Zeroizing}; 10 | 11 | /// The (optional) `[providers.yubihsm]` config section 12 | #[derive(Clone, Deserialize, Debug)] 13 | #[serde(deny_unknown_fields)] 14 | pub struct YubihsmConfig { 15 | /// Adapter configuration 16 | pub adapter: AdapterConfig, 17 | 18 | /// Authentication configuration 19 | pub auth: AuthConfig, 20 | 21 | /// List of signing keys in this YubiHSM 22 | #[serde(default)] 23 | pub keys: Vec, 24 | 25 | /// Serial number of the YubiHSM to connect to 26 | pub serial_number: Option, 27 | 28 | /// Configuration for `yubihsm-connector` compatible HTTP server. 29 | #[cfg(feature = "yubihsm-server")] 30 | pub connector_server: Option, 31 | } 32 | 33 | /// Configuration for an individual YubiHSM 34 | #[derive(Clone, Deserialize, Debug)] 35 | #[serde(deny_unknown_fields, tag = "type")] 36 | pub enum AdapterConfig { 37 | /// Connect to the YubiHSM2 directly via USB 38 | #[serde(rename = "usb")] 39 | Usb { 40 | /// Timeout when communicating with YubiHSM2 41 | #[serde(default = "usb_timeout_ms_default")] 42 | timeout_ms: u64, 43 | }, 44 | 45 | /// Connect to the YubiHSM2 via `yubihsm-connector` 46 | #[serde(rename = "http")] 47 | Http { 48 | /// `yubihsm-connector` configuration 49 | addr: net::Address, 50 | }, 51 | } 52 | 53 | /// Configuration options for this connector 54 | #[derive(Clone, Debug, Deserialize)] 55 | #[serde(deny_unknown_fields, untagged)] 56 | pub enum AuthConfig { 57 | /// Path to a separate password file 58 | Path { 59 | /// Authentication key ID to use to authenticate to the YubiHSM 60 | key: u16, 61 | 62 | /// Password file path 63 | password_file: PathBuf, 64 | }, 65 | /// Read password directly from the config file 66 | String { 67 | /// Authentication key ID to use to authenticate to the YubiHSM 68 | key: u16, 69 | 70 | /// Password to use to authenticate to the YubiHSM 71 | password: Password, 72 | }, 73 | } 74 | 75 | impl AuthConfig { 76 | /// Get the `yubihsm::Credentials` for this `AuthConfig` 77 | pub fn credentials(&self) -> Credentials { 78 | match self { 79 | AuthConfig::Path { key, password_file } => { 80 | let password = 81 | Zeroizing::new(fs::read_to_string(password_file).unwrap_or_else(|e| { 82 | status_err!("couldn't read key from {}: {}", password_file.display(), e); 83 | process::exit(1); 84 | })); 85 | 86 | // TODO(tarcieri): constant-time string trimming 87 | let password_trimmed = password.trim_end(); 88 | Credentials::from_password(*key, password_trimmed.as_bytes()) 89 | } 90 | AuthConfig::String { key, password } => { 91 | Credentials::from_password(*key, password.0.as_bytes()) 92 | } 93 | } 94 | } 95 | } 96 | 97 | /// Password to the YubiHSM 98 | #[derive(Clone, Deserialize, Zeroize)] 99 | #[serde(deny_unknown_fields)] 100 | #[zeroize(drop)] 101 | pub struct Password(String); 102 | 103 | impl fmt::Debug for Password { 104 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 105 | f.write_str("REDACTED PASSWORD") 106 | } 107 | } 108 | 109 | /// Signing key configuration 110 | #[derive(Clone, Debug, Deserialize)] 111 | #[serde(deny_unknown_fields)] 112 | pub struct SigningKeyConfig { 113 | /// Chains this signing key is authorized to be used from 114 | pub chain_ids: Vec, 115 | 116 | /// Signing key ID 117 | pub key: u16, 118 | 119 | /// Type of key 120 | #[serde(default, rename = "type")] 121 | pub key_type: KeyType, 122 | } 123 | 124 | /// Default value for `AdapterConfig::Usb { timeout_ms }` 125 | fn usb_timeout_ms_default() -> u64 { 126 | 1000 127 | } 128 | 129 | /// Configuration for `yubihsm-connector` compatible service 130 | #[cfg(feature = "yubihsm-server")] 131 | #[derive(Clone, Debug, Deserialize)] 132 | #[serde(deny_unknown_fields)] 133 | pub struct ConnectorServerConfig { 134 | /// Listen address to run the connector service at 135 | pub laddr: net::Address, 136 | 137 | /// Connect to the listen address when using `tmkms yubihsm` CLI 138 | pub cli: Option, 139 | } 140 | 141 | /// Overrides for when using the `tmkms yubihsm` command-line interface 142 | #[cfg(feature = "yubihsm-server")] 143 | #[derive(Clone, Debug, Deserialize)] 144 | #[serde(deny_unknown_fields)] 145 | pub struct CliConfig { 146 | /// Override the auth key to use when using the CLI. This will additionally 147 | /// prompt for a password from the terminal. 148 | pub auth_key: Option, 149 | } 150 | -------------------------------------------------------------------------------- /src/config/validator.rs: -------------------------------------------------------------------------------- 1 | //! Validator configuration 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::path::PathBuf; 5 | use tendermint::chain; 6 | use tendermint_config::net; 7 | use tendermint_p2p::secret_connection; 8 | 9 | /// Validator configuration 10 | #[derive(Clone, Debug, Deserialize, Serialize)] 11 | #[serde(deny_unknown_fields)] 12 | pub struct ValidatorConfig { 13 | /// Address of the validator (`tcp://` or `unix://`) 14 | pub addr: net::Address, 15 | 16 | /// Chain ID of the Tendermint network this validator is part of 17 | pub chain_id: chain::Id, 18 | 19 | /// Automatically reconnect on error? (default: true) 20 | #[serde(default = "reconnect_default")] 21 | pub reconnect: bool, 22 | 23 | /// Optional timeout value in seconds 24 | pub timeout: Option, 25 | 26 | /// Path to our Ed25519 identity key (if applicable) 27 | pub secret_key: Option, 28 | 29 | /// Height at which to stop signing 30 | pub max_height: Option, 31 | 32 | /// Version of Secret Connection protocol to use when connecting 33 | pub protocol_version: ProtocolVersion, 34 | } 35 | 36 | /// Protocol version (based on the Tendermint version) 37 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] 38 | #[allow(non_camel_case_types)] 39 | pub enum ProtocolVersion { 40 | /// Tendermint v0.34 and newer. 41 | #[serde(rename = "v0.34")] 42 | V0_34, 43 | 44 | /// Tendermint v0.33 45 | #[serde(rename = "v0.33")] 46 | V0_33, 47 | } 48 | 49 | impl From for secret_connection::Version { 50 | fn from(version: ProtocolVersion) -> secret_connection::Version { 51 | match version { 52 | ProtocolVersion::V0_34 => secret_connection::Version::V0_34, 53 | ProtocolVersion::V0_33 => secret_connection::Version::V0_33, 54 | } 55 | } 56 | } 57 | 58 | /// Default value for the `ValidatorConfig` reconnect field 59 | fn reconnect_default() -> bool { 60 | true 61 | } 62 | -------------------------------------------------------------------------------- /src/connection.rs: -------------------------------------------------------------------------------- 1 | //! Connections to a validator (TCP or Unix socket) 2 | 3 | use std::io; 4 | 5 | use tendermint_p2p::secret_connection::SecretConnection; 6 | 7 | use self::unix::UnixConnection; 8 | 9 | pub mod tcp; 10 | pub mod unix; 11 | 12 | /// Connections to a validator 13 | pub trait Connection: io::Read + io::Write + Sync + Send {} 14 | 15 | impl Connection for SecretConnection where T: io::Read + io::Write + Sync + Send {} 16 | impl Connection for UnixConnection where T: io::Read + io::Write + Sync + Send {} 17 | -------------------------------------------------------------------------------- /src/connection/tcp.rs: -------------------------------------------------------------------------------- 1 | //! TCP socket connection to a validator 2 | 3 | use std::{net::TcpStream, path::PathBuf, time::Duration}; 4 | 5 | use subtle::ConstantTimeEq; 6 | use tendermint::node; 7 | use tendermint_p2p::error::ErrorDetail as TmError; 8 | use tendermint_p2p::secret_connection::{self, PublicKey, SecretConnection}; 9 | 10 | use crate::{ 11 | error::{Error, ErrorKind::*}, 12 | key_utils, 13 | prelude::*, 14 | }; 15 | 16 | /// Default timeout in seconds 17 | const DEFAULT_TIMEOUT: u16 = 10; 18 | 19 | /// Open a TCP socket connection encrypted with SecretConnection 20 | pub fn open_secret_connection( 21 | host: &str, 22 | port: u16, 23 | identity_key_path: &Option, 24 | peer_id: &Option, 25 | timeout: Option, 26 | protocol_version: secret_connection::Version, 27 | ) -> Result, Error> { 28 | let identity_key_path = identity_key_path.as_ref().ok_or_else(|| { 29 | format_err!( 30 | ConfigError, 31 | "config error: no `secret_key` for validator: {}:{}", 32 | host, 33 | port 34 | ) 35 | })?; 36 | 37 | let identity_key = key_utils::load_base64_ed25519_key(identity_key_path)?; 38 | info!("KMS node ID: {}", PublicKey::from(&identity_key)); 39 | 40 | let socket = TcpStream::connect(format!("{host}:{port}"))?; 41 | let timeout = Duration::from_secs(timeout.unwrap_or(DEFAULT_TIMEOUT).into()); 42 | socket.set_read_timeout(Some(timeout))?; 43 | socket.set_write_timeout(Some(timeout))?; 44 | 45 | let connection = match SecretConnection::new(socket, identity_key.into(), protocol_version) { 46 | Ok(conn) => conn, 47 | Err(error) => match error.detail() { 48 | TmError::Crypto(_) => fail!(CryptoError, format!("{error}")), 49 | TmError::Protocol(_) => fail!(ProtocolError, format!("{error}")), 50 | TmError::InvalidKey(_) => fail!(InvalidKey, format!("{error}")), 51 | _ => fail!(ProtocolError, format!("{error}")), 52 | }, 53 | }; 54 | let actual_peer_id = connection.remote_pubkey().peer_id(); 55 | 56 | // TODO(tarcieri): move this into `SecretConnection::new` 57 | if let Some(expected_peer_id) = peer_id { 58 | if expected_peer_id.ct_eq(&actual_peer_id).unwrap_u8() == 0 { 59 | fail!( 60 | VerificationError, 61 | "{}:{}: validator peer ID mismatch! (expected {}, got {})", 62 | host, 63 | port, 64 | expected_peer_id, 65 | actual_peer_id 66 | ); 67 | } 68 | } 69 | 70 | Ok(connection) 71 | } 72 | -------------------------------------------------------------------------------- /src/connection/unix.rs: -------------------------------------------------------------------------------- 1 | //! Unix domain socket connection to a validator 2 | 3 | use std::io; 4 | 5 | /// Protocol implementation of the UNIX socket domain connection 6 | pub struct UnixConnection { 7 | socket: IoHandler, 8 | } 9 | 10 | impl UnixConnection 11 | where 12 | IoHandler: io::Read + io::Write + Send + Sync, 13 | { 14 | /// Create a new `UnixConnection` for the given socket 15 | pub fn new(socket: IoHandler) -> Self { 16 | Self { socket } 17 | } 18 | } 19 | 20 | impl io::Read for UnixConnection 21 | where 22 | IoHandler: io::Read + io::Write + Send + Sync, 23 | { 24 | fn read(&mut self, data: &mut [u8]) -> Result { 25 | self.socket.read(data) 26 | } 27 | } 28 | 29 | impl io::Write for UnixConnection 30 | where 31 | IoHandler: io::Read + io::Write + Send + Sync, 32 | { 33 | fn write(&mut self, data: &[u8]) -> Result { 34 | self.socket.write(data) 35 | } 36 | 37 | fn flush(&mut self) -> Result<(), io::Error> { 38 | self.socket.flush() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use crate::{chain, prelude::*}; 4 | use abscissa_core::error::{BoxError, Context}; 5 | use std::{ 6 | any::Any, 7 | fmt::{self, Display}, 8 | io, 9 | ops::Deref, 10 | }; 11 | use thiserror::Error; 12 | 13 | /// Kinds of errors 14 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Error)] 15 | pub enum ErrorKind { 16 | /// Access denied 17 | #[error("access denied")] 18 | AccessError, 19 | 20 | /// Invalid Chain ID 21 | #[error("chain ID error")] 22 | ChainIdError, 23 | 24 | /// Error in configuration file 25 | #[error("config error")] 26 | ConfigError, 27 | 28 | /// Cryptographic operation failed 29 | #[error("cryptographic error")] 30 | CryptoError, 31 | 32 | /// Double sign attempted 33 | #[error("attempted double sign")] 34 | DoubleSign, 35 | 36 | /// Request a signature above max height 37 | #[error("requested signature above stop height")] 38 | ExceedMaxHeight, 39 | 40 | /// Fortanix DSM related error 41 | #[cfg(feature = "fortanixdsm")] 42 | #[error("Fortanix DSM error")] 43 | FortanixDsmError, 44 | 45 | /// Error running a subcommand to update chain state 46 | #[error("subcommand hook failed")] 47 | HookError, 48 | 49 | /// Malformed or otherwise invalid cryptographic key 50 | #[error("invalid key")] 51 | InvalidKey, 52 | 53 | /// Validation of consensus message failed 54 | #[error("invalid consensus message")] 55 | InvalidMessageError, 56 | 57 | /// Input/output error 58 | #[error("I/O error")] 59 | IoError, 60 | 61 | /// KMS internal panic 62 | #[error("internal crash")] 63 | PanicError, 64 | 65 | /// Parse error 66 | #[error("parse error")] 67 | ParseError, 68 | 69 | /// KMS state has been poisoned 70 | #[error("internal state poisoned")] 71 | PoisonError, 72 | 73 | /// Network protocol-related errors 74 | #[error("protocol error")] 75 | ProtocolError, 76 | 77 | /// Serialization error 78 | #[error("serialization error")] 79 | SerializationError, 80 | 81 | /// Signing operation failed 82 | #[error("signing operation failed")] 83 | SigningError, 84 | 85 | /// Errors originating in the Tendermint crate 86 | #[error("Tendermint error")] 87 | TendermintError, 88 | 89 | /// Verification operation failed 90 | #[error("verification failed")] 91 | VerificationError, 92 | 93 | /// YubiHSM-related errors 94 | #[cfg(feature = "yubihsm")] 95 | #[error("YubiHSM error")] 96 | YubihsmError, 97 | } 98 | 99 | impl ErrorKind { 100 | /// Create an error context from this error 101 | pub fn context(self, source: impl Into) -> Context { 102 | Context::new(self, Some(source.into())) 103 | } 104 | } 105 | 106 | /// Error type 107 | #[derive(Debug)] 108 | pub struct Error(Box>); 109 | 110 | impl Error { 111 | /// Create an error from a panic 112 | pub fn from_panic(panic_msg: Box) -> Self { 113 | let err_msg = if let Some(msg) = panic_msg.downcast_ref::() { 114 | msg.as_ref() 115 | } else if let Some(msg) = panic_msg.downcast_ref::<&str>() { 116 | msg 117 | } else { 118 | "unknown cause" 119 | }; 120 | 121 | let kind = if err_msg.contains("PoisonError") { 122 | ErrorKind::PoisonError 123 | } else { 124 | ErrorKind::PanicError 125 | }; 126 | 127 | format_err!(kind, err_msg).into() 128 | } 129 | } 130 | 131 | impl Deref for Error { 132 | type Target = Context; 133 | 134 | fn deref(&self) -> &Context { 135 | &self.0 136 | } 137 | } 138 | 139 | impl Display for Error { 140 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 141 | self.0.fmt(f) 142 | } 143 | } 144 | 145 | impl From for Error { 146 | fn from(kind: ErrorKind) -> Self { 147 | Context::new(kind, None).into() 148 | } 149 | } 150 | 151 | impl From> for Error { 152 | fn from(context: Context) -> Self { 153 | Error(Box::new(context)) 154 | } 155 | } 156 | 157 | impl From for Error { 158 | fn from(other: io::Error) -> Self { 159 | ErrorKind::IoError.context(other).into() 160 | } 161 | } 162 | 163 | impl std::error::Error for Error { 164 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 165 | self.0.source() 166 | } 167 | } 168 | 169 | impl From for Error { 170 | fn from(other: prost::DecodeError) -> Self { 171 | ErrorKind::ProtocolError.context(other).into() 172 | } 173 | } 174 | 175 | impl From for Error { 176 | fn from(other: prost::EncodeError) -> Self { 177 | ErrorKind::ProtocolError.context(other).into() 178 | } 179 | } 180 | 181 | impl From for Error { 182 | fn from(other: serde_json::error::Error) -> Self { 183 | ErrorKind::SerializationError.context(other).into() 184 | } 185 | } 186 | 187 | impl From for Error { 188 | fn from(other: signature::Error) -> Self { 189 | ErrorKind::CryptoError.context(other).into() 190 | } 191 | } 192 | 193 | impl From for Error { 194 | fn from(other: tendermint::error::Error) -> Self { 195 | ErrorKind::TendermintError.context(other).into() 196 | } 197 | } 198 | 199 | impl From for Error { 200 | fn from(other: chain::state::StateError) -> Self { 201 | ErrorKind::DoubleSign.context(other).into() 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/key_utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities 2 | 3 | use crate::{ 4 | error::{Error, ErrorKind::*}, 5 | keyring::ed25519, 6 | prelude::*, 7 | }; 8 | use k256::ecdsa; 9 | use rand_core::{OsRng, RngCore}; 10 | use std::{ 11 | fs::{self, OpenOptions}, 12 | io::Write, 13 | os::unix::fs::OpenOptionsExt, 14 | path::Path, 15 | }; 16 | use subtle_encoding::base64; 17 | use zeroize::Zeroizing; 18 | 19 | /// File permissions for secret data 20 | pub const SECRET_FILE_PERMS: u32 = 0o600; 21 | 22 | /// Load Base64-encoded secret data (i.e. key) from the given path 23 | pub fn load_base64_secret(path: impl AsRef) -> Result>, Error> { 24 | // TODO(tarcieri): check file permissions are correct 25 | let base64_data = Zeroizing::new(fs::read_to_string(path.as_ref()).map_err(|e| { 26 | format_err!( 27 | IoError, 28 | "couldn't read key from {}: {}", 29 | path.as_ref().display(), 30 | e 31 | ) 32 | })?); 33 | 34 | // TODO(tarcieri): constant-time string trimming 35 | let data = Zeroizing::new(base64::decode(base64_data.trim_end()).map_err(|e| { 36 | format_err!( 37 | IoError, 38 | "can't decode key from `{}`: {}", 39 | path.as_ref().display(), 40 | e 41 | ) 42 | })?); 43 | 44 | Ok(data) 45 | } 46 | 47 | /// Load a Base64-encoded Ed25519 secret key 48 | pub fn load_base64_ed25519_key(path: impl AsRef) -> Result { 49 | let key_bytes = load_base64_secret(path)?; 50 | 51 | Ok(ed25519::SigningKey::try_from(key_bytes.as_ref()) 52 | .map_err(|e| format_err!(InvalidKey, "invalid Ed25519 key: {}", e))?) 53 | } 54 | 55 | /// Load a Base64-encoded Secp256k1 secret key 56 | pub fn load_base64_secp256k1_key( 57 | path: impl AsRef, 58 | ) -> Result<(ecdsa::SigningKey, ecdsa::VerifyingKey), Error> { 59 | let key_bytes = load_base64_secret(path)?; 60 | 61 | let signing = ecdsa::SigningKey::try_from(key_bytes.as_slice()) 62 | .map_err(|e| format_err!(InvalidKey, "invalid ECDSA key: {}", e))?; 63 | 64 | let veryfing = ecdsa::VerifyingKey::from(&signing); 65 | 66 | Ok((signing, veryfing)) 67 | } 68 | 69 | /// Store Base64-encoded secret data at the given path 70 | pub fn write_base64_secret(path: impl AsRef, data: &[u8]) -> Result<(), Error> { 71 | let base64_data = Zeroizing::new(base64::encode(data)); 72 | 73 | OpenOptions::new() 74 | .create(true) 75 | .write(true) 76 | .truncate(true) 77 | .mode(SECRET_FILE_PERMS) 78 | .open(path.as_ref()) 79 | .and_then(|mut file| file.write_all(&base64_data)) 80 | .map_err(|e| { 81 | format_err!( 82 | IoError, 83 | "couldn't write `{}`: {}", 84 | path.as_ref().display(), 85 | e 86 | ) 87 | .into() 88 | }) 89 | } 90 | 91 | /// Generate a Secret Connection key at the given path 92 | pub fn generate_key(path: impl AsRef) -> Result<(), Error> { 93 | let mut secret_key = Zeroizing::new([0u8; ed25519::SigningKey::BYTE_SIZE]); 94 | OsRng.fill_bytes(&mut *secret_key); 95 | write_base64_secret(path, &*secret_key) 96 | } 97 | -------------------------------------------------------------------------------- /src/keyring.rs: -------------------------------------------------------------------------------- 1 | //! Signing keyring. Presently specialized for Ed25519 and ECDSA. 2 | 3 | pub mod ecdsa; 4 | pub mod ed25519; 5 | pub mod format; 6 | pub mod providers; 7 | pub mod signature; 8 | 9 | pub use self::{format::Format, providers::SigningProvider, signature::Signature}; 10 | use crate::{ 11 | chain, 12 | config::provider::ProviderConfig, 13 | error::{Error, ErrorKind::*}, 14 | prelude::*, 15 | Map, 16 | }; 17 | use tendermint::{account, TendermintKey}; 18 | 19 | /// File encoding for software-backed secret keys 20 | pub type SecretKeyEncoding = subtle_encoding::Base64; 21 | 22 | /// Signing keyring 23 | pub struct KeyRing { 24 | /// ECDSA keys in the keyring 25 | ecdsa_keys: Map, 26 | 27 | /// Ed25519 keys in the keyring 28 | ed25519_keys: Map, 29 | 30 | /// Formatting configuration when displaying keys (e.g. bech32) 31 | format: Format, 32 | } 33 | 34 | impl KeyRing { 35 | /// Create a new keyring 36 | pub fn new(format: Format) -> Self { 37 | Self { 38 | ecdsa_keys: Map::new(), 39 | ed25519_keys: Map::new(), 40 | format, 41 | } 42 | } 43 | 44 | /// Add na ECDSA key to the keyring, returning an error if we already have a 45 | /// signer registered for the given public key 46 | pub fn add_ecdsa(&mut self, signer: ecdsa::Signer) -> Result<(), Error> { 47 | let provider = signer.provider(); 48 | let public_key = signer.public_key(); 49 | let public_key_serialized = self.format.serialize(public_key); 50 | let key_type = match public_key { 51 | TendermintKey::AccountKey(_) => "account", 52 | TendermintKey::ConsensusKey(_) => unimplemented!( 53 | "ECDSA consensus keys unsupported: {:?}", 54 | public_key_serialized 55 | ), 56 | }; 57 | 58 | info!( 59 | "[keyring:{}] added {} ECDSA key: {}", 60 | provider, key_type, public_key_serialized 61 | ); 62 | 63 | if let Some(other) = self.ecdsa_keys.insert(public_key, signer) { 64 | fail!( 65 | InvalidKey, 66 | "[keyring:{}] duplicate key {} already registered as {}", 67 | provider, 68 | public_key_serialized, 69 | other.provider(), 70 | ) 71 | } else { 72 | Ok(()) 73 | } 74 | } 75 | 76 | /// Add a key to the keyring, returning an error if we already have a 77 | /// signer registered for the given public key 78 | pub fn add_ed25519(&mut self, signer: ed25519::Signer) -> Result<(), Error> { 79 | let provider = signer.provider(); 80 | let public_key = signer.public_key(); 81 | let public_key_serialized = self.format.serialize(public_key); 82 | let key_type = match public_key { 83 | TendermintKey::AccountKey(_) => unimplemented!( 84 | "Ed25519 account keys unsupported: {:?}", 85 | public_key_serialized 86 | ), 87 | TendermintKey::ConsensusKey(_) => "consensus", 88 | }; 89 | 90 | info!( 91 | "[keyring:{}] added {} Ed25519 key: {}", 92 | provider, key_type, public_key_serialized 93 | ); 94 | 95 | if let Some(other) = self.ed25519_keys.insert(public_key, signer) { 96 | fail!( 97 | InvalidKey, 98 | "[keyring:{}] duplicate key {} already registered as {}", 99 | provider, 100 | public_key_serialized, 101 | other.provider(), 102 | ) 103 | } else { 104 | Ok(()) 105 | } 106 | } 107 | 108 | /// Get the default Ed25519 (i.e. consensus) public key for this keyring 109 | pub fn default_pubkey(&self) -> Result { 110 | if !self.ed25519_keys.is_empty() { 111 | let mut keys = self.ed25519_keys.keys(); 112 | 113 | if keys.len() == 1 { 114 | Ok(*keys.next().unwrap()) 115 | } else { 116 | fail!(InvalidKey, "expected only one ed25519 key in keyring"); 117 | } 118 | } else if !self.ecdsa_keys.is_empty() { 119 | let mut keys = self.ecdsa_keys.keys(); 120 | 121 | if keys.len() == 1 { 122 | Ok(*keys.next().unwrap()) 123 | } else { 124 | fail!(InvalidKey, "expected only one ecdsa key in keyring"); 125 | } 126 | } else { 127 | fail!(InvalidKey, "keyring is empty"); 128 | } 129 | } 130 | 131 | /// Get ECDSA public key bytes for a given account ID 132 | pub fn get_account_pubkey(&self, account_id: account::Id) -> Option { 133 | for key in self.ecdsa_keys.keys() { 134 | if let TendermintKey::AccountKey(pk) = key { 135 | if account_id == account::Id::from(*pk) { 136 | return Some(*pk); 137 | } 138 | } 139 | } 140 | 141 | None 142 | } 143 | 144 | /// Sign a message using ECDSA 145 | pub fn sign_ecdsa( 146 | &self, 147 | account_id: account::Id, 148 | msg: &[u8], 149 | ) -> Result { 150 | for (key, signer) in &self.ecdsa_keys { 151 | if let TendermintKey::AccountKey(pk) = key { 152 | if account_id == account::Id::from(*pk) { 153 | return signer.sign(msg); 154 | } 155 | } 156 | } 157 | 158 | fail!( 159 | InvalidKey, 160 | "no ECDSA key in keyring for account ID: {}", 161 | account_id 162 | ) 163 | } 164 | 165 | /// Sign a message using the secret key associated with the given public key 166 | /// (if it is in our keyring) 167 | pub fn sign(&self, public_key: Option<&TendermintKey>, msg: &[u8]) -> Result { 168 | if self.ed25519_keys.len() > 1 || self.ecdsa_keys.len() > 1 { 169 | fail!(SigningError, "expected only one key in keyring"); 170 | } 171 | 172 | if !self.ed25519_keys.is_empty() { 173 | let signer = match public_key { 174 | Some(public_key) => self.ed25519_keys.get(public_key).ok_or_else(|| { 175 | format_err!( 176 | InvalidKey, 177 | "not in keyring: {}", 178 | match public_key { 179 | TendermintKey::AccountKey(pk) => pk.to_bech32(""), 180 | TendermintKey::ConsensusKey(pk) => pk.to_bech32(""), 181 | } 182 | ) 183 | }), 184 | None => self 185 | .ed25519_keys 186 | .values() 187 | .next() 188 | .ok_or_else(|| format_err!(InvalidKey, "ed25519 keyring is empty")), 189 | }?; 190 | 191 | Ok(Signature::Ed25519(signer.sign(msg)?)) 192 | } else if !self.ecdsa_keys.is_empty() { 193 | let signer = match public_key { 194 | Some(public_key) => self.ecdsa_keys.get(public_key).ok_or_else(|| { 195 | format_err!( 196 | InvalidKey, 197 | "not in keyring: {}", 198 | match public_key { 199 | TendermintKey::AccountKey(pk) => pk.to_bech32(""), 200 | TendermintKey::ConsensusKey(pk) => pk.to_bech32(""), 201 | } 202 | ) 203 | }), 204 | None => self 205 | .ecdsa_keys 206 | .values() 207 | .next() 208 | .ok_or_else(|| format_err!(InvalidKey, "ecdsa keyring is empty")), 209 | }?; 210 | 211 | Ok(Signature::Ecdsa(signer.sign(msg)?)) 212 | } else { 213 | Err(format_err!(InvalidKey, "keyring is empty").into()) 214 | } 215 | } 216 | } 217 | 218 | /// Initialize the keyring from the configuration file 219 | pub fn load_config(registry: &mut chain::Registry, config: &ProviderConfig) -> Result<(), Error> { 220 | #[cfg(feature = "softsign")] 221 | providers::softsign::init(registry, &config.softsign)?; 222 | 223 | #[cfg(feature = "yubihsm")] 224 | providers::yubihsm::init(registry, &config.yubihsm)?; 225 | 226 | #[cfg(feature = "ledger")] 227 | providers::ledgertm::init(registry, &config.ledgertm)?; 228 | 229 | #[cfg(feature = "fortanixdsm")] 230 | providers::fortanixdsm::init(registry, &config.fortanixdsm)?; 231 | 232 | Ok(()) 233 | } 234 | -------------------------------------------------------------------------------- /src/keyring/ecdsa.rs: -------------------------------------------------------------------------------- 1 | //! ECDSA keys 2 | 3 | pub use k256::{ecdsa::Signature, EncodedPoint as PublicKey}; 4 | 5 | use crate::{ 6 | error::{Error, ErrorKind::*}, 7 | keyring::SigningProvider, 8 | prelude::*, 9 | }; 10 | use std::sync::Arc; 11 | use tendermint::TendermintKey; 12 | 13 | #[allow(clippy::redundant_allocation)] 14 | 15 | /// ECDSA signer 16 | #[derive(Clone)] 17 | pub struct Signer { 18 | /// Provider for this signer 19 | provider: SigningProvider, 20 | 21 | /// Tendermint public key 22 | public_key: TendermintKey, 23 | 24 | /// Signer trait object 25 | signer: Arc + Send + Sync>>, 26 | } 27 | 28 | impl Signer { 29 | /// Create a new signer 30 | 31 | pub fn new( 32 | provider: SigningProvider, 33 | public_key: TendermintKey, 34 | signer: Box + Send + Sync>, 35 | ) -> Self { 36 | Self { 37 | provider, 38 | public_key, 39 | signer: Arc::new(signer), 40 | } 41 | } 42 | 43 | /// Get the Tendermint public key for this signer 44 | pub fn public_key(&self) -> TendermintKey { 45 | self.public_key 46 | } 47 | 48 | /// Get the provider for this signer 49 | pub fn provider(&self) -> SigningProvider { 50 | self.provider 51 | } 52 | 53 | /// Sign the given message using this signer 54 | pub fn sign(&self, msg: &[u8]) -> Result { 55 | Ok(self 56 | .signer 57 | .try_sign(msg) 58 | .map_err(|e| format_err!(SigningError, "{}", e))?) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/keyring/ed25519.rs: -------------------------------------------------------------------------------- 1 | //! Ed25519 signing keys 2 | 3 | mod signing_key; 4 | mod verifying_key; 5 | 6 | pub use self::{signing_key::SigningKey, verifying_key::VerifyingKey}; 7 | pub use ed25519::Signature; 8 | 9 | use crate::{ 10 | error::{Error, ErrorKind::*}, 11 | keyring::SigningProvider, 12 | prelude::*, 13 | }; 14 | use std::sync::Arc; 15 | use tendermint::TendermintKey; 16 | 17 | #[allow(clippy::redundant_allocation)] 18 | 19 | /// Ed25519 signer 20 | #[derive(Clone)] 21 | pub struct Signer { 22 | /// Provider for this signer 23 | provider: SigningProvider, 24 | 25 | /// Tendermint public key 26 | public_key: TendermintKey, 27 | 28 | /// Signer trait object 29 | signer: Arc + Send + Sync>>, 30 | } 31 | 32 | impl Signer { 33 | /// Create a new signer 34 | pub fn new( 35 | provider: SigningProvider, 36 | public_key: TendermintKey, 37 | signer: Box + Send + Sync>, 38 | ) -> Self { 39 | Self { 40 | provider, 41 | public_key, 42 | signer: Arc::new(signer), 43 | } 44 | } 45 | 46 | /// Get the Tendermint public key for this signer 47 | pub fn public_key(&self) -> TendermintKey { 48 | self.public_key 49 | } 50 | 51 | /// Get the provider for this signer 52 | pub fn provider(&self) -> SigningProvider { 53 | self.provider 54 | } 55 | 56 | /// Sign the given message using this signer 57 | pub fn sign(&self, msg: &[u8]) -> Result { 58 | Ok(self 59 | .signer 60 | .try_sign(msg) 61 | .map_err(|e| format_err!(SigningError, "{}", e))?) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/keyring/ed25519/signing_key.rs: -------------------------------------------------------------------------------- 1 | use super::{Signature, VerifyingKey}; 2 | use crate::error::{Error, ErrorKind}; 3 | use signature::Signer; 4 | 5 | /// Signing key serialized as bytes. 6 | type SigningKeyBytes = [u8; SigningKey::BYTE_SIZE]; 7 | 8 | /// Ed25519 signing key. 9 | #[derive(Clone, Debug)] 10 | pub struct SigningKey(ed25519_consensus::SigningKey); 11 | 12 | impl SigningKey { 13 | /// Size of an encoded Ed25519 signing key in bytes. 14 | pub const BYTE_SIZE: usize = 32; 15 | 16 | /// Borrow the serialized signing key as bytes. 17 | pub fn as_bytes(&self) -> &SigningKeyBytes { 18 | self.0.as_bytes() 19 | } 20 | 21 | /// Get the verifying key for this signing key. 22 | pub fn verifying_key(&self) -> VerifyingKey { 23 | VerifyingKey(self.0.verification_key()) 24 | } 25 | } 26 | 27 | impl From for SigningKey { 28 | fn from(bytes: SigningKeyBytes) -> Self { 29 | Self(bytes.into()) 30 | } 31 | } 32 | 33 | impl From for ed25519_consensus::SigningKey { 34 | fn from(signing_key: SigningKey) -> ed25519_consensus::SigningKey { 35 | signing_key.0 36 | } 37 | } 38 | 39 | impl From<&SigningKey> for tendermint_p2p::secret_connection::PublicKey { 40 | fn from(signing_key: &SigningKey) -> tendermint_p2p::secret_connection::PublicKey { 41 | Self::from(&signing_key.0) 42 | } 43 | } 44 | 45 | impl From for SigningKey { 46 | fn from(signing_key: ed25519_consensus::SigningKey) -> SigningKey { 47 | SigningKey(signing_key) 48 | } 49 | } 50 | 51 | impl From for SigningKey { 52 | fn from(signing_key: tendermint::private_key::Ed25519) -> SigningKey { 53 | signing_key 54 | .as_bytes() 55 | .try_into() 56 | .expect("invalid Ed25519 signing key") 57 | } 58 | } 59 | 60 | impl Signer for SigningKey { 61 | fn try_sign(&self, msg: &[u8]) -> signature::Result { 62 | Ok(self.0.sign(msg).to_bytes().into()) 63 | } 64 | } 65 | 66 | impl TryFrom<&[u8]> for SigningKey { 67 | type Error = Error; 68 | 69 | fn try_from(slice: &[u8]) -> Result { 70 | slice 71 | .try_into() 72 | .map(Self) 73 | .map_err(|_| ErrorKind::InvalidKey.into()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/keyring/ed25519/verifying_key.rs: -------------------------------------------------------------------------------- 1 | use super::Signature; 2 | use super::SigningKey; 3 | use crate::error::{Error, ErrorKind}; 4 | use signature::Verifier; 5 | 6 | /// Ed25519 verification key. 7 | #[derive(Clone, Debug)] 8 | pub struct VerifyingKey(pub(super) ed25519_consensus::VerificationKey); 9 | 10 | impl VerifyingKey { 11 | /// Size of an encoded Ed25519 verifying key in bytes. 12 | pub const BYTE_SIZE: usize = 32; 13 | 14 | /// Borrow the serialized verification key as bytes. 15 | pub fn as_bytes(&self) -> &[u8; Self::BYTE_SIZE] { 16 | self.0.as_bytes() 17 | } 18 | } 19 | 20 | impl From<&SigningKey> for VerifyingKey { 21 | fn from(signing_key: &SigningKey) -> VerifyingKey { 22 | signing_key.verifying_key() 23 | } 24 | } 25 | 26 | impl From for tendermint::PublicKey { 27 | fn from(verifying_key: VerifyingKey) -> tendermint::PublicKey { 28 | tendermint::PublicKey::from_raw_ed25519(verifying_key.as_bytes()) 29 | .expect("invalid Ed25519 key") 30 | } 31 | } 32 | 33 | impl From for tendermint_p2p::secret_connection::PublicKey { 34 | #[inline] 35 | fn from(verifying_key: VerifyingKey) -> tendermint_p2p::secret_connection::PublicKey { 36 | Self::from(&verifying_key) 37 | } 38 | } 39 | 40 | impl From<&VerifyingKey> for tendermint_p2p::secret_connection::PublicKey { 41 | fn from(verifying_key: &VerifyingKey) -> tendermint_p2p::secret_connection::PublicKey { 42 | verifying_key.0.into() 43 | } 44 | } 45 | 46 | impl Verifier for VerifyingKey { 47 | fn verify(&self, msg: &[u8], sig: &Signature) -> signature::Result<()> { 48 | let sig = ed25519_consensus::Signature::from(sig.to_bytes()); 49 | self.0 50 | .verify(&sig, msg) 51 | .map_err(|_| signature::Error::new()) 52 | } 53 | } 54 | 55 | impl TryFrom<&[u8]> for VerifyingKey { 56 | type Error = Error; 57 | 58 | fn try_from(slice: &[u8]) -> Result { 59 | slice 60 | .try_into() 61 | .map(Self) 62 | .map_err(|_| ErrorKind::InvalidKey.into()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/keyring/format.rs: -------------------------------------------------------------------------------- 1 | //! Chain-specific key configuration 2 | 3 | use cosmrs::crypto::PublicKey; 4 | use serde::Deserialize; 5 | use subtle_encoding::bech32; 6 | use tendermint::TendermintKey; 7 | 8 | /// Options for how keys for this chain are represented 9 | #[derive(Clone, Debug, Deserialize)] 10 | #[serde(tag = "type")] 11 | pub enum Format { 12 | /// Use the Bech32 serialization format with the given key prefixes 13 | #[serde(rename = "bech32")] 14 | Bech32 { 15 | /// Prefix to use for Account keys 16 | account_key_prefix: String, 17 | 18 | /// Prefix to use for Consensus keys 19 | consensus_key_prefix: String, 20 | }, 21 | 22 | /// JSON-encoded Cosmos protobuf representation of keys 23 | #[serde(rename = "cosmos-json")] 24 | CosmosJson, 25 | 26 | /// Hex is a baseline representation 27 | #[serde(rename = "hex")] 28 | Hex, 29 | } 30 | 31 | impl Format { 32 | /// Serialize a `TendermintKey` according to chain-specific rules 33 | pub fn serialize(&self, public_key: TendermintKey) -> String { 34 | match self { 35 | Format::Bech32 { 36 | account_key_prefix, 37 | consensus_key_prefix, 38 | } => match public_key { 39 | TendermintKey::AccountKey(pk) => { 40 | bech32::encode(account_key_prefix, tendermint::account::Id::from(pk)) 41 | } 42 | TendermintKey::ConsensusKey(pk) => pk.to_bech32(consensus_key_prefix), 43 | }, 44 | Format::CosmosJson => PublicKey::from(*public_key.public_key()).to_json(), 45 | Format::Hex => match public_key { 46 | TendermintKey::AccountKey(pk) => pk.to_hex(), 47 | TendermintKey::ConsensusKey(pk) => pk.to_hex(), 48 | }, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/keyring/providers.rs: -------------------------------------------------------------------------------- 1 | //! Signature providers (i.e. backends/plugins) 2 | 3 | #[cfg(feature = "ledger")] 4 | pub mod ledgertm; 5 | 6 | #[cfg(feature = "softsign")] 7 | pub mod softsign; 8 | 9 | #[cfg(feature = "yubihsm")] 10 | pub mod yubihsm; 11 | 12 | #[cfg(feature = "fortanixdsm")] 13 | pub mod fortanixdsm; 14 | 15 | use std::fmt::{self, Display}; 16 | 17 | /// Enumeration of signing key providers 18 | #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] 19 | pub enum SigningProvider { 20 | /// YubiHSM provider 21 | #[cfg(feature = "yubihsm")] 22 | Yubihsm, 23 | 24 | /// Ledger + Tendermint application 25 | #[cfg(feature = "ledger")] 26 | LedgerTm, 27 | 28 | /// Software signer (not intended for production use) 29 | #[cfg(feature = "softsign")] 30 | SoftSign, 31 | 32 | /// Fortanix DSM signer 33 | #[cfg(feature = "fortanixdsm")] 34 | FortanixDsm, 35 | } 36 | 37 | impl Display for SigningProvider { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | match self { 40 | #[cfg(feature = "yubihsm")] 41 | SigningProvider::Yubihsm => write!(f, "yubihsm"), 42 | 43 | #[cfg(feature = "ledger")] 44 | SigningProvider::LedgerTm => write!(f, "ledgertm"), 45 | 46 | #[cfg(feature = "softsign")] 47 | SigningProvider::SoftSign => write!(f, "softsign"), 48 | 49 | #[cfg(feature = "fortanixdsm")] 50 | SigningProvider::FortanixDsm => write!(f, "fortanixdsm"), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/keyring/providers/fortanixdsm.rs: -------------------------------------------------------------------------------- 1 | //! Fortanix DSM signing provider 2 | 3 | use crate::{ 4 | chain, 5 | config::provider::fortanixdsm::{FortanixDsmConfig, KeyDescriptor, SigningKeyConfig}, 6 | config::provider::KeyType, 7 | error::{Error, ErrorKind::*}, 8 | keyring::{self, ed25519, SigningProvider}, 9 | prelude::*, 10 | }; 11 | use elliptic_curve::pkcs8::{ 12 | spki::Error as SpkiError, DecodePublicKey, ObjectIdentifier, SubjectPublicKeyInfoRef, 13 | }; 14 | use elliptic_curve::PublicKey as EcPublicKey; 15 | use k256::ecdsa::{Error as SignError, Signature as EcdsaSignature}; 16 | use sdkms::api_model::{ 17 | DigestAlgorithm, EllipticCurve, ObjectType, SignRequest, SignResponse, SobjectDescriptor, 18 | }; 19 | use sdkms::{Error as SdkmsError, SdkmsClient}; 20 | use signature::Signer; 21 | use std::sync::Arc; 22 | use tendermint::public_key::{Ed25519, Secp256k1}; 23 | use tendermint::{PublicKey, TendermintKey}; 24 | use url::Url; 25 | 26 | /// Create Fortanix DSM backed signer objects from the given configuration 27 | pub fn init(registry: &mut chain::Registry, configs: &[FortanixDsmConfig]) -> Result<(), Error> { 28 | if configs.is_empty() { 29 | return Ok(()); 30 | } 31 | for config in configs { 32 | let client = make_sdkms_client(config)?; 33 | for key in &config.signing_keys { 34 | add_key(registry, key, client.clone())?; 35 | } 36 | } 37 | Ok(()) 38 | } 39 | 40 | fn make_sdkms_client(config: &FortanixDsmConfig) -> Result, Error> { 41 | let api_endpoint = Url::parse(&config.api_endpoint) 42 | .map_err(|e| format_err!(FortanixDsmError, "`api_endpoint` is not a valid URL: {}", e))?; 43 | if api_endpoint.scheme() != "https" { 44 | fail!( 45 | FortanixDsmError, 46 | "`api_endpoint` must be an `https` URL, found: `{}`", 47 | api_endpoint.scheme() 48 | ); 49 | } 50 | if api_endpoint.path() != "/" { 51 | fail!(FortanixDsmError, "`api_endpoint` must not have a path"); 52 | } 53 | if api_endpoint.query().is_some() || api_endpoint.fragment().is_some() { 54 | fail!( 55 | FortanixDsmError, 56 | "`api_endpoint` must not have query parameters or fragment" 57 | ); 58 | } 59 | let client = SdkmsClient::builder() 60 | .with_api_endpoint(&config.api_endpoint) 61 | .with_api_key(&config.api_key) 62 | .build() 63 | .map_err(|e| map_dsm_error("failed to create DSM client", e))?; 64 | 65 | Ok(Arc::new(client)) 66 | } 67 | 68 | fn map_dsm_error(ctx: &str, e: SdkmsError) -> Error { 69 | format_err!(FortanixDsmError, "{}: {}", ctx, e).into() 70 | } 71 | 72 | struct SigningKey { 73 | client: Arc, 74 | descriptor: SobjectDescriptor, 75 | elliptic_curve: EllipticCurve, 76 | } 77 | 78 | impl SigningKey { 79 | fn new( 80 | client: Arc, 81 | descriptor: KeyDescriptor, 82 | key_type: KeyType, 83 | ) -> Result<(Self, TendermintKey), Error> { 84 | let descriptor: SobjectDescriptor = descriptor.into(); 85 | let key = client 86 | .get_sobject(None, &descriptor) 87 | .map_err(|e| map_dsm_error("failed to get security object", e))?; 88 | 89 | let required_curve = match key_type { 90 | KeyType::Account => EllipticCurve::SecP256K1, 91 | KeyType::Consensus => EllipticCurve::Ed25519, 92 | }; 93 | 94 | if key.obj_type != ObjectType::Ec { 95 | fail!(FortanixDsmError, "expected an EC found {:?}", key.obj_type); 96 | } 97 | if key.elliptic_curve != Some(required_curve) { 98 | fail!( 99 | FortanixDsmError, 100 | "expected elliptic curve {:?}, found {:?}", 101 | required_curve, 102 | key.elliptic_curve 103 | ); 104 | } 105 | 106 | let public_key = key.pub_key.ok_or_else(|| { 107 | format_err!( 108 | FortanixDsmError, 109 | "could not find security object's public key" 110 | ) 111 | })?; 112 | 113 | let public_key = match key_type { 114 | KeyType::Account => { 115 | let pub_key: Secp256k1 = EcPublicKey::from_public_key_der(&public_key) 116 | .map_err(|e| { 117 | format_err!( 118 | FortanixDsmError, 119 | "failed to parse secp256k1 public key: {}", 120 | e 121 | ) 122 | })? 123 | .into(); 124 | TendermintKey::AccountKey(PublicKey::from(pub_key)) 125 | } 126 | KeyType::Consensus => { 127 | let pub_key = Ed25519PublicKey::from_public_key_der(&public_key).map_err(|e| { 128 | format_err!( 129 | FortanixDsmError, 130 | "failed to parse ed25519 public key: {}", 131 | e 132 | ) 133 | })?; 134 | TendermintKey::ConsensusKey(PublicKey::from(pub_key.0)) 135 | } 136 | }; 137 | 138 | Ok(( 139 | SigningKey { 140 | client, 141 | descriptor, 142 | elliptic_curve: required_curve, 143 | }, 144 | public_key, 145 | )) 146 | } 147 | 148 | fn sign(&self, msg: &[u8], hash_alg: DigestAlgorithm) -> Result { 149 | let req = SignRequest { 150 | key: Some(self.descriptor.clone()), 151 | data: Some(msg.to_owned().into()), 152 | hash_alg, 153 | hash: None, 154 | mode: None, 155 | deterministic_signature: None, 156 | }; 157 | self.client.sign(&req).map_err(SignError::from_source) 158 | } 159 | } 160 | 161 | impl Signer for SigningKey { 162 | fn try_sign(&self, msg: &[u8]) -> Result { 163 | assert_eq!(self.elliptic_curve, EllipticCurve::SecP256K1); 164 | let resp = self.sign(msg, DigestAlgorithm::Sha256)?; 165 | EcdsaSignature::from_der(&resp.signature) 166 | } 167 | } 168 | 169 | impl Signer for SigningKey { 170 | fn try_sign(&self, msg: &[u8]) -> Result { 171 | assert_eq!(self.elliptic_curve, EllipticCurve::Ed25519); 172 | let resp = self.sign(msg, DigestAlgorithm::Sha512)?; 173 | ed25519::Signature::from_slice(&resp.signature) 174 | } 175 | } 176 | 177 | fn add_key( 178 | registry: &mut chain::Registry, 179 | config: &SigningKeyConfig, 180 | client: Arc, 181 | ) -> Result<(), Error> { 182 | let (signing_key, public_key) = 183 | SigningKey::new(client, config.key.clone(), config.key_type.clone())?; 184 | 185 | match config.key_type { 186 | KeyType::Account => { 187 | let signer = keyring::ecdsa::Signer::new( 188 | SigningProvider::FortanixDsm, 189 | public_key, 190 | Box::new(signing_key), 191 | ); 192 | for chain_id in &config.chain_ids { 193 | registry.add_account_key(chain_id, signer.clone())?; 194 | } 195 | } 196 | KeyType::Consensus => { 197 | let signer = ed25519::Signer::new( 198 | SigningProvider::FortanixDsm, 199 | public_key, 200 | Box::new(signing_key), 201 | ); 202 | for chain_id in &config.chain_ids { 203 | registry.add_consensus_key(chain_id, signer.clone())?; 204 | } 205 | } 206 | } 207 | 208 | Ok(()) 209 | } 210 | 211 | // See RFC 8410 section 3 212 | const ED_25519_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112"); 213 | 214 | struct Ed25519PublicKey(Ed25519); 215 | 216 | impl TryFrom> for Ed25519PublicKey { 217 | type Error = SpkiError; 218 | 219 | fn try_from(spki: SubjectPublicKeyInfoRef<'_>) -> Result { 220 | spki.algorithm.assert_algorithm_oid(ED_25519_OID)?; 221 | 222 | if spki.algorithm.parameters.is_some() { 223 | // TODO: once/if https://github.com/RustCrypto/formats/issues/354 is addressed we should use that error variant. 224 | return Err(SpkiError::KeyMalformed); 225 | } 226 | 227 | Ed25519::try_from(spki.subject_public_key.as_bytes().unwrap()) 228 | .map_err(|_| SpkiError::KeyMalformed) 229 | .map(Ed25519PublicKey) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/keyring/providers/ledgertm.rs: -------------------------------------------------------------------------------- 1 | //! Ledger Tendermint signer 2 | 3 | mod client; 4 | mod error; 5 | mod signer; 6 | 7 | use self::signer::Ed25519LedgerTmAppSigner; 8 | use crate::{ 9 | chain, 10 | config::provider::ledgertm::LedgerTendermintConfig, 11 | error::{Error, ErrorKind::*}, 12 | keyring::{ 13 | ed25519::{self, Signer}, 14 | SigningProvider, 15 | }, 16 | prelude::*, 17 | }; 18 | use tendermint::{PublicKey, TendermintKey}; 19 | 20 | /// Create Ledger Tendermint signer object from the given configuration 21 | pub fn init( 22 | chain_registry: &mut chain::Registry, 23 | ledgertm_configs: &[LedgerTendermintConfig], 24 | ) -> Result<(), Error> { 25 | if ledgertm_configs.is_empty() { 26 | return Ok(()); 27 | } 28 | 29 | if ledgertm_configs.len() != 1 { 30 | fail!( 31 | ConfigError, 32 | "expected one [providers.ledgertm] in config, found: {}", 33 | ledgertm_configs.len() 34 | ); 35 | } 36 | 37 | let provider = Ed25519LedgerTmAppSigner::connect().map_err(|_| Error::from(SigningError))?; 38 | 39 | let public_key = PublicKey::from_raw_ed25519(ed25519::VerifyingKey::from(&provider).as_bytes()) 40 | .expect("invalid Ed25519 public key"); 41 | 42 | let signer = Signer::new( 43 | SigningProvider::LedgerTm, 44 | TendermintKey::ConsensusKey(public_key), 45 | Box::new(provider), 46 | ); 47 | 48 | for chain_id in &ledgertm_configs[0].chain_ids { 49 | chain_registry.add_consensus_key(chain_id, signer.clone())?; 50 | } 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /src/keyring/providers/ledgertm/error.rs: -------------------------------------------------------------------------------- 1 | //! Ledger errors 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | #[error("This version is not supported")] 8 | InvalidVersion, 9 | 10 | #[error("message cannot be empty")] 11 | InvalidEmptyMessage, 12 | 13 | #[error("message size is invalid (too big)")] 14 | InvalidMessageSize, 15 | 16 | #[error("received an invalid PK")] 17 | InvalidPk, 18 | 19 | #[error("received no signature back")] 20 | NoSignature, 21 | 22 | #[error("received an invalid signature")] 23 | InvalidSignature, 24 | 25 | #[error("ledger error")] 26 | Ledger(ledger::Error), 27 | } 28 | 29 | impl From for Error { 30 | fn from(err: ledger::Error) -> Error { 31 | Error::Ledger(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/keyring/providers/ledgertm/signer.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * (c) 2018, 2019 ZondaX GmbH 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ********************************************************************************/ 16 | 17 | use super::client::TendermintValidatorApp; 18 | use crate::keyring::ed25519::{Signature, VerifyingKey}; 19 | use signature::{Error, Signer}; 20 | use std::sync::{Arc, Mutex}; 21 | 22 | /// ed25519 signature provider for the Ledger Tendermint Validator app 23 | pub(super) struct Ed25519LedgerTmAppSigner { 24 | app: Arc>, 25 | } 26 | 27 | impl Ed25519LedgerTmAppSigner { 28 | /// Create a new Ed25519 signer based on Ledger Nano S - Tendermint Validator app 29 | pub fn connect() -> Result { 30 | let validator_app = TendermintValidatorApp::connect().map_err(Error::from_source)?; 31 | let app = Arc::new(Mutex::new(validator_app)); 32 | Ok(Ed25519LedgerTmAppSigner { app }) 33 | } 34 | } 35 | 36 | impl From<&Ed25519LedgerTmAppSigner> for VerifyingKey { 37 | /// Returns the public key that corresponds to the Tendermint Validator app connected to this signer 38 | fn from(signer: &Ed25519LedgerTmAppSigner) -> VerifyingKey { 39 | let app = signer.app.lock().unwrap(); 40 | VerifyingKey::try_from(app.public_key().unwrap().as_ref()) 41 | .expect("invalid Ed25519 public key") 42 | } 43 | } 44 | 45 | impl Signer for Ed25519LedgerTmAppSigner { 46 | /// c: Compute a compact, fixed-sized signature of the given amino/json vote 47 | fn try_sign(&self, msg: &[u8]) -> Result { 48 | let app = self.app.lock().unwrap(); 49 | let sig = app.sign(msg).map_err(Error::from_source)?; 50 | Ok(Signature::from(sig)) 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::{Ed25519LedgerTmAppSigner, VerifyingKey}; 57 | use signature::Signer; 58 | 59 | #[test] 60 | #[ignore] 61 | fn public_key() { 62 | let signer = Ed25519LedgerTmAppSigner::connect().unwrap(); 63 | let pk = VerifyingKey::from(&signer); 64 | println!("PK {pk:0X?}"); 65 | } 66 | 67 | #[test] 68 | #[ignore] 69 | fn sign() { 70 | let signer = Ed25519LedgerTmAppSigner::connect().unwrap(); 71 | 72 | // Sign message1 73 | let some_message1 = [ 74 | 33, 0x8, // (field_number << 3) | wire_type 75 | 0x1, // PrevoteType 76 | 0x11, // (field_number << 3) | wire_type 77 | 0x10, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height 78 | 0x19, // (field_number << 3) | wire_type 79 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round 80 | 0x22, // (field_number << 3) | wire_type 81 | // remaining fields (timestamp): 82 | 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1, 83 | ]; 84 | 85 | signer.sign(&some_message1); 86 | } 87 | 88 | #[test] 89 | #[ignore] 90 | fn sign2() { 91 | let signer = Ed25519LedgerTmAppSigner::connect().unwrap(); 92 | 93 | // Sign message1 94 | let some_message1 = [ 95 | 33, 0x8, // (field_number << 3) | wire_type 96 | 0x1, // PrevoteType 97 | 0x11, // (field_number << 3) | wire_type 98 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height 99 | 0x19, // (field_number << 3) | wire_type 100 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round 101 | 0x22, // (field_number << 3) | wire_type 102 | // remaining fields (timestamp): 103 | 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1, 104 | ]; 105 | 106 | signer.sign(&some_message1); 107 | 108 | // Sign message2 109 | let some_message2 = [ 110 | 33, 0x8, // (field_number << 3) | wire_type 111 | 0x1, // PrevoteType 112 | 0x11, // (field_number << 3) | wire_type 113 | 0x10, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height 114 | 0x19, // (field_number << 3) | wire_type 115 | 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round 116 | 0x22, // (field_number << 3) | wire_type 117 | // remaining fields (timestamp): 118 | 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1, 119 | ]; 120 | 121 | signer.sign(&some_message2); 122 | } 123 | 124 | #[test] 125 | #[ignore] 126 | fn sign_many() { 127 | let signer = Ed25519LedgerTmAppSigner::connect().unwrap(); 128 | 129 | // Get public key to initialize 130 | let pk = VerifyingKey::from(&signer); 131 | println!("PK {pk:0X?}"); 132 | 133 | for index in 50u8..254u8 { 134 | // Sign message1 135 | let some_message = [ 136 | 33, 0x8, // (field_number << 3) | wire_type 137 | 0x1, // PrevoteType 138 | 0x11, // (field_number << 3) | wire_type 139 | 0x40, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height 140 | 0x19, // (field_number << 3) | wire_type 141 | index, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round 142 | 0x22, // (field_number << 3) | wire_type 143 | // remaining fields (timestamp): 144 | 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1, 145 | ]; 146 | 147 | signer.sign(&some_message); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/keyring/providers/softsign.rs: -------------------------------------------------------------------------------- 1 | //! ed25519-dalek software-based signer 2 | //! 3 | //! This is mainly intended for testing/CI. Ideally real validators will use HSMs. 4 | 5 | use crate::{ 6 | chain, 7 | config::provider::{ 8 | softsign::{KeyFormat, SoftsignConfig}, 9 | KeyType, 10 | }, 11 | error::{Error, ErrorKind::*}, 12 | key_utils, 13 | keyring::{self, ed25519, SigningProvider}, 14 | prelude::*, 15 | }; 16 | use k256::ecdsa; 17 | use tendermint::{PrivateKey, TendermintKey}; 18 | use tendermint_config::PrivValidatorKey; 19 | 20 | /// Create software-backed Ed25519 signer objects from the given configuration 21 | pub fn init(chain_registry: &mut chain::Registry, configs: &[SoftsignConfig]) -> Result<(), Error> { 22 | if configs.is_empty() { 23 | return Ok(()); 24 | } 25 | 26 | let mut loaded_consensus_key = false; 27 | 28 | for config in configs { 29 | match config.key_type { 30 | KeyType::Account => { 31 | let signer = load_secp256k1_key(config)?; 32 | let public_key = tendermint::PublicKey::from_raw_secp256k1( 33 | &signer.verifying_key().to_sec1_bytes(), 34 | ) 35 | .unwrap(); 36 | 37 | let account_pubkey = TendermintKey::AccountKey(public_key); 38 | 39 | let signer = keyring::ecdsa::Signer::new( 40 | SigningProvider::SoftSign, 41 | account_pubkey, 42 | Box::new(signer), 43 | ); 44 | 45 | for chain_id in &config.chain_ids { 46 | chain_registry.add_account_key(chain_id, signer.clone())?; 47 | } 48 | } 49 | KeyType::Consensus => { 50 | if loaded_consensus_key { 51 | fail!( 52 | ConfigError, 53 | "only one [[providers.softsign]] consensus key allowed" 54 | ); 55 | } 56 | 57 | loaded_consensus_key = true; 58 | 59 | let signing_key = load_ed25519_key(config)?; 60 | let consensus_pubkey = 61 | TendermintKey::ConsensusKey(signing_key.verifying_key().into()); 62 | 63 | let signer = ed25519::Signer::new( 64 | SigningProvider::SoftSign, 65 | consensus_pubkey, 66 | Box::new(signing_key), 67 | ); 68 | 69 | for chain_id in &config.chain_ids { 70 | chain_registry.add_consensus_key(chain_id, signer.clone())?; 71 | } 72 | } 73 | } 74 | } 75 | 76 | Ok(()) 77 | } 78 | 79 | /// Load an Ed25519 key according to the provided configuration 80 | fn load_ed25519_key(config: &SoftsignConfig) -> Result { 81 | let key_format = config.key_format.as_ref().cloned().unwrap_or_default(); 82 | 83 | match key_format { 84 | KeyFormat::Base64 => key_utils::load_base64_ed25519_key(&config.path), 85 | KeyFormat::Json => { 86 | let private_key = PrivValidatorKey::load_json_file(&config.path) 87 | .map_err(|e| { 88 | format_err!( 89 | ConfigError, 90 | "couldn't load `{}`: {}", 91 | config.path.as_ref().display(), 92 | e 93 | ) 94 | })? 95 | .priv_key; 96 | 97 | if let PrivateKey::Ed25519(pk) = private_key { 98 | Ok(pk.into()) 99 | } else { 100 | unreachable!("unsupported priv_validator.json algorithm"); 101 | } 102 | } 103 | } 104 | } 105 | 106 | /// Load a secp256k1 (ECDSA) key according to the provided configuration 107 | fn load_secp256k1_key(config: &SoftsignConfig) -> Result { 108 | if config.key_format.unwrap_or_default() != KeyFormat::Base64 { 109 | fail!( 110 | ConfigError, 111 | "[[providers.softsign]] account keys must be `base64` encoded" 112 | ); 113 | } 114 | 115 | let key_bytes = key_utils::load_base64_secret(&config.path)?; 116 | 117 | let secret_key = ecdsa::SigningKey::try_from(key_bytes.as_slice()).map_err(|e| { 118 | format_err!( 119 | ConfigError, 120 | "can't decode account key base64 from {}: {}", 121 | config.path.as_ref().display(), 122 | e 123 | ) 124 | })?; 125 | 126 | Ok(secret_key) 127 | } 128 | -------------------------------------------------------------------------------- /src/keyring/providers/yubihsm.rs: -------------------------------------------------------------------------------- 1 | //! YubiHSM2 signing provider 2 | 3 | use crate::{ 4 | chain, 5 | config::provider::{ 6 | yubihsm::{SigningKeyConfig, YubihsmConfig}, 7 | KeyType, 8 | }, 9 | error::{Error, ErrorKind::*}, 10 | keyring::{self, SigningProvider}, 11 | prelude::*, 12 | }; 13 | use tendermint::TendermintKey; 14 | 15 | /// Create hardware-backed YubiHSM signer objects from the given configuration 16 | pub fn init( 17 | chain_registry: &mut chain::Registry, 18 | yubihsm_configs: &[YubihsmConfig], 19 | ) -> Result<(), Error> { 20 | if yubihsm_configs.is_empty() { 21 | return Ok(()); 22 | } 23 | 24 | // TODO(tarcieri): support for multiple YubiHSMs per host? 25 | if yubihsm_configs.len() != 1 { 26 | fail!( 27 | ConfigError, 28 | "expected one [yubihsm.provider] in config, found: {}", 29 | yubihsm_configs.len() 30 | ); 31 | } 32 | 33 | for config in &yubihsm_configs[0].keys { 34 | match config.key_type { 35 | KeyType::Account => add_account_key(chain_registry, config)?, 36 | KeyType::Consensus => add_consensus_key(chain_registry, config)?, 37 | } 38 | } 39 | 40 | Ok(()) 41 | } 42 | 43 | /// Add an account key (ECDSA/secp256k1) to the keychain 44 | fn add_account_key( 45 | chain_registry: &mut chain::Registry, 46 | config: &SigningKeyConfig, 47 | ) -> Result<(), Error> { 48 | let signer = yubihsm::ecdsa::Signer::create(crate::yubihsm::client().clone(), config.key) 49 | .map_err(|_| { 50 | format_err!( 51 | InvalidKey, 52 | "YubiHSM key ID 0x{:04x} is not a valid ECDSA signing key", 53 | config.key 54 | ) 55 | })?; 56 | 57 | let public_key = 58 | tendermint::PublicKey::from_raw_secp256k1(signer.public_key().compress().as_bytes()) 59 | .expect("invalid secp256k1 key"); 60 | 61 | let signer = keyring::ecdsa::Signer::new( 62 | SigningProvider::Yubihsm, 63 | TendermintKey::AccountKey(public_key), 64 | Box::new(signer), 65 | ); 66 | 67 | for chain_id in &config.chain_ids { 68 | chain_registry.add_account_key(chain_id, signer.clone())?; 69 | } 70 | 71 | Ok(()) 72 | } 73 | 74 | /// Add a consensus key (Ed25519) to the keychain 75 | fn add_consensus_key( 76 | chain_registry: &mut chain::Registry, 77 | config: &SigningKeyConfig, 78 | ) -> Result<(), Error> { 79 | let signer = yubihsm::ed25519::Signer::create(crate::yubihsm::client().clone(), config.key) 80 | .map_err(|_| { 81 | format_err!( 82 | InvalidKey, 83 | "YubiHSM key ID 0x{:04x} is not a valid Ed25519 signing key", 84 | config.key 85 | ) 86 | })?; 87 | 88 | let public_key = tendermint::PublicKey::from_raw_ed25519(signer.public_key().as_bytes()) 89 | .expect("invalid Ed25519 key"); 90 | 91 | let signer = keyring::ed25519::Signer::new( 92 | SigningProvider::Yubihsm, 93 | TendermintKey::ConsensusKey(public_key), 94 | Box::new(signer), 95 | ); 96 | 97 | for chain_id in &config.chain_ids { 98 | chain_registry.add_consensus_key(chain_id, signer.clone())?; 99 | } 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /src/keyring/signature.rs: -------------------------------------------------------------------------------- 1 | //! Signing signature 2 | 3 | pub use super::ed25519; 4 | pub use k256::ecdsa; 5 | 6 | /// Cryptographic signature used for block signing 7 | pub enum Signature { 8 | /// ECDSA signature (e.g secp256k1) 9 | Ecdsa(ecdsa::Signature), 10 | 11 | /// ED25519 signature 12 | Ed25519(ed25519::Signature), 13 | } 14 | 15 | impl Signature { 16 | /// Serialize this signature as a byte vector. 17 | pub fn to_vec(&self) -> Vec { 18 | match self { 19 | Self::Ecdsa(sig) => sig.to_vec(), 20 | Self::Ed25519(sig) => sig.to_vec(), 21 | } 22 | } 23 | } 24 | 25 | impl From for Signature { 26 | fn from(sig: ecdsa::Signature) -> Signature { 27 | Self::Ecdsa(sig) 28 | } 29 | } 30 | 31 | impl From for Signature { 32 | fn from(sig: ed25519::Signature) -> Signature { 33 | Self::Ed25519(sig) 34 | } 35 | } 36 | 37 | impl From for tendermint::Signature { 38 | fn from(sig: Signature) -> tendermint::Signature { 39 | sig.to_vec().try_into().expect("signature should be valid") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Tendermint Key Management System 2 | 3 | #![deny(unsafe_code)] 4 | #![warn(missing_docs, rust_2018_idioms, unused_qualifications)] 5 | 6 | #[cfg(not(any( 7 | feature = "softsign", 8 | feature = "yubihsm", 9 | feature = "ledger", 10 | feature = "fortanixdsm" 11 | )))] 12 | compile_error!( 13 | "please enable one of the following backends with cargo's --features argument: \ 14 | yubihsm, ledgertm, softsign, fortanixdsm (e.g. --features=yubihsm)" 15 | ); 16 | 17 | pub mod application; 18 | pub mod chain; 19 | pub mod client; 20 | pub mod commands; 21 | pub mod config; 22 | pub mod connection; 23 | pub mod error; 24 | pub mod key_utils; 25 | pub mod keyring; 26 | pub mod prelude; 27 | pub mod privval; 28 | pub mod rpc; 29 | pub mod session; 30 | 31 | #[cfg(feature = "yubihsm")] 32 | pub mod yubihsm; 33 | 34 | pub use crate::application::KmsApplication; 35 | 36 | // Map type used within this application 37 | use std::collections::BTreeMap as Map; 38 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Application-local prelude: conveniently import types/functions/macros 2 | //! which are generally useful and should be available everywhere. 3 | 4 | /// Abscissa core prelude 5 | pub use abscissa_core::prelude::*; 6 | 7 | /// Status macros 8 | pub use abscissa_core::{status_attr_err, status_attr_ok}; 9 | 10 | /// Application state 11 | pub use crate::application::APP; 12 | -------------------------------------------------------------------------------- /src/rpc.rs: -------------------------------------------------------------------------------- 1 | //! Remote Procedure Calls 2 | 3 | // TODO: docs for everything 4 | #![allow(missing_docs)] 5 | 6 | use crate::privval::SignableMsg; 7 | use prost::Message as _; 8 | use std::io::Read; 9 | use tendermint::{chain, Proposal, Vote}; 10 | use tendermint_p2p::secret_connection::DATA_MAX_SIZE; 11 | use tendermint_proto as proto; 12 | 13 | use crate::{ 14 | error::{Error, ErrorKind}, 15 | prelude::*, 16 | }; 17 | 18 | /// RPC requests to the KMS 19 | #[derive(Debug)] 20 | pub enum Request { 21 | /// Sign the given message 22 | SignProposal(Proposal), 23 | SignVote(Vote), 24 | ShowPublicKey, 25 | PingRequest, 26 | } 27 | 28 | impl Request { 29 | /// Read a request from the given readable. 30 | pub fn read(conn: &mut impl Read, expected_chain_id: &chain::Id) -> Result { 31 | let mut msg_bytes: Vec = vec![]; 32 | let msg; 33 | 34 | // fix for Sei: collect incoming bytes of Protobuf from incoming msg 35 | loop { 36 | let mut msg_chunk = read_msg(conn)?; 37 | let chunk_len = msg_chunk.len(); 38 | msg_bytes.append(&mut msg_chunk); 39 | 40 | // if we can decode it, great, break the loop 41 | match proto::privval::Message::decode_length_delimited(msg_bytes.as_ref()) { 42 | Ok(m) => { 43 | msg = m.sum; 44 | break; 45 | } 46 | Err(e) => { 47 | // if chunk_len < DATA_MAX_SIZE (1024) we assume it was the end of the message and it is malformed 48 | if chunk_len < DATA_MAX_SIZE { 49 | return Err(format_err!( 50 | ErrorKind::ProtocolError, 51 | "malformed message packet: {}", 52 | e 53 | ) 54 | .into()); 55 | } 56 | // otherwise, we go to start of the loop assuming next chunk(s) 57 | // will fill the message 58 | } 59 | } 60 | } 61 | 62 | let (req, chain_id) = match msg { 63 | Some(proto::privval::message::Sum::SignVoteRequest( 64 | proto::privval::SignVoteRequest { 65 | vote: Some(vote), 66 | chain_id, 67 | }, 68 | )) => (Request::SignVote(vote.try_into()?), chain_id), 69 | Some(proto::privval::message::Sum::SignProposalRequest( 70 | proto::privval::SignProposalRequest { 71 | proposal: Some(proposal), 72 | chain_id, 73 | }, 74 | )) => (Request::SignProposal(proposal.try_into()?), chain_id), 75 | Some(proto::privval::message::Sum::PubKeyRequest(req)) => { 76 | (Request::ShowPublicKey, req.chain_id) 77 | } 78 | Some(proto::privval::message::Sum::PingRequest(_)) => { 79 | return Ok(Request::PingRequest); 80 | } 81 | _ => fail!(ErrorKind::ProtocolError, "invalid RPC message: {:?}", msg), 82 | }; 83 | 84 | ensure!( 85 | expected_chain_id == &chain::Id::try_from(chain_id.as_str())?, 86 | ErrorKind::ChainIdError, 87 | "got unexpected chain ID: {} (expecting: {})", 88 | &chain_id, 89 | expected_chain_id 90 | ); 91 | 92 | Ok(req) 93 | } 94 | 95 | /// Convert this request into a [`SignableMsg`]. 96 | /// 97 | /// The expected `chain::Id` is used to validate the request. 98 | pub fn into_signable_msg(self) -> Result { 99 | match self { 100 | Self::SignProposal(proposal) => Ok(proposal.into()), 101 | Self::SignVote(vote) => Ok(vote.into()), 102 | _ => fail!( 103 | ErrorKind::InvalidMessageError, 104 | "expected a signable message type: {:?}", 105 | self 106 | ), 107 | } 108 | } 109 | } 110 | 111 | /// RPC responses from the KMS 112 | #[derive(Debug)] 113 | pub enum Response { 114 | /// Signature response 115 | SignedVote(proto::privval::SignedVoteResponse), 116 | SignedProposal(proto::privval::SignedProposalResponse), 117 | Ping(proto::privval::PingResponse), 118 | PublicKey(proto::privval::PubKeyResponse), 119 | } 120 | 121 | impl Response { 122 | /// Encode response to bytes. 123 | pub fn encode(self) -> Result, Error> { 124 | let mut buf = Vec::new(); 125 | let msg = match self { 126 | Response::SignedVote(resp) => proto::privval::message::Sum::SignedVoteResponse(resp), 127 | Response::SignedProposal(resp) => { 128 | proto::privval::message::Sum::SignedProposalResponse(resp) 129 | } 130 | Response::Ping(resp) => proto::privval::message::Sum::PingResponse(resp), 131 | Response::PublicKey(resp) => proto::privval::message::Sum::PubKeyResponse(resp), 132 | }; 133 | proto::privval::Message { sum: Some(msg) }.encode_length_delimited(&mut buf)?; 134 | Ok(buf) 135 | } 136 | 137 | /// Construct an error response for a given [`SignableMsg`]. 138 | pub fn error(msg: SignableMsg, error: proto::privval::RemoteSignerError) -> Response { 139 | match msg { 140 | SignableMsg::Proposal(_) => { 141 | Response::SignedProposal(proto::privval::SignedProposalResponse { 142 | proposal: None, 143 | error: Some(error), 144 | }) 145 | } 146 | SignableMsg::Vote(_) => Response::SignedVote(proto::privval::SignedVoteResponse { 147 | vote: None, 148 | error: Some(error), 149 | }), 150 | } 151 | } 152 | } 153 | 154 | impl From for Response { 155 | fn from(msg: SignableMsg) -> Response { 156 | match msg { 157 | SignableMsg::Proposal(proposal) => { 158 | Response::SignedProposal(proto::privval::SignedProposalResponse { 159 | proposal: Some(proposal.into()), 160 | error: None, 161 | }) 162 | } 163 | SignableMsg::Vote(vote) => Response::SignedVote(proto::privval::SignedVoteResponse { 164 | vote: Some(vote.into()), 165 | error: None, 166 | }), 167 | } 168 | } 169 | } 170 | 171 | /// Read a message from a Secret Connection 172 | // TODO(tarcieri): extract this into Secret Connection 173 | fn read_msg(conn: &mut impl Read) -> Result, Error> { 174 | let mut buf = vec![0; DATA_MAX_SIZE]; 175 | let buf_read = conn.read(&mut buf)?; 176 | buf.truncate(buf_read); 177 | Ok(buf) 178 | } 179 | -------------------------------------------------------------------------------- /src/yubihsm.rs: -------------------------------------------------------------------------------- 1 | //! Application-local YubiHSM configuration and initialization 2 | 3 | use crate::{ 4 | config::provider::yubihsm::YubihsmConfig, 5 | error::{Error, ErrorKind}, 6 | prelude::*, 7 | }; 8 | use once_cell::sync::Lazy; 9 | #[cfg(all(feature = "yubihsm-server", not(feature = "yubihsm-mock")))] 10 | use std::thread; 11 | use std::{ 12 | process, 13 | sync::{ 14 | atomic::{self, AtomicBool}, 15 | Mutex, MutexGuard, 16 | }, 17 | }; 18 | use yubihsm::{Client, Connector}; 19 | 20 | #[cfg(feature = "yubihsm-server")] 21 | use zeroize::Zeroizing; 22 | 23 | #[cfg(not(feature = "yubihsm-mock"))] 24 | use { 25 | crate::config::provider::yubihsm::AdapterConfig, 26 | tendermint_config::net, 27 | yubihsm::{device::SerialNumber, HttpConfig, UsbConfig}, 28 | }; 29 | 30 | /// Connection to the YubiHSM device 31 | // TODO(tarcieri): refactor with a straightforward `once_cell::sync::OnceCell` 32 | static HSM_CONNECTOR: Lazy = Lazy::new(init_connector); 33 | 34 | /// Authenticated client connection to the YubiHSM device 35 | // TODO(tarcieri): refactor with a straightforward `once_cell::sync::OnceCell` 36 | static HSM_CLIENT: Lazy> = Lazy::new(|| Mutex::new(init_client())); 37 | 38 | /// Flag indicating we're inside of a `tmkms yubihsm` command 39 | // TODO(tarcieri): refactor with a straightforward `once_cell::sync::OnceCell` 40 | static CLI_COMMAND: AtomicBool = AtomicBool::new(false); 41 | 42 | /// Mark that we're in a `tmkms yubihsm` command when initializing the YubiHSM 43 | pub(crate) fn mark_cli_command() { 44 | CLI_COMMAND.store(true, atomic::Ordering::SeqCst); 45 | } 46 | 47 | /// Are we running a `tmkms yubihsm` subcommand? 48 | #[cfg(feature = "yubihsm-server")] 49 | fn is_cli_command() -> bool { 50 | CLI_COMMAND.load(atomic::Ordering::SeqCst) 51 | } 52 | 53 | /// Get the global HSM connector configured from global settings 54 | pub fn connector() -> &'static Connector { 55 | &HSM_CONNECTOR 56 | } 57 | 58 | /// Get an HSM client configured from global settings 59 | pub fn client() -> MutexGuard<'static, Client> { 60 | HSM_CLIENT.lock().unwrap() 61 | } 62 | 63 | /// Open a session with the YubiHSM2 using settings from the global config 64 | #[cfg(not(feature = "yubihsm-mock"))] 65 | fn init_connector() -> Connector { 66 | let cfg = config(); 67 | let serial_number = cfg 68 | .serial_number 69 | .as_ref() 70 | .map(|serial| serial.parse::().unwrap()); 71 | 72 | // Use CLI overrides if enabled and we're in a CLI context 73 | #[cfg(feature = "yubihsm-server")] 74 | { 75 | if let Some(ref connector_server) = cfg.connector_server { 76 | if connector_server.cli.is_some() && is_cli_command() { 77 | let adapter = AdapterConfig::Http { 78 | addr: connector_server.laddr.clone(), 79 | }; 80 | 81 | return init_connector_adapter(&adapter, serial_number); 82 | } 83 | } 84 | } 85 | 86 | let connector = init_connector_adapter(&cfg.adapter, serial_number); 87 | 88 | // Start connector server if configured 89 | #[cfg(feature = "yubihsm-server")] 90 | { 91 | if let Some(ref connector_server) = cfg.connector_server { 92 | run_connnector_server( 93 | http_config_for_address(&connector_server.laddr), 94 | connector.clone(), 95 | ) 96 | } 97 | } 98 | 99 | connector 100 | } 101 | 102 | /// Initialize a connector from the given adapter 103 | #[cfg(not(feature = "yubihsm-mock"))] 104 | fn init_connector_adapter(adapter: &AdapterConfig, serial: Option) -> Connector { 105 | match *adapter { 106 | AdapterConfig::Http { ref addr } => connect_http(addr), 107 | AdapterConfig::Usb { timeout_ms } => { 108 | let usb_config = UsbConfig { serial, timeout_ms }; 109 | 110 | Connector::usb(&usb_config) 111 | } 112 | } 113 | } 114 | 115 | /// Convert a `net::Address` to an `HttpConfig` 116 | #[cfg(not(feature = "yubihsm-mock"))] 117 | fn http_config_for_address(addr: &net::Address) -> HttpConfig { 118 | match addr { 119 | net::Address::Tcp { 120 | peer_id: _, 121 | ref host, 122 | port, 123 | } => { 124 | let mut config = HttpConfig::default(); 125 | config.addr = host.clone(); 126 | config.port = *port; 127 | config 128 | } 129 | net::Address::Unix { .. } => panic!("yubihsm does not support Unix sockets"), 130 | } 131 | } 132 | 133 | /// Connect to the given address via HTTP 134 | #[cfg(not(feature = "yubihsm-mock"))] 135 | fn connect_http(addr: &net::Address) -> Connector { 136 | Connector::http(&http_config_for_address(addr)) 137 | } 138 | 139 | #[cfg(feature = "yubihsm-mock")] 140 | fn init_connector() -> Connector { 141 | Connector::mockhsm() 142 | } 143 | 144 | /// Get a `yubihsm::Client` configured from the global configuration 145 | fn init_client() -> Client { 146 | let (credentials, reconnect) = client_config(); 147 | 148 | Client::open(connector().clone(), credentials, reconnect).unwrap_or_else(|e| { 149 | status_err!("error connecting to YubiHSM2: {}", e); 150 | process::exit(1); 151 | }) 152 | } 153 | 154 | /// Get client configuration settings 155 | #[cfg(not(feature = "yubihsm-server"))] 156 | fn client_config() -> (yubihsm::Credentials, bool) { 157 | (config().auth.credentials(), true) 158 | } 159 | 160 | /// Get client configuration settings, accounting for `yubihsm-server` server 161 | /// overrides (i.e. local loopback for `tmkms yubihsm` commands) 162 | #[cfg(feature = "yubihsm-server")] 163 | fn client_config() -> (yubihsm::Credentials, bool) { 164 | let cfg = config(); 165 | 166 | cfg.connector_server 167 | .as_ref() 168 | .and_then(|connector_server| { 169 | connector_server.cli.as_ref().and_then(|cli| { 170 | cli.auth_key.and_then(|auth_key_id| { 171 | if is_cli_command() { 172 | Some((prompt_for_auth_key_password(auth_key_id), false)) 173 | } else { 174 | None 175 | } 176 | }) 177 | }) 178 | }) 179 | .unwrap_or_else(|| (cfg.auth.credentials(), true)) 180 | } 181 | 182 | /// Prompt for the password for the given auth key and generate `yubihsm::Credentials` 183 | #[cfg(feature = "yubihsm-server")] 184 | fn prompt_for_auth_key_password(auth_key_id: u16) -> yubihsm::Credentials { 185 | let prompt = format!("Enter password for YubiHSM2 auth key 0x{auth_key_id:04x}: "); 186 | 187 | let password = 188 | Zeroizing::new(rpassword::prompt_password(prompt).expect("error reading password")); 189 | 190 | yubihsm::Credentials::from_password(auth_key_id, password.as_bytes()) 191 | } 192 | 193 | /// Get the YubiHSM-related configuration 194 | pub fn config() -> YubihsmConfig { 195 | let kms_config = APP.config(); 196 | let yubihsm_configs = &kms_config.providers.yubihsm; 197 | 198 | if yubihsm_configs.len() != 1 { 199 | status_err!( 200 | "expected one [yubihsm.provider] in config, found: {}", 201 | yubihsm_configs.len() 202 | ); 203 | process::exit(1); 204 | } 205 | 206 | yubihsm_configs[0].clone() 207 | } 208 | 209 | /// Run a `yubihsm-connector` service in a background thread 210 | #[cfg(all(feature = "yubihsm-server", not(feature = "yubihsm-mock")))] 211 | fn run_connnector_server(config: HttpConfig, connector: Connector) { 212 | thread::spawn(move || loop { 213 | let server = yubihsm::connector::http::Server::new(&config, connector.clone()) 214 | .expect("couldn't start yubihsm connector HTTP server"); 215 | 216 | if let Err(e) = server.run() { 217 | error!("yubihsm HTTP service crashed! {}", e); 218 | } 219 | }); 220 | } 221 | 222 | impl From for Error { 223 | fn from(other: yubihsm::client::Error) -> Error { 224 | ErrorKind::YubihsmError.context(other).into() 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /tests/cli/init.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `init` subcommand 2 | 3 | use crate::cli; 4 | use abscissa_core::Config; 5 | use std::{ffi::OsStr, fs}; 6 | use tmkms::{commands::init::networks::Network, config::KmsConfig}; 7 | 8 | #[test] 9 | fn test_command() { 10 | let parent_dir = tempfile::tempdir().unwrap(); 11 | 12 | let output_dir = parent_dir.path().join("tmkms"); 13 | assert!(!output_dir.exists()); 14 | 15 | // Network names to test with 16 | let networks = Network::all() 17 | .iter() 18 | .map(ToString::to_string) 19 | .collect::>(); 20 | 21 | let result = cli::run([ 22 | OsStr::new("init"), 23 | OsStr::new("-n"), 24 | OsStr::new(&networks.join(",")), 25 | output_dir.as_os_str(), 26 | ]); 27 | 28 | assert!(result.status.success()); 29 | 30 | // Ensure generated configuration file parses 31 | let kms_config_path = output_dir.join("tmkms.toml"); 32 | let kms_config = KmsConfig::load_toml(fs::read_to_string(kms_config_path).unwrap()).unwrap(); 33 | 34 | // Ensure all expected chain IDs are present 35 | assert_eq!( 36 | &kms_config 37 | .chain 38 | .iter() 39 | .map(|c| c.id.as_str().split('-').next().unwrap().to_owned()) 40 | .collect::>(), 41 | &networks 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /tests/cli/mod.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the KMS command-line interface 2 | 3 | use std::{ 4 | ffi::OsStr, 5 | io::{self, Write}, 6 | process::{Command, Output}, 7 | }; 8 | 9 | use super::KMS_EXE_PATH; 10 | 11 | mod init; 12 | mod version; 13 | 14 | #[cfg(feature = "yubihsm")] 15 | mod yubihsm; 16 | 17 | /// Run the `tmkms` CLI command with the given arguments 18 | pub fn run(args: I) -> Output 19 | where 20 | I: IntoIterator, 21 | S: AsRef, 22 | { 23 | Command::new(KMS_EXE_PATH).args(args).output().unwrap() 24 | } 25 | 26 | /// Run the `tmkms` CLI command with the expectation that it will exit successfully, 27 | /// panicking and printing stdout/stderr if it does not 28 | #[allow(dead_code)] 29 | pub fn run_successfully(args: I) -> Output 30 | where 31 | I: IntoIterator, 32 | S: AsRef, 33 | { 34 | let output = run(args); 35 | let status_code = output.status.code().unwrap(); 36 | 37 | if status_code == 0 { 38 | output 39 | } else { 40 | io::stdout().write(&output.stdout).unwrap(); 41 | io::stderr().write(&output.stderr).unwrap(); 42 | 43 | panic!("{KMS_EXE_PATH} exited with error status: {status_code}"); 44 | } 45 | } 46 | 47 | #[test] 48 | fn test_usage() { 49 | let status_code = run(&[] as &[&OsStr]).status.code().unwrap(); 50 | assert_eq!(status_code, 2); 51 | } 52 | -------------------------------------------------------------------------------- /tests/cli/version.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `version` subcommand 2 | 3 | use crate::cli; 4 | use std::{ffi::OsStr, str}; 5 | 6 | #[test] 7 | fn test_version() { 8 | let result = cli::run([OsStr::new("version")]); 9 | 10 | assert!(result.status.success()); 11 | let stdout = str::from_utf8(&result.stdout).unwrap().trim().to_owned(); 12 | assert!(stdout.eq(option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"))); 13 | } 14 | -------------------------------------------------------------------------------- /tests/cli/yubihsm.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `yubihsm` subcommands 2 | 3 | /// Path to KMS configuration file for `yubihsm::MockHSM`-based testing 4 | #[allow(dead_code)] 5 | pub const KMS_CONFIG_PATH: &str = "tests/support/kms_yubihsm_mock.toml"; 6 | #[allow(dead_code)] 7 | pub const PRIV_VALIDATOR_CONFIG_PATH: &str = "tests/support/priv_validator_mock.json"; 8 | 9 | // This test requires USB access to a YubiHSM2 10 | #[cfg(not(feature = "yubihsm-mock"))] 11 | mod detect; 12 | mod keys; 13 | -------------------------------------------------------------------------------- /tests/cli/yubihsm/detect.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `yubihsm detect` subcommand 2 | 3 | use crate::cli; 4 | 5 | #[test] 6 | fn detect_command_test() { 7 | // TODO: parse results 8 | cli::run_successfully(&["yubihsm", "detect"]); 9 | } 10 | -------------------------------------------------------------------------------- /tests/cli/yubihsm/keys.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `yubihsm keys` subcommand 2 | 3 | mod generate; 4 | mod import; 5 | mod list; 6 | 7 | pub use super::{KMS_CONFIG_PATH, PRIV_VALIDATOR_CONFIG_PATH}; 8 | use crate::cli; 9 | 10 | #[test] 11 | fn test_usage() { 12 | let status_code = cli::run(["yubihsm", "keys"]).status.code().unwrap(); 13 | assert_eq!(status_code, 2); 14 | } 15 | -------------------------------------------------------------------------------- /tests/cli/yubihsm/keys/export.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `yubihsm keys export` subcommand 2 | 3 | use crate::cli; 4 | 5 | #[test] 6 | fn keys_export_command_test() { 7 | #[allow(unused_mut)] 8 | let mut args = vec!["yubihsm", "keys", "export", "1"]; 9 | 10 | #[cfg(feature = "yubihsm-mock")] 11 | args.extend_from_slice(&["-c", super::KMS_CONFIG_PATH]); 12 | 13 | cli::run_successfully(args.as_slice()); 14 | } 15 | -------------------------------------------------------------------------------- /tests/cli/yubihsm/keys/generate.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `yubihsm keys generate` subcommand 2 | 3 | use crate::cli; 4 | use std::str; 5 | 6 | #[test] 7 | fn keys_generate_command_test() { 8 | #[allow(unused_mut)] 9 | let mut args = vec!["yubihsm", "keys", "generate", "1"]; 10 | 11 | #[cfg(feature = "yubihsm-mock")] 12 | args.extend_from_slice(&["-c", super::KMS_CONFIG_PATH]); 13 | 14 | let cmd_out = cli::run_successfully(args.as_slice()); 15 | assert!(cmd_out.status.success()); 16 | 17 | let stderr = str::from_utf8(&cmd_out.stderr).unwrap().trim().to_owned(); 18 | assert!(stderr.contains("Generated")); 19 | assert!(stderr.contains("key 0x0001")); 20 | } 21 | -------------------------------------------------------------------------------- /tests/cli/yubihsm/keys/import.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `yubihsm keys import` subcommand 2 | 3 | use crate::cli; 4 | use std::str; 5 | 6 | #[test] 7 | fn keys_import_priv_validator_test() { 8 | #[allow(unused_mut)] 9 | let mut args = vec!["yubihsm", "keys", "import"]; 10 | 11 | #[cfg(feature = "yubihsm-mock")] 12 | args.extend_from_slice(&["-c", super::KMS_CONFIG_PATH]); 13 | args.extend_from_slice(&["-t", "json"]); 14 | args.extend_from_slice(&["-i", "1"]); // key ID 15 | args.extend_from_slice(&[super::PRIV_VALIDATOR_CONFIG_PATH]); 16 | 17 | let out = cli::run_successfully(args.as_slice()); 18 | 19 | assert!(out.status.success()); 20 | 21 | let message = str::from_utf8(&out.stderr).unwrap().trim().to_owned(); 22 | assert!(message.contains("key 0x0001")); 23 | } 24 | -------------------------------------------------------------------------------- /tests/cli/yubihsm/keys/list.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the `yubihsm keys list` subcommand 2 | 3 | use crate::cli; 4 | use std::str; 5 | 6 | #[test] 7 | fn keys_command_test() { 8 | #[allow(unused_mut)] 9 | let mut args = vec!["yubihsm", "keys", "list"]; 10 | 11 | #[cfg(feature = "yubihsm-mock")] 12 | args.extend_from_slice(&["-c", super::KMS_CONFIG_PATH]); 13 | 14 | let out = cli::run_successfully(args.as_slice()); 15 | 16 | assert!(out.status.success()); 17 | assert!(out.stdout.is_empty()); 18 | 19 | let stderr = str::from_utf8(&out.stderr).unwrap().trim().to_owned(); 20 | assert!(stderr.contains("no keys in this YubiHSM")); 21 | } 22 | -------------------------------------------------------------------------------- /tests/support/buffer-underflow-proposal.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iqlusioninc/tmkms/f5d31f8923a5470d119614adc4a9dcba98c88b00/tests/support/buffer-underflow-proposal.bin -------------------------------------------------------------------------------- /tests/support/gen-validator-integration-cfg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PWD=`pwd` 4 | TMHOME=${TMHOME:-${PWD}} 5 | OUTPUT_PATH=${OUTPUT_PATH:-${PWD}} 6 | GENESIS_FILE=${GENESIS_FILE:-${TMHOME}/config/genesis.json} 7 | SIGNING_KEY=${SIGNING_KEY:-${OUTPUT_PATH}/signing.key} 8 | SECRET_KEY=${SECRET_KEY:-${OUTPUT_PATH}/secret_connection.key} 9 | OUTPUT_FILE=${OUTPUT_FILE:-${OUTPUT_PATH}/tmkms.toml} 10 | #TODO: Restore once https://github.com/tendermint/tendermint/issues/3105 is resolved 11 | #VALIDATOR_ID=${VALIDATOR_ID:-"f88883b673fc69d7869cab098de3bafc2ff76eb8"} 12 | #VALIDATOR_ADDR=${VALIDATOR_ADDR:-"tcp://${VALIDATOR_ID}@127.0.0.1:61278"} 13 | VALIDATOR_ADDR=${VALIDATOR_ADDR:-"tcp://127.0.0.1:61278"} 14 | CFG_TEMPLATE=$(cat <<-EOF 15 | # Information about Tenderment blockchain networks this KMS services 16 | [[chain]] 17 | id = "CHAIN_ID" 18 | key_format = { type = "bech32", account_key_prefix = "cosmospub", consensus_key_prefix = "cosmosvalconspub" } 19 | 20 | [[validator]] 21 | addr = "VALIDATOR_ADDR" 22 | chain_id = "CHAIN_ID" 23 | reconnect = true # true is the default 24 | secret_key = "SECRET_KEY" 25 | protocol_version = "v0.34" 26 | 27 | [[providers.softsign]] 28 | chain_ids = ["CHAIN_ID"] 29 | path = "SIGNING_KEY" 30 | EOF 31 | ) 32 | 33 | # First extract the chain ID from the genesis file 34 | CHAIN_ID_SED_EXPR='s/[ ]*"chain_id":[ ]*"\([^"]*\)".*/\1/' 35 | CHAIN_ID=`grep '"chain_id"' ${GENESIS_FILE} | sed "${CHAIN_ID_SED_EXPR}"` 36 | 37 | # Now generate the tmkms.toml file 38 | echo "${CFG_TEMPLATE}" | \ 39 | sed "s|CHAIN_ID|${CHAIN_ID}|g" | \ 40 | sed "s|VALIDATOR_ADDR|${VALIDATOR_ADDR}|g" | \ 41 | sed "s|SECRET_KEY|${SECRET_KEY}|g" | \ 42 | sed "s|SIGNING_KEY|${SIGNING_KEY}|g" > ${OUTPUT_FILE} 43 | 44 | echo "Wrote ${OUTPUT_FILE}" 45 | -------------------------------------------------------------------------------- /tests/support/kms_yubihsm_mock.toml: -------------------------------------------------------------------------------- 1 | # KMS configuration for testing `yubihsm` subcommands 2 | # 3 | # This file is passed to the KMS executable during integration tests 4 | 5 | # Information about Tenderment blockchain networks this KMS services 6 | [[chain]] 7 | id = "cosmoshub" 8 | key_format = { type = "bech32", account_key_prefix = "cosmospub", consensus_key_prefix = "cosmosvalconspub" } 9 | 10 | [[validator]] 11 | addr = "tcp://f88883b673fc69d7869cab098de3bafc2ff76eb8@127.0.0.1:23456" 12 | chain_id = "test_chain_id" 13 | reconnect = false 14 | secret_key = "tests/seccon.key" 15 | protocol_version = "v0.34" 16 | 17 | [[providers.yubihsm]] 18 | adapter = { type = "usb" } 19 | auth = { key = 1, password = "password" } 20 | keys = [{ chain_ids = ["cosmoshub"], key = 1 }] 21 | serial_number = "0123456789" 22 | -------------------------------------------------------------------------------- /tests/support/priv_validator_mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "TEST KEY ONLY: DO **NOT** USE!!", 3 | "address": "275184F7F3BB2D1EB56FB91F0DC9E0FE6FB64097", 4 | "pub_key": { 5 | "type": "tendermint/PubKeyEd25519", 6 | "value": "Dicjr17rwt2WMy+Wo/hLGc2qPpx03kvyUJCJGqh1MXM=" 7 | }, 8 | "last_height": "0", 9 | "last_round": "0", 10 | "last_step": 0, 11 | "priv_key": { 12 | "type": "tendermint/PrivKeyEd25519", 13 | "value": "qIoGOuRm6GHobrRGq9t2I388pnWO7ZB9FE0R+M8TM6oOJyOvXuvC3ZYzL5aj+EsZzao+nHTeS/JQkIkaqHUxcw==" 14 | } 15 | } -------------------------------------------------------------------------------- /tests/support/run-harness-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TMKMS_BIN=${TMKMS_BIN:-"./target/debug/tmkms"} 3 | TMKMS_CONFIG=${TMKMS_CONFIG:-"/harness/tmkms.toml"} 4 | HARNESS_BIN=${HARNESS_BIN:-"tm-signer-harness"} 5 | TMHOME=${TMHOME:-"/harness"} 6 | 7 | # Run KMS in the background 8 | ${TMKMS_BIN} start -c ${TMKMS_CONFIG} & 9 | TMKMS_PID=$! 10 | 11 | # Run the test harness in the foreground 12 | ${HARNESS_BIN} run \ 13 | -addr tcp://127.0.0.1:61278 \ 14 | -tmhome ${TMHOME} 15 | HARNESS_EXIT_CODE=$? 16 | 17 | # Kill the KMS, if it's still running 18 | if ps -p ${TMKMS_PID} > /dev/null 19 | then 20 | echo "Killing KMS (pid ${TMKMS_PID})" 21 | kill ${TMKMS_PID} 22 | # Wait a few seconds for KMS to die properly. 23 | # NOTE: This also acts as a test of the KMS listening for and properly 24 | # responding to the SIGTERM signal from `kill`. 25 | sleep 3 26 | # Make sure KMS has actually stopped properly now. 27 | if ps -p ${TMKMS_PID} > /dev/null 28 | then 29 | echo "Failed to stop KMS!" 30 | exit 100 31 | fi 32 | else 33 | echo "KMS (pid ${TMKMS_PID}) already stopped, not killing" 34 | fi 35 | 36 | # Bubble the exit code up out of the script 37 | echo "Harness tests exiting with code ${HARNESS_EXIT_CODE}" 38 | exit ${HARNESS_EXIT_CODE} 39 | -------------------------------------------------------------------------------- /tests/support/secret_connection.key: -------------------------------------------------------------------------------- 1 | VEVTVCBLRVkgT05MWTogRE8gKipOT1QqKiBVU0UhISE= -------------------------------------------------------------------------------- /tests/support/signing_ed25519.key: -------------------------------------------------------------------------------- 1 | DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEA= -------------------------------------------------------------------------------- /tests/support/signing_secp256k1.key: -------------------------------------------------------------------------------- 1 | IPB0J/bxo6RxloXxwAskEkJNtVEeq43gY+2m5nhtkto= -------------------------------------------------------------------------------- /tmkms.toml.example: -------------------------------------------------------------------------------- 1 | # Example KMS configuration file 2 | # 3 | # This is just an example for reference. You can now use the following command 4 | # to generate a customizable configuration based on your preferences: 5 | # 6 | # $ tmkms init [-n cosmoshub,irishub,...] /path/to/tmkms/homedir 7 | 8 | # Information about Tendermint blockchain networks this KMS services 9 | # 10 | # - id: The chain ID for this chain 11 | # - key_format: How this chain handles serialization. Type may be "bech32", "cosmos-json" or "hex" 12 | # - state_file (optional): path to where the state of the last signing operation is persisted 13 | # - state_hook (optional): user-specified command to run on startup to obtain the current height 14 | # of this chain. The command should output JSON which looks like the following: 15 | # {"latest_block_height": "347290"} 16 | [[chain]] 17 | id = "cosmoshub-3" 18 | key_format = { type = "bech32", account_key_prefix = "cosmospub", consensus_key_prefix = "cosmosvalconspub" } 19 | sign_extensions = false # Should vote extensions for this chain be signed? (default: false) 20 | # state_file = "/path/to/cosmoshub_priv_validator_state.json" 21 | # state_hook = { cmd = ["/path/to/block/height_script", "--example-arg", "cosmoshub"] } 22 | 23 | [[chain]] 24 | id = "irishub" 25 | key_format = { type = "bech32", account_key_prefix = "iap", consensus_key_prefix = "icp" } 26 | # state_file = "/path/to/irishub_priv_validator_state.json" 27 | 28 | [[chain]] 29 | id = "agoricdev-4" 30 | key_format = { type = "cosmos-json" } 31 | # state_file = "/path/to/agoricdev_priv_validator_state.json" 32 | 33 | ## Validator configuration 34 | [[validator]] 35 | addr = "tcp://f88883b673fc69d7869cab098de3bafc2ff76eb8@example1.example.com:26658" 36 | # or addr = "unix:///path/to/socket" 37 | chain_id = "cosmoshub-3" 38 | reconnect = true # true is the default 39 | secret_key = "path/to/secret_connection.key" 40 | # max_height = "500000" 41 | protocol_version = "v0.34" # or "v0.33" (i.e. Tendermint version) 42 | 43 | ## Signing provider configuration 44 | 45 | # enable the `yubihsm` feature to use this backend 46 | [[providers.yubihsm]] 47 | adapter = { type = "usb" } 48 | auth = { key = 1, password_file = "/path/to/password" } # or pass raw password as `password` 49 | keys = [ 50 | { chain_ids = ["cosmoshub-3"], key = 1, type = "consensus" } 51 | # { chain_ids = ["irishub"], key = 2, type = "account" } 52 | ] 53 | #serial_number = "0123456789" # identify serial number of a specific YubiHSM to connect to 54 | #connector_server = { laddr = "tcp://127.0.0.1:12345", cli = { auth_key = 2 } } # run yubihsm-connector compatible server 55 | 56 | # enable the `ledger` feature to use this backend 57 | #[[providers.ledgertm]] 58 | #chain_ids = ["cosmoshub-3"] 59 | 60 | # enable the `softsign` feature to use this backend 61 | # note: the `yubihsm` or `ledger` backends are preferred over this one 62 | [[providers.softsign]] 63 | chain_ids = ["cosmoshub-3"] 64 | key_type = "consensus" 65 | path = "path/to/consensus-ed25519.key" # generate using `tmkms softsign keygen -t consensus consensus-ed25519.key` 66 | 67 | # the `softsign` backend also supports account keys 68 | #[[providers.softsign]] 69 | #chain_ids = ["irishub"] 70 | #key_type = "account" 71 | #path = "path/to/account-secp256k1.key" # generate using `tmkms softsign keygen -t account account-secp256k1.key` 72 | 73 | ## (Optional) Transaction signer configuration 74 | 75 | # example transaction signer: sign StdTx-strucutred transactions with a KMS-managed key 76 | # [[tx_signer]] 77 | # chain_id = "irishub" 78 | # schema = "iris_tx_schema.toml" # See example TERRA_SCHEMA at https://docs.rs/stdtx#usage 79 | # account_number = 101072 80 | # account_address = "iap1qqqsyqcyq5rqwzqfpg9scrgwpugpzysnfxh53h" # must be in the keyring for this chain 81 | # acl = { msg_type = ["oracle/MsgExchangeRatePrevote", "oracle/MsgExchangeRateVote"] } 82 | # poll_interval = { blocks = 5 } 83 | # source = { protocol = "jsonrpc", uri = "http://127.0.0.1:23456" } 84 | # rpc = { addr = "tcp://127.0.0.1:26657" } 85 | # seq_file = "irishub-account-seq.json" 86 | --------------------------------------------------------------------------------