diff --git a/Cargo.lock b/Cargo.lock index 90695fc..95b4af0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ "tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "tendermint 0.3.0-beta1", "tiny-bip39 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "yubihsm 0.22.0-alpha2 (registry+https://github.com/rust-lang/crates.io-index)", + "yubihsm 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "zeroize 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "yubihsm" -version = "0.22.0-alpha2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1421,6 +1421,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum x25519-dalek 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "58e75de7a035f694df91838afa87faa8278bc484fa8d7dc34b5a24535cc2bb41" -"checksum yubihsm 0.22.0-alpha2 (registry+https://github.com/rust-lang/crates.io-index)" = "9163ad90f2ff993df23530b1e4c1bcd98e4e32903656c935040f6349d8126413" +"checksum yubihsm 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b61bfeae6f2ff19327d5ce0c9263afd7f80bda2c3fc5ffd9afd98146d0e72457" "checksum zeroize 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d7ddffec9ddef28ba2d6359bcbf0dc6772e62b112bc103dfb1e6fab46cd47c39" "checksum zeroize 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8ddfeb6eee2fb3b262ef6e0898a52b7563bb8e0d5955a313b3cf2f808246ea14" diff --git a/Cargo.toml b/Cargo.toml index bc9d981..30a2460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ signatory-ledger-tm = { version = "0.11", optional = true } subtle-encoding = { version = "0.3", features = ["bech32-preview"] } tendermint = { version = "0.3.0-beta1", path = "tendermint-rs" } tiny-bip39 = "0.6" -yubihsm = { version = "0.22.0-alpha2", features = ["setup", "usb"], optional = true } +yubihsm = { version = "0.22", features = ["setup", "usb"], optional = true } zeroize = "0.5" [dev-dependencies] diff --git a/README.md b/README.md index 737f6c4..4d90c87 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ You will need the following prerequisites: To install `tmkms`, do the following: -1. (x86_64 only) Configure `RUSTFLAGS` environment variable: `export RUSTFLAGS=-Ctarget-feature=+aes` +1. (x86_64 only) Configure `RUSTFLAGS` environment variable: `export RUSTFLAGS=-Ctarget-feature=+aes,+ssse3` 2. Run the following to install Tendermint KMS using Rust's `cargo` tool: ``` diff --git a/README.yubihsm.md b/README.yubihsm.md new file mode 100644 index 0000000..98e26d5 --- /dev/null +++ b/README.yubihsm.md @@ -0,0 +1,414 @@ +# YubiHSM 2 + Tendermint KMS + +The [YubiHSM 2] from Yubico is a relatively low-cost solution for online +key storage featuring support for random key generation, encrypted backup +and export, and audit logging. + +This document describes how to configure a YubiHSM 2 for production use +with Tendermint KMS. + +## Compiling `tmkms` with YubiHSM support + +Please see the [toplevel README.md] for prerequisites for compiling `tmkms` +from source code. You will need: Rust (stable, 1.31+), a C compiler, +`pkg-config`, and `libusb` (1.0+) installed. + +There are two ways to install `tmkms` with YubiHSM 2 support, and in either +case, you will need to pass the `--features=yubihsm` parameter to enable +YubiHSM 2 support: + +### Compiling from source code (via git) + +`tmkms` can be compiled directly from the git repository source code using the +following method. + +``` +$ git clone https://github.com/tendermint/kms.git && cd kms +[...] +$ cargo build --release --features=yubihsm +``` + +If successful, this will produce a `tmkms` executable located at +`./target/release/tmkms` + +### Installing with the `cargo install` command + +With Rust (1.31+) installed, you can install tmkms with the following: + +``` +cargo install tmkms --features=yubihsm +``` + +Or to install a specific version (recommended): + +``` +cargo install tmkms --features=yubihsm --version=0.4.0 +``` + +This command installs `tmkms` directly from packages hosted on Rust's +[crates.io] service. Package authenticity is verified via the +[crates.io index] (itself a git repository) and by SHA-256 digests of +released artifacts. + +However, if newer dependencies are available, it may use newer versions +besides the ones which are "locked" in the source code repository. We +cannot verify those dependencies do not contain malicious code. If you would +like to ensure the dependencies in use are identical to the main repository, +please build from source code instead. + +### Verifying YubiHSM support was included in a build + +Run the following subcommand of the resulting `tmkms` executable to ensure +that YubiHSM 2 support was compiled-in successfully: + +``` +$ tmkms yubihsm help 127 ↵ +tmkms 0.4.0 +Tony Arcieri , Ismail Khoffi +Tendermint Key Management System + +USAGE: + tmkms + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +SUBCOMMANDS: + detect detect all YubiHSM2 devices connected via USB + help show help for the 'yubihsm' subcommand + keys key management subcommands + setup initial device setup and configuration + test perform a signing test +``` + +If `detect`, `help`, `keys`, `setup` etc are listed under `SUBCOMMANDS` then +the build was successfully configured with YubiHSM 2 support. + +## Production YubiHSM 2 setup + +`tmkms` contains built-in support for fully automated production YubiHSM 2 +setup, including deterministically generating authentication keys and backup +encryption keys from a [BIP39] 24-word seed phrase. This allows new YubiHSM 2s +to be provisioned with the same initial set of keys from the phrase alone, +while also creating multiple "roles" within the HSM. + +Alternatively Yubico provides the [yubihsm-setup] tool, however the setup +process internal to `tmkms` provides a "happy path" for Tendermint validator +usage, and also operational processes which should be familiar to +cryptocurrency users. + +### Configuring `tmkms` for initial setup + +In order to perform setup, `tmkms` needs a minimal configuration file which +contains the credentials needed to authenticate to the HSM with an +administrator key. + +This configuration should be placed in a file called: `tmkms.toml`. +By default `tmkms` will look for this file in the current working directory, +however most subcommands take a `-c /path/to/tmkms.toml` argument if you +would like to place it somewhere else. + +Here is an example `tmkms.toml` file which can be used for initial setup +with a YubiHSM 2 which is still configured with its default authentication +keys (i.e. authentication key 1, with a default password of `password`): + +```toml +[[providers.yubihsm]] +adapter = { type = "usb" } +auth = { key = 1, password = "password" } +``` + +If you have changed the default authentication key ID and/or password, you +will need to provide the correct credentials. + +NOTE: if you have *lost or forgotten* the admin authentication key, you +can *factory reset* the YubiHSM 2 to a default state (wiping all keys) +by pushing down on the top (LED) immediately after inserting it and continuing +to push down on it for 10 seconds. + +### `tmkms yubihsm setup`: Initial YubiHSM setup + +**WARNING: THIS PROCESS PERFORMS A FACTORY RESET OF THE YUBIHSM, DELETING ALL +EXISTING KEYS AND REPLACING THEM WITH NEW ONES. MAKE SURE YOU HAVE MADE BACKUPS +OF IMPORTANT KEYS BEFORE PROCEEDING!!!** + +After configuring your YubiHSM 2's credentials in `tmkms.toml`, you can run the +following command to perform automatic setup: + +``` +$ tmkms yubihsm setup +``` + +We recommend this process be performed on an airgapped computer which is not +connected to any network. It will generate master secrets which can be used +to decrypt encrypted backups of keys within the HSM. + +This process will perform the following steps: + +- Generate a random 24-word recovery mnemonic phrase from randomness taken + from the host OS as well as the YubiHSM2 itself. +- Deterministically (ala BIP32) generate authentication keys for the following + four roles within the YubiHSM (with [yubihsm-shell] compatible passwords): + - **admin** (authentication key `0x0001`): full access to all HSM capabilities + - **operator** (authentication key `0x0002`): ability to generate new signing + keys, export/import encrypted backups of keys, and view the audit log + - **auditor** (authentication key `0x0003`): ability to view and consume the + audit log + - **validator** (authentication key `0x0004`): ability to generate signatures + using signing keys loaded within the device +- Deterministically generate "wrap key" `0x0001`: symmetric encryption key + (AES-CCM) used for making encrypted backups of other keys generated within + the device. If you have existing keys you would like to transfer out of other + YubiHSM 2s, this key can be imported into those HSMs in order to export + encrypted backups (see below). + +Notably different from cryptocurrency hardware wallets: this process does not +actually generate any signing keys, only authentication keys and the wrap key. +Generating validator signing keys (and creating backups) occurs in a subsequent +step (see below). + +The following is example output from running the above command: + +``` +$ tmkms yubihsm setup +This process will *ERASE* the configured YubiHSM2 and reinitialize it: + +- YubiHSM serial: 9876543210 + +Authentication keys with the following IDs and passwords will be created: + +- key 0x0001: admin: + + double section release consider diet pilot flip shell mother alone what fantasy + much answer lottery crew nut reopen stereo square popular addict just animal + +- authkey 0x0002 [operator]: kms-operator-password-1k02vtxh4ggxct5tngncc33rk9yy5yjhk +- authkey 0x0003 [auditor]: kms-auditor-password-1s0ynq69ezavnqgq84p0rkhxvkqm54ks9 +- authkey 0x0004 [validator]: kms-validator-password-1x4anf3n8vqkzm0klrwljhcx72sankcw0 +- wrapkey 0x0001 [primary]: 21a6ca8cfd5dbe9c26320b5c4935ff1e63b9ab54e2dfe24f66677aba8852be13 + +*** Are you SURE you want erase and reinitialize this HSM? (y/N): +``` + +NOTE: the admin password is *displayed* on two separate lines. When using it +from [yubihsm-shell] or as the `password` field in tmkms.toml, it is not split +across multiple lines and is separated only by a single space between words. + +If you are certain you are ready to initialize your first YubiHSM 2, type `y` +to proceed: + +``` +*** Are you SURE you want erase and reinitialize this HSM? (y/N): y +21:08:09 [WARN] factory resetting HSM device! all data will be lost! +21:08:10 [INFO] waiting for device reset to complete +21:08:11 [INFO] installed temporary setup authentication key into slot 65534 +21:08:11 [WARN] deleting default authentication key from slot 1 +21:08:11 [INFO] installing role: admin:2019-03-05T20:31:07Z +21:08:11 [INFO] installing role: operator:2019-03-05T20:31:08Z +21:08:11 [INFO] installing role: auditor:2019-03-05T20:31:08Z +21:08:11 [INFO] installing role: validator:2019-03-05T20:31:08Z +21:08:11 [INFO] installing wrap key: primary:2019-03-05T20:31:08Z +21:08:11 [INFO] storing provisioning report in opaque object 0xfffe +21:08:11 [WARN] deleting temporary setup authentication key from slot 65534 + Success reinitialized YubiHSM (serial: 9876543210) +``` + +Make sure to write down the 24-word recovery phrase and store it in a +secure location! + +### Initializing additional HSMs from an existing 24-word recovery phrase + +After initializing your first HSM, you can bootstrap additional YubiHSM 2s as +a clone of the initial one using the same 24-word recovery phrase. To do that, +pass the `-r` (or `--restore`) flag when running the setup command: + +``` +$ tmkms yubihsm setup -r +Restoring and reprovisioning YubiHSM from existing 24-word mnemonic phrase. + +*** Enter mnemonic (separate words with spaces): double section release consider [...] + +Mnemonic phrase decoded/checksummed successfully! + +This process will *ERASE* the configured YubiHSM2 and reinitialize it: + +- YubiHSM serial: 9876543211 + +Authentication keys with the following IDs and passwords will be created: + +- key 0x0001: admin: + + double section release consider diet pilot flip shell mother alone what fantasy + much answer lottery crew nut reopen stereo square popular addict just animal + +- authkey 0x0002 [operator]: kms-operator-password-1k02vtxh4ggxct5tngncc33rk9yy5yjhk +- authkey 0x0003 [auditor]: kms-auditor-password-1s0ynq69ezavnqgq84p0rkhxvkqm54ks9 +- authkey 0x0004 [validator]: kms-validator-password-1x4anf3n8vqkzm0klrwljhcx72sankcw0 +- wrapkey 0x0001 [primary]: 21a6ca8cfd5dbe9c26320b5c4935ff1e63b9ab54e2dfe24f66677aba8852be13 + +*** Are you SURE you want erase and reinitialize this HSM? (y/N): y +21:47:18 [WARN] factory resetting HSM device! all data will be lost! +21:47:19 [INFO] waiting for device reset to complete +21:47:21 [INFO] installed temporary setup authentication key into slot 65534 +21:47:21 [WARN] deleting default authentication key from slot 1 +21:47:21 [INFO] installing role: admin:2019-03-05T21:47:02Z +21:47:21 [INFO] installing role: operator:2019-03-05T21:47:03Z +21:47:21 [INFO] installing role: auditor:2019-03-05T21:47:03Z +21:47:21 [INFO] installing role: validator:2019-03-05T21:47:03Z +21:47:21 [INFO] installing wrap key: primary:2019-03-05T21:47:03Z +21:47:21 [INFO] storing provisioning report in opaque object 0xfffe +21:47:21 [WARN] deleting temporary setup authentication key from slot 65534 + Success reinitialized YubiHSM (serial: 9876543211) +``` + +## `tmkms yubihsm keys generate`: signing key generation + +The `tmkms` YubiHSM backend is designed to support signing keys which are +randomly generated by the device's internal cryptographically secure random +number generator, as opposed to ones which are deterministically generated +from the 24-word BIP39 mnemonic phrase. + +This means you will need to do the following: + +- Run `tmkms yubihsm keys generate` to create signing keys + (i.e. validator consensus keys) +- Retain backups of these keys for disaster recovery + +This command integrates a feature to export a backup of the keys at the +time they are generated, which is compatible with the [yubihsm-shell] tool. + +Below is an example of the command to generate and export an encrypted backup +of an Ed25519 signing key: + +``` +$ tmkms yubihsm keys generate 1 -l "steakz4u-validator:2019-03-06T01:25:39Z" -b steakz4u-validator-key.enc + Generated key #1: cosmosvalconspub1zcjduepqtvzxa733n7dhrjf247n0jtdwsvvsd4jgqvzexj5tkwerpzy5sugsvmfja3 + Wrote backup of key 1 (encrypted under wrap key 1) to steakz4u-validator-key.enc +``` + +This operation must be performed after configuring `tmkms.toml` with one of +the following sets of credentials: + +- The `admin` key and associated 24-word password (i.e. key ID `0x0001`) +- The `operator` key (`0x0002`) and associated `kms-operator-password` + +### Parameters + +- `tmkms yubihsm keys generate 1` - generates asymmetric key 0x0001, which is by + default an Ed25519 signing key. +- `-l` (or `--label`): an up-to-40-character label describing the key +- `-b` (or `--backup`): path to a file where an *encrypted* backup of the + generated key should be written +- (not used in the example) `-w` (or `--wrapkey`): ID of the "wrap" + (i.e encryption) key used to encrypt the backup key. It defaults to wrap + key 0x0001 which was automatically generated as part of the + `tmkms yubihsm setup` process. + +## `tmkms yubihsm keys list`: list signing keys + +The following command lists keys in the HSM: + +``` +$ tmkms yubihsm keys list +Listing keys in YubiHSM #0007550072: +- #1: cosmosvalconspub1zcjduepqtvzxa733n7dhrjf247n0jtdwsvvsd4jgqvzexj5tkwerpzy5sugsvmfja3 +``` + +## Exporting and Importing Keys + +`tmkms` contains functionality for exporting and importing keys, including +making encrypted backups of keys, and also importing existing +`priv_validator.json` keys. + +We recommend you randomly generate keys using the above +`tmkms yubihsm keys generate` procedure to avoid exposing plaintext copies +of signing private keys outside of the HSM. However, below are instructions +which can hopefully accommodate any situation you happen to be in with regard +to exporting and importing existing keys. + +### `tmkms yubihsm keys export`: export encrypted backups of signing keys + +If you ran `tmkms yubihsm keys generate` (or equivalent [yubihsm-shell]) +command without creating a backup, the `keys export` subcommand can also +export a backup: + +``` +$ yubihsm keys export --id 1 steakz4u2-validator-key.enc + Exported key 0x0001 (encrypted under wrap key 0x0001) to steakz4u2-validator-key.enc +``` + +The backups generated are compatible with the ones generated by the Yubico +`yubihsm-shell` utility. + +#### Parameters + +- `-i` (or `--id`): ID of the asymmetric key to export +- `-w` (or `--wrapkey`): ID of the wrap key under which the exported key will + be encrypted. + +### `tmkms yubihsm keys import`: import encrypted backups of signing keys + +After generating a key on a YubiHSM and exporting a backup, you can import the +encrypted copy into another HSM with the following command: + +``` +$ tmkms yubihsm keys import steakz4u-validator-key.enc + Imported key 0x0001: cosmosvalconspub1zcjduepqtvzxa733n7dhrjf247n0jtdwsvvsd4jgqvzexj5tkwerpzy5sugsvmfja3 +``` + +### Exporting keys from previously configured YubiHSM 2s + +If you've previously configured a production key within a YubiHSM 2 and wish to +securely export it and import it into a YubiHSM 2 provisioned using the +`tmkms yubihsm setup` workflow, here are the steps to securely export it. + +#### Note wrap key during the `tmkms yubihsm setup` procedure + +Among the keys generated during the initial procedure is the wrap key, which +is the encryption key used for all backups. It's at the bottom of this list: + +``` +- authkey 0x0002 [operator]: kms-operator-password-1k02vtxh4ggxct5tngncc33rk9yy5yjhk +- authkey 0x0003 [auditor]: kms-auditor-password-1s0ynq69ezavnqgq84p0rkhxvkqm54ks9 +- authkey 0x0004 [validator]: kms-validator-password-1x4anf3n8vqkzm0klrwljhcx72sankcw0 +- wrapkey 0x0001 [primary]: 21a6ca8cfd5dbe9c26320b5c4935ff1e63b9ab54e2dfe24f66677aba8852be13 +``` + +(i.e. `wrapkey 0x0001 [primary]`) + +The number `21a6ca8cfd5dbe9c26320b5c4935ff1e63b9ab54e2dfe24f66677aba8852be13` +is the hex serialization of an AES-256-CCM encryption key, and also compatible +with the syntax used by the [yubihsm-shell] utility. + +If you can authenticate to a YubiHSM 2 which contains an existing key you with +to export, you can import the wrap key to export it under into the HSM with +the following `yubihsm-shell` command: + +``` +yubihsm> put wrapkey 0 1 wrapkey 1 export-wrapped,import-wrapped exportable-under-wrap,sign-ecdsa,sign-eddsa 21a6ca8cfd5dbe9c26320b5c4935ff1e63b9ab54e2dfe24f66677aba8852be13 +Stored Wrap key 0x0001 +``` + +#### Parameters + +- `put wrapkey 0 1 wrapkey`: put the specified wrap key (via session 0) into slot `0x0001` +- `1`: put the wrap key into [domain] 1 (use any of the domains the original key + is accessible from) +- `export-wrapped,import-wrapped`: grant the [capabilities] to export and + import other keys +- `exportable-under-wrap,sign-ecdsa,sign-eddsa`: [delegated capabilities] which + allow imported keys to be exported again, as well as used to generate ECDSA + and "EdDSA" (i.e. Ed25519) signatures. +- `[hex string]`: raw AES-256-CCM wrap key to import + +[YubiHSM 2]: https://www.yubico.com/product/yubihsm-2/ +[toplevel README.md]: https://github.com/tendermint/kms/blob/master/README.md#installation +[crates.io]: https://crates.io +[crates.io index]: https://github.com/rust-lang/crates.io-index +[BIP39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki +[yubihsm-setup]: https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-setup/ +[yubihsm-shell]: https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-shell/ +[domain]: https://developers.yubico.com/YubiHSM2/Concepts/Domain.html +[capabilities]: https://developers.yubico.com/YubiHSM2/Concepts/Capability.html +[delegated capabilities]: https://developers.yubico.com/YubiHSM2/Concepts/Effective_Capabilities.html diff --git a/src/commands/yubihsm/keys/export.rs b/src/commands/yubihsm/keys/export.rs new file mode 100644 index 0000000..3052386 --- /dev/null +++ b/src/commands/yubihsm/keys/export.rs @@ -0,0 +1,74 @@ +use super::*; +use abscissa::Callable; +use std::{fs::OpenOptions, io::Write, os::unix::fs::OpenOptionsExt, path::PathBuf, process}; +use subtle_encoding::base64; + +/// The `yubihsm keys export` subcommand: create encrypted backups of keys +#[derive(Debug, Default, Options)] +pub struct ExportCommand { + /// Path to configuration file + #[options(short = "c", long = "config")] + pub config: Option, + + /// ID of the key to export + #[options(short = "i", long = "id")] + pub key_id: u16, + + /// ID of the wrap key to encrypt the exported key under + #[options(short = "w", long = "wrapkey")] + pub wrap_key_id: Option, + + /// Path to write the resulting file to + #[options(free)] + pub path: PathBuf, +} + +impl Callable for ExportCommand { + fn call(&self) { + let wrap_key_id = self.wrap_key_id.unwrap_or(DEFAULT_WRAP_KEY); + + let wrapped_bytes = crate::yubihsm::client() + .export_wrapped( + wrap_key_id, + yubihsm::object::Type::AsymmetricKey, + self.key_id, + ) + .unwrap_or_else(|e| { + status_err!( + "couldn't export key {} under wrap key {}: {}", + self.key_id, + wrap_key_id, + e + ); + process::exit(1); + }); + + let mut export_file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o600) + .open(&self.path) + .unwrap_or_else(|e| { + status_err!("couldn't export to {} ({})", &self.path.display(), e); + process::exit(1); + }); + + export_file + .write_all(&base64::encode(&wrapped_bytes.into_vec())) + .unwrap_or_else(|e| { + status_err!("error exporting {}: {}", &self.path.display(), e); + process::exit(1); + }); + + status_ok!( + "Exported", + "key 0x{:04x} (encrypted under wrap key 0x{:04x}) to {}", + self.key_id, + wrap_key_id, + self.path.display() + ); + } +} + +impl_command!(ExportCommand); diff --git a/src/commands/yubihsm/keys/generate.rs b/src/commands/yubihsm/keys/generate.rs index 040b16f..9de4301 100644 --- a/src/commands/yubihsm/keys/generate.rs +++ b/src/commands/yubihsm/keys/generate.rs @@ -35,8 +35,8 @@ pub struct GenerateCommand { pub backup_file: Option, /// Key ID of the wrap key to use when creating a backup - #[options(no_short, long = "backup-key")] - pub backup_wrap_key: Option, + #[options(short = "w", long = "wrapkey")] + pub wrap_key_id: Option, /// Key IDs to generate #[options(free)] @@ -108,7 +108,7 @@ impl Callable for GenerateCommand { &hsm, *key_id, &backup_file, - self.backup_wrap_key.unwrap_or(DEFAULT_WRAP_KEY), + self.wrap_key_id.unwrap_or(DEFAULT_WRAP_KEY), ); } } @@ -119,6 +119,7 @@ impl Callable for GenerateCommand { impl_command!(GenerateCommand); /// Create an encrypted backup of this key under the given wrap key ID +// TODO(tarcieri): unify this with the similar code in export? fn create_encrypted_backup( hsm: &yubihsm::Client, key_id: yubihsm::object::Id, diff --git a/src/commands/yubihsm/keys/import.rs b/src/commands/yubihsm/keys/import.rs index ca42bc3..8c6b4a2 100644 --- a/src/commands/yubihsm/keys/import.rs +++ b/src/commands/yubihsm/keys/import.rs @@ -1,10 +1,10 @@ use super::*; use abscissa::Callable; -use serde_json::Value; use signatory::ed25519; -use std::{fs::File, io::prelude::*, process, str}; +use std::{fs, path::PathBuf, process}; use subtle_encoding::base64; use tendermint::public_keys::ConsensusKey; +use yubihsm::object; /// The `yubihsm keys import` subcommand #[derive(Debug, Default, Options)] @@ -13,115 +13,191 @@ pub struct ImportCommand { #[options(short = "c", long = "config")] pub config: Option, - /// Path to the validator configuration file - #[options(short = "p", long = "path")] - pub path: Option, + /// ID of the key to import (if applicable) + #[options(short = "i", long = "id")] + pub key_id: Option, - /// Type of key to import (default 'ed25519') + /// ID of the wrap key the original key was encrypted with + #[options(short = "w", long = "wrapkey")] + pub wrap_key_id: Option, + + /// Type of key to import (either `wrap` or `priv_validator`, default `wrap`) #[options(short = "t")] pub key_type: Option, - /// Label for imported key + /// Label for imported key (only applicable to `priv_validator` keys) #[options(short = "l", long = "label")] pub label: Option, - /// Key ID for imported key + /// Path to the key to import #[options(free)] - key_id: Option, + pub path: PathBuf, } impl Callable for ImportCommand { fn call(&self) { - if self.path.is_none() { - status_err!("must provide a valid path to priv_validator.json"); + let contents = fs::read_to_string(&self.path).unwrap_or_else(|e| { + status_err!("couldn't import file {}: {}", self.path.display(), e); process::exit(1); + }); + + match self.key_type.as_ref().map(|ty| ty.as_str()) { + Some("wrap") => self.import_wrapped(&contents), + Some("priv_validator") => self.import_priv_validator_json(&contents), + Some(other) => { + status_err!("invalid key type: {}", other); + process::exit(1); + } + None => { + if self.path.ends_with("priv_validator.json") { + self.import_priv_validator_json(&contents) + } else { + self.import_wrapped(&contents) + } + } } + } +} - if self.key_id.is_none() { - status_err!("must provide a unique key_id"); - process::exit(1); +impl ImportCommand { + /// Import a wrapped object into the HSM + fn import_wrapped(&self, wrapped_key_base64: &str) { + if let Some(id) = self.key_id { + status_warn!("ignoring key ID: {} (wrapped keys use original key ID)", id); } - match &self.key_type { - Some(ref key_type) => { - if key_type != DEFAULT_KEY_TYPE { - status_err!( - "only supported key type is: ed25519 (given: \"{}\")", - key_type - ); - process::exit(1); - } - } - None => (), + if let Some(ref label) = self.label { + status_warn!( + "ignoring label: {:?} (wrapped keys use original label)", + label + ); } - if let Some(path) = &self.path { - let mut f = File::open(path).unwrap_or_else(|e| { - status_err!("couldn't open validator config file {}: {}", path, e); - process::exit(1); - }); + let wrap_key_id = self.wrap_key_id.unwrap_or(DEFAULT_WRAP_KEY); - let mut contents = Vec::new(); - f.read_to_end(&mut contents).unwrap_or_else(|e| { - status_err!("couldn't read validator config file {}: {}", path, e); + let wrapped_key_ciphertext = + base64::decode(wrapped_key_base64.as_bytes()).unwrap_or_else(|e| { + status_err!( + "couldn't decode Base64-encoded wrapped key from {}: {}", + self.path.display(), + e + ); process::exit(1); }); - let v: Value = serde_json::from_slice(&contents).unwrap(); - let s = v["priv_key"]["value"].as_str().unwrap_or_else(|| { - status_err!("couldn't read validator private key from config: {}", path); + let wrapped_message = yubihsm::wrap::Message::from_vec(wrapped_key_ciphertext) + .unwrap_or_else(|e| { + status_err!( + "couldn't parse wrapped key from {}: {}", + self.path.display(), + e + ); process::exit(1); }); - let key_pair = base64::decode(s).unwrap_or_else(|e| { - status_err!("couldn't decode validator private key from config: {}", e); + let hsm = crate::yubihsm::client(); + + let obj = hsm + .import_wrapped(wrap_key_id, wrapped_message) + .unwrap_or_else(|e| { + status_err!( + "error importing encrypted key from {} (using wrapkey 0x{:04x}): {}", + self.path.display(), + wrap_key_id, + e + ); process::exit(1); }); - let seed = ed25519::Seed::from_keypair(&key_pair).unwrap_or_else(|e| { - status_err!("invalid key in validator config: {}", e); - process::exit(1); - }); + if obj.object_type != object::Type::AsymmetricKey { + // We mainly care about importing asymmetric keys, but can handle other types. + // If we encounter a non-asymmetric key type, display basic info. + status_ok!( + "Imported", + "object 0x{:04x} ({:?})", + obj.object_id, + obj.object_type + ); + process::exit(0); + } - let key = seed.as_secret_slice(); + let public_key = hsm.get_public_key(obj.object_id).unwrap_or_else(|e| { + status_err!( + "couldn't get public key for asymmetric key #{}: {}", + obj.object_id, + e + ); + process::exit(1); + }); - let label = - yubihsm::object::Label::from(self.label.as_ref().map(|l| l.as_ref()).unwrap_or("")); + // TODO: support for non-Cosmos keys, non-Ed25519 keys, non-validator (i.e. account) keys + let key_info = match public_key.algorithm { + yubihsm::asymmetric::Algorithm::Ed25519 => { + ConsensusKey::from(ed25519::PublicKey::from_bytes(&public_key.as_ref()).unwrap()) + .to_string() + } + alg => format!("{:?}: {:?}", alg, public_key.as_ref()), + }; - let hsm = crate::yubihsm::client(); + status_ok!("Imported", "key 0x{:04x}: {}", obj.object_id, key_info); + } - if let Err(e) = hsm.put_asymmetric_key( - self.key_id.unwrap(), - label, - DEFAULT_DOMAINS, - DEFAULT_CAPABILITIES, - yubihsm::asymmetric::Algorithm::Ed25519, - key, - ) { - status_err!("couldn't import key #{}: {}", self.key_id.unwrap(), e); - process::exit(1); - } + /// Import an existing priv_validator file into the HSM + // TODO(tarcieri): ideally this can eventually be removed. Its value seems time-limited + // and it makes this module much more complex than functionality for importing wrapped backups + fn import_priv_validator_json(&self, json_data: &str) { + if let Some(id) = self.wrap_key_id { + status_warn!( + "ignoring wrapkey ID: {} (not applicable to priv_validator.json files)", + id + ); + } + + let key_id = self.key_id.unwrap_or_else(|| { + status_err!( + "no key ID specified (use e.g. tmkms yubihsm keys import -i 1 priv_validator.json)" + ); + process::exit(1); + }); - let public_key = ed25519::PublicKey::from_bytes( - hsm.get_public_key(self.key_id.unwrap()) - .unwrap_or_else(|e| { - status_err!( - "couldn't get public key for key #{}: {}", - self.key_id.unwrap(), - e - ); - process::exit(1); - }), - ) - .unwrap(); + let v: serde_json::Value = serde_json::from_str(json_data).unwrap(); - status_ok!( - "Imported", - "key #{}: {}", - self.key_id.unwrap(), - ConsensusKey::from(public_key) + let s = v["priv_key"]["value"].as_str().unwrap_or_else(|| { + status_err!( + "couldn't read validator private key from config: {}", + self.path.display() ); + process::exit(1); + }); + + let key_pair = base64::decode(s).unwrap_or_else(|e| { + status_err!("couldn't decode validator private key from config: {}", e); + process::exit(1); + }); + + let seed = ed25519::Seed::from_keypair(&key_pair).unwrap_or_else(|e| { + status_err!("invalid key in validator config: {}", e); + process::exit(1); + }); + + let key = seed.as_secret_slice(); + + let label = + yubihsm::object::Label::from(self.label.as_ref().map(|l| l.as_ref()).unwrap_or("")); + + if let Err(e) = crate::yubihsm::client().put_asymmetric_key( + key_id, + label, + DEFAULT_DOMAINS, + DEFAULT_CAPABILITIES, + yubihsm::asymmetric::Algorithm::Ed25519, + key, + ) { + status_err!("couldn't import key #{}: {}", self.key_id.unwrap(), e); + process::exit(1); } + + status_ok!("Imported", "key 0x{:04x}", key_id); } } diff --git a/src/commands/yubihsm/keys/mod.rs b/src/commands/yubihsm/keys/mod.rs index 10f0ade..972eabe 100644 --- a/src/commands/yubihsm/keys/mod.rs +++ b/src/commands/yubihsm/keys/mod.rs @@ -1,10 +1,12 @@ +mod export; mod generate; mod help; mod import; mod list; use self::{ - generate::GenerateCommand, help::HelpCommand, import::ImportCommand, list::ListCommand, + export::ExportCommand, generate::GenerateCommand, help::HelpCommand, import::ImportCommand, + list::ListCommand, }; use abscissa::Callable; @@ -23,6 +25,9 @@ pub const DEFAULT_WRAP_KEY: yubihsm::object::Id = 1; /// The `yubihsm keys` subcommand #[derive(Debug, Options)] pub enum KeysCommand { + #[options(help = "export an encrypted backup of a signing key inside the HSM device")] + Export(ExportCommand), + #[options(help = "generate an Ed25519 signing key inside the HSM device")] Generate(GenerateCommand), @@ -40,6 +45,7 @@ impl KeysCommand { /// Optional path to the configuration file pub(super) fn config_path(&self) -> Option<&str> { match self { + KeysCommand::Export(export) => export.config.as_ref().map(|s| s.as_ref()), KeysCommand::Generate(generate) => generate.config.as_ref().map(|s| s.as_ref()), KeysCommand::List(list) => list.config.as_ref().map(|s| s.as_ref()), KeysCommand::Import(import) => import.config.as_ref().map(|s| s.as_ref()), @@ -53,6 +59,7 @@ impl Callable for KeysCommand { /// Call the given command chosen via the CLI fn call(&self) { match self { + KeysCommand::Export(export) => export.call(), KeysCommand::Generate(generate) => generate.call(), KeysCommand::Help(help) => help.call(), KeysCommand::Import(import) => import.call(), diff --git a/src/config/mod.rs b/src/config/mod.rs index fdbc46f..614dad7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -14,6 +14,7 @@ pub const CONFIG_FILE_NAME: &str = "tmkms.toml"; #[serde(deny_unknown_fields)] pub struct KmsConfig { /// Addresses of validator nodes + #[serde(default)] pub validator: Vec, /// Cryptographic signature provider configuration diff --git a/tests/cli/yubihsm/keys/export.rs b/tests/cli/yubihsm/keys/export.rs new file mode 100644 index 0000000..504957f --- /dev/null +++ b/tests/cli/yubihsm/keys/export.rs @@ -0,0 +1,14 @@ +//! Integration tests for the `yubihsm keys export` subcommand + +use crate::cli; + +#[test] +fn keys_export_command_test() { + #[allow(unused_mut)] + let mut args = vec!["yubihsm", "keys", "export", "1"]; + + #[cfg(feature = "yubihsm-mock")] + args.extend_from_slice(&["-c", super::KMS_CONFIG_PATH]); + + cli::run_successfully(args.as_slice()); +} diff --git a/tests/cli/yubihsm/keys/import.rs b/tests/cli/yubihsm/keys/import.rs index 8877145..a4f883f 100644 --- a/tests/cli/yubihsm/keys/import.rs +++ b/tests/cli/yubihsm/keys/import.rs @@ -1,27 +1,24 @@ //! Integration tests for the `yubihsm keys import` subcommand use crate::cli; +use std::str; #[test] -fn keys_import_command_test() { +fn keys_import_priv_validator_test() { #[allow(unused_mut)] let mut args = vec!["yubihsm", "keys", "import"]; #[cfg(feature = "yubihsm-mock")] args.extend_from_slice(&["-c", super::KMS_CONFIG_PATH]); - args.extend_from_slice(&["-p", super::PRIV_VALIDATOR_CONFIG_PATH]); - // key_id: - args.extend_from_slice(&["1"]); + args.extend_from_slice(&["-t", "priv_validator"]); + args.extend_from_slice(&["-i", "1"]); // key ID + args.extend_from_slice(&[super::PRIV_VALIDATOR_CONFIG_PATH]); let out = cli::run_successfully(args.as_slice()); assert_eq!(true, out.status.success()); assert_eq!(true, out.stderr.is_empty()); - assert_eq!( - true, - String::from_utf8(out.stdout) - .unwrap() - .trim() - .starts_with("Imported key #1:") - ); + + let message = str::from_utf8(&out.stdout).unwrap().trim().to_owned(); + assert_eq!(true, message.starts_with("Imported key 0x0001")); }