summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStuart Stock <stuart@int08h.com>2018-10-27 14:20:00 -0500
committerGitHub <noreply@github.com>2018-10-27 14:20:00 -0500
commitff90f0d8a4648048d94a8b13ced0f697c8df0b71 (patch)
tree890c4e1d0a60f9cf5c22eedc62521061266c38b8
parentb43bcb27ad303afd56cfe1d767e95c10cf3d1cb2 (diff)
parent86345e4538cedccae80811fd6d165a3a7a948485 (diff)
downloadroughenough-ff90f0d8a4648048d94a8b13ced0f697c8df0b71.zip
Merge pull request #12 from int08h/1.1
Merge 1.1.0
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md46
-rw-r--r--Cargo.toml19
-rw-r--r--README.md36
-rw-r--r--doc/OPTIONAL-FEATURES.md217
-rw-r--r--example.cfg1
-rw-r--r--src/bin/roughenough-client.rs87
-rw-r--r--src/bin/roughenough-kms.rs114
-rw-r--r--src/bin/roughenough-server.rs247
-rw-r--r--src/config/environment.rs70
-rw-r--r--src/config/file.rs33
-rw-r--r--src/config/memory.rs78
-rw-r--r--src/config/mod.rs67
-rw-r--r--src/error.rs20
-rw-r--r--src/key/longterm.rs69
-rw-r--r--src/key/mod.rs99
-rw-r--r--src/key/online.rs (renamed from src/keys.rs)45
-rw-r--r--src/kms/awskms.rs130
-rw-r--r--src/kms/envelope.rs289
-rw-r--r--src/kms/gcpkms.rs169
-rw-r--r--src/kms/mod.rs226
-rw-r--r--src/lib.rs45
-rw-r--r--src/merkle.rs2
-rw-r--r--src/message.rs3
-rw-r--r--src/server.rs389
-rw-r--r--src/sign.rs10
-rw-r--r--src/tag.rs6
27 files changed, 2162 insertions, 358 deletions
diff --git a/.gitignore b/.gitignore
index ff6d05d..4045cf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
Cargo.lock
target/
*.rs.bk
+example-kms.cfg
+example-gcp.cfg
+creds.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1c1a0cc
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,46 @@
+## Version 1.1.0
+
+* Optional HTTP health check (requested in #8), see the
+ [feature's documentation](https://github.com/int08h/roughenough/blob/doc/OPTIONAL-FEATURES.md#http-health-check)
+* Support AWS and Google Key Management Systems (KMS) to protect the server's long-term key.
+ See the [KMS documentation](https://github.com/int08h/roughenough/blob/doc/OPTIONAL-FEATURES.md#key-management-system-kms-support).
+* Numerous refactorings and clean ups to support fuzzing of
+ server components (b801eda, thanks to @Aaron1011)
+
+## Version 1.0.6
+
+* As pointed out in #10, the client and server binary names were too generic. Rename
+ them to be packaging friendly. Thank you @grempe. (b43bcb27ad)
+
+## Version 1.0.5
+
+* The server now supports configuration from
+ [environment variables](https://github.com/int08h/roughenough#server-configuration)
+
+## Version 1.0.4
+
+* Update `untrusted` dependency to incorporate security fix (see https://github.com/RustSec/advisory-db/pull/24).
+ Fixes #6 reported by @tirkarthi (383b0347).
+
+## Release 1.0.3
+
+* Limit the number of tags in a message to 1024 (0b8c965)
+
+## Release 1.0.2
+
+* Merge input validation and error handling improvements from #5. Fuzzing FTW.
+* Misc docstring and README updates
+* Fix incorrect range-check introduced in 9656fda and released as 1.0.1.
+
+## Release 1.0.1 (yanked)
+
+* Release 1.0.1 was removed from Github and yanked from crates.io due to a range-check bug.
+ 1.0.2 is its replacement.
+
+## Release 1.0.0
+
+Thanks to @Aaron1011's work, Roughenough has 1.0 level of functionality.
+
+* Server batches responses and signs Merkle tree root (3471e04, ee38933f, and 31bf8b3)
+* `mio` error handling improvement (613fb01f)
+* Build on Rust Nightly (350b23a) \ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index c669bfd..def762d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "roughenough"
-version = "1.0.6"
+version = "1.1.0"
repository = "https://github.com/int08h/roughenough"
authors = ["Stuart Stock <stuart@int08h.com>", "Aaron Hill <aa1ronham@gmail.com>"]
license = "Apache-2.0"
@@ -11,6 +11,11 @@ keywords = ["roughtime", "cryptography", "crypto"]
[badges]
travis-ci = { repository = "int08h/roughenough", branch = "master" }
+[features]
+default = []
+awskms = ["rusoto_core", "rusoto_kms"]
+gcpkms = ["google-cloudkms1", "hyper", "hyper-rustls", "serde", "serde_json", "yup-oauth2"]
+
[dependencies]
mio = "0.6"
mio-extras = "2.0"
@@ -25,4 +30,16 @@ ctrlc = { version = "3.1", features = ["termination"] }
clap = "2"
chrono = "0.4"
hex = "0.3"
+base64 = "0.9"
+
+rusoto_core = { version = "0.34", optional = true }
+rusoto_kms = { version = "0.34", optional = true }
+# google-cloudkms1 intentionally uses an old version of Hyper. See
+# https://github.com/Byron/google-apis-rs/issues/173 for more information.
+google-cloudkms1 = { version = "1.0.8+20181005", optional = true }
+hyper = { version = "^0.10", optional = true }
+hyper-rustls = { version = "^0.6", optional = true }
+serde = { version = "^1.0", optional = true }
+serde_json = { version = "^1.0", optional = true }
+yup-oauth2 = { version = "^1.0", optional = true }
diff --git a/README.md b/README.md
index ac9343e..d907d7a 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Requires latest stable Rust to compile. Contributions welcome, see
## Building and Running
-Requires the latest stable Rust to build.
+Requires Rust 1.28 or newer to build.
```bash
# Build roughenough
@@ -33,7 +33,7 @@ The client binary is `target/release/roughenough-client`. After building you can
binary and run on its own (no `cargo` needed) if you wish.
```bash
-$ cp target/release/roughenough-server /usr/local/bin
+$ cp target/release/roughenough-client /usr/local/bin
```
### Using the Client to Query a Roughtime Server
@@ -41,7 +41,7 @@ $ cp target/release/roughenough-server /usr/local/bin
```bash
$ target/release/roughenough-client roughtime.int08h.com 2002
Requesting time from: "roughtime.int08h.com":2002
-Received time from server: midpoint="Jul 28 2018 15:21:31", radius=1000000 (merkle_index=0, verified=false)
+Received time from server: midpoint="Oct 26 2018 23:20:44", radius=1000000, verified=No (merkle_index=0)
```
### Validating Server Responses
@@ -56,10 +56,10 @@ roughtime.int08h.com descriptive text "016e6e0284d24c37c6e4d7d8d5b4e1d3c1949ceaa
# Validate the server response using its public key
$ target/release/roughenough-client roughtime.int08h.com 2002 -p 016e6e0284d24c37c6e4d7d8d5b4e1d3c1949ceaa545bf875616c9dce0c9bec1
Requesting time from: "roughtime.int08h.com":2002
-Received time from server: midpoint="Jul 28 2018 15:26:54", radius=1000000 (merkle_index=0, verified=true)
+Received time from server: midpoint="Oct 26 2018 23:22:20", radius=1000000, verified=Yes (merkle_index=0)
```
-The **`verified=true`** in the output confirms that the server's response had a valid signature.
+The **`verified=Yes`** in the output confirms that the server's response had a valid signature.
### Server Configuration
@@ -74,9 +74,11 @@ YAML Key | Environment Variable | Necessity | Description
--- | --- | --- | ---
`interface` | `ROUGHENOUGH_INTERFACE` | Required | IP address or interface name for listening to client requests
`port` | `ROUGHENOUGH_PORT` | Required | UDP port to listen for requests
-`seed` | `ROUGHENOUGH_SEED` | Required | A 32-byte hexadecimal value used to generate the server's long-term key pair. **This is a secret value and must be un-guessable**, treat it with care.
+`seed` | `ROUGHENOUGH_SEED` | Required | A 32-byte hexadecimal value used to generate the server's long-term key pair. **This is a secret value and must be un-guessable**, treat it with care. (If compiled with KMS support, length will vary; see [Optional Features](#optional-features))
`batch_size` | `ROUGHENOUGH_BATCH_SIZE` | Optional | The maximum number of requests to process in one batch. All nonces in a batch are used to build a Merkle tree, the root of which is signed. Default is `64` requests per batch.
`status_interval` | `ROUGHENOUGH_STATUS_INTERVAL` | Optional | Number of _seconds_ between each logged status update. Default is `600` seconds (10 minutes).
+`health_check_port` | `ROUGHENOUGH_HEALTH_CHECK_PORT` | Optional | If present, enable an HTTP health check responder on the provided port. **Use with caution**, see [Optional Features](#optional-features).
+`kms_protection` | `ROUGHENOUGH_KMS_PROTECTION` | Optional | If compiled with KMS support, the ID of the KMS key used to protect the long-term identity. See [Optional Features](#optional-features).
#### YAML Configuration
@@ -110,6 +112,7 @@ $ /path/to/roughenough-server ENV
### Starting the Server
```bash
+# Build roughenough
$ cargo build --release
# Via a config file
@@ -141,6 +144,22 @@ $ cp target/release/roughenough-server /usr/local/bin
Use Ctrl-C or `kill` the process.
+
+## Optional Features
+
+Roughenough has two opt-in (disabled by default) features that are enabled either
+A) via a config setting, or B) at compile-time.
+
+* [HTTP Health Check responder](doc/OPTIONAL-FEATURES.md#http-health-check)
+ to facilitate detection and replacement of "sick" Roughenough servers.
+* [Key Management System (KMS) support](doc/OPTIONAL-FEATURES.md#key-management-system-kms-support)
+ to protect the long-term server identity using envelope encryption and
+ AWS or Google KMS.
+
+See [OPTIONAL-FEATURES.md](doc/OPTIONAL-FEATURES.md) for details and instructions
+how to enable and use.
+
+
## Limitations
Roughtime features not implemented by the server:
@@ -152,11 +171,6 @@ Roughtime features not implemented by the server:
smeared leap-seconds but time sourced from members of `pool.ntp.org` likely will not.
* Ecosystem-style response fault injection.
-Other notes:
-
-* Per-request heap allocations could probably be reduced: a few `Vec`'s could be replaced by
- lifetime scoped slices.
-
## About the Roughtime Protocol
[Roughtime](https://roughtime.googlesource.com/roughtime) is a protocol that aims to achieve rough
time synchronisation in a secure way that doesn't depend on any particular time server, and in such
diff --git a/doc/OPTIONAL-FEATURES.md b/doc/OPTIONAL-FEATURES.md
new file mode 100644
index 0000000..80310fd
--- /dev/null
+++ b/doc/OPTIONAL-FEATURES.md
@@ -0,0 +1,217 @@
+# Optional Features
+
+These features are **disabled by default** and must be explicitly enabled as
+described below.
+
+* [HTTP Health Check responder](#http-health-check)
+* [Key Management System (KMS) support](#key-management-system-kms-support)
+
+# HTTP Health Check
+
+## Description
+
+Intended for use by load balancers or other control plane facilities to monitor
+the state of Roughenough servers and remove unhealthy instances automatically.
+
+The server unconditionally emits a response to *any TCP connection* to the health
+check port, then closes the connection:
+
+```http
+HTTP/1.1 200 OK
+Content-Length: 0
+Connection: Close
+
+```
+
+No attempt is made to parse the request, the server *always* emits this response.
+
+## How to enable
+
+Provide a value for the `health_check_port` setting. This enables the HTTP
+health check responder on the configured port.
+
+```yaml
+interface: 127.0.0.1
+port: 8686
+seed: f61075c988feb9cb700a4a6a3291bfbc9cab11b9c9eca8c802468eb38a43d7d3
+health_check_port: 8000
+```
+
+## DoS Warning
+
+**An unprotected health-check port can be used to DoS the server. Do NOT expose
+the health check port to the internet!**
+
+To accurately reflect the ability of a Roughenough server to respond to requests,
+the health check socket is serviced in the same event loop executing the primary Roughtime
+protocol. Abuse of the health-check port can denial-of-service the whole server.
+
+If enabled, ensure the health check port is accessible only to the *intended load-balancer(s)
+and/or control plane components*.
+
+
+# Key Management System (KMS) Support
+
+## Description
+
+The server's long-term identity can be protected by encrypting it, storing the encrypted value
+in the configuration, and invoking a cloud key management system to temporarily decrypt
+(in memory) the long-term identity at server start-up.
+
+This way the server's long-term identity is never stored in plaintext. Instead the encrypted
+long-term identity "blob" is safe to store on disk, on Github, in a container, etc. Ability
+to access the unencrypted identity is controlled "out of band" by the KMS system.
+
+## How to enable KMS support
+
+KMS support must be compiled-in. To enable:
+
+```bash
+# Build with Google Cloud KMS support
+$ cargo build --release --features "gcpkms"
+
+# Build with AWS KMS support
+$ cargo build --release --features "awskms"
+```
+
+## Google or Amazon: choose one and one only
+
+Sadly, due to incompatibilities with dependencies of the KMS libraries, only **one**
+KMS system can be enabled at a time. Attempting `--features "awskms,gcpkms"` will result
+in a build failure.
+
+## Using `roughtime-kms` to encrypt the long-term seed
+
+Use the command line tool `roughtime-kms` to encrypt the seed value for the
+server's long-term identity. To do this you will need:
+
+ 1. The long-term key seed value
+ 2. Access credentials for your cloud of choice
+ 3. An identifier for the KMS key to be used
+ 4. Necessary permissions to perform symmetric encrypt/decrypt operations
+ using the selected key
+
+For Amazon the key identifier is an ARN in the form:
+```
+arn:aws:kms:SOME_AWS_REGION:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab
+```
+
+For Google the key identifier is a resource ID in the form:
+```
+projects/PROJECT_NAME/locations/GCP_LOCATION/keyRings/KEYRING_NAME/cryptoKeys/KEY_NAME
+```
+
+### AWS Example
+
+#### Credentials
+
+[Rusoto](https://rusoto.org/) is used by Roughenough to access AWS. If your system
+has AWS credentials in the typical `~/.aws/credentials` then everything should "just work".
+
+Otherwise Rusoto supports alternative ways to provide AWS credentials. See
+[Rusoto's documentation](https://github.com/rusoto/rusoto/blob/master/AWS-CREDENTIALS.md)
+for details.
+
+#### `roughenough-kms` Command line
+
+```bash
+# Provide AWS credentials as described in the Rusoto docs
+
+# Build roughenough with AWS KMS support
+$ cargo build --release --features "awskms"
+
+# Encrypt the seed value
+$ target/release/roughenough-kms \
+ -k arn:aws:kms:SOME_AWS_REGION:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab \
+ -s a0a31d76900080c3cdc42fe69de8dd0086d6b54de7814004befd0b9c4447757e
+
+# Output of above will be something like this
+kms_protection: "arn:aws:kms:SOME_AWS_REGION:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
+seed: b8000c000102020078d39e85c7386e9e2bed1f30fac6dd322db96b8aaac8974fc6c0e0f566f8f6c971012fca1e69fffffd947fe82a9e505baf580000007e307c06092a864886f70d010706a06f306d020100306806092a864886f70d010701301e060960864801650304012e3011040c55d16d891b3b2a1ae2587a9c020110803bcc74dd96336009087772b28ec908c40e4113b1ab9b98934bd3b4f3dd3c1e8cdc6da82a4321fd8378ad0e2e0507bf0c5ea0e28d447e5f8482533baa423b7af8459ae87736f381d87fe38c21a805fae1c25c43d59200f42cae0d07f741e787a04c0ad72774942dddf818be0767e4963fe5a810f734a0125c
+```
+
+#### Configuration
+
+Copy and paste the output `kms_protection` and `seed` values into a config or
+set the corresponding environment variables. The `roughenough-server` will detect that
+AWS KMS is being used and decrypt the seed automatically. For example:
+
+```yaml
+interface: 127.0.0.1
+port: 8686
+kms_protection: "arn:aws:kms:SOME_AWS_REGION:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
+seed: b8000c000102020078d39e85c7386e9e2bed1f30fac6dd322db96b8aaac8974fc6c0e0f566f8f6c971012fca1e69fffffd947fe82a9e505baf580000007e307c06092a864886f70d010706a06f306d020100306806092a864886f70d010701301e060960864801650304012e3011040c55d16d891b3b2a1ae2587a9c020110803bcc74dd96336009087772b28ec908c40e4113b1ab9b98934bd3b4f3dd3c1e8cdc6da82a4321fd8378ad0e2e0507bf0c5ea0e28d447e5f8482533baa423b7af8459ae87736f381d87fe38c21a805fae1c25c43d59200f42cae0d07f741e787a04c0ad72774942dddf818be0767e4963fe5a810f734a0125c
+```
+
+or using environment based configuration:
+
+```bash
+$ export ROUGHENOUGH_INTERFACE=127.0.0.1
+$ export ROUGHENOUGH_PORT=8686
+$ export ROUGHENOUGH_KMS_PROTECTION="arn:aws:kms:SOME_AWS_REGION:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
+$ export ROUGHENOUGH_SEED=b8000c000102020078d39e85c7386e9e2bed1f30fac6dd322db96b8aaac8974fc6c0e0f566f8f6c971012fca1e69fffffd947fe82a9e505baf580000007e307c06092a864886f70d010706a06f306d020100306806092a864886f70d010701301e060960864801650304012e3011040c55d16d891b3b2a1ae2587a9c020110803bcc74dd96336009087772b28ec908c40e4113b1ab9b98934bd3b4f3dd3c1e8cdc6da82a4321fd8378ad0e2e0507bf0c5ea0e28d447e5f8482533baa423b7af8459ae87736f381d87fe38c21a805fae1c25c43d59200f42cae0d07f741e787a04c0ad72774942dddf818be0767e4963fe5a810f734a0125c
+```
+
+### GCP Example
+
+#### Credentials
+
+Only **Service Account credentials** (in `.json` format) are currently supported. OAuth, bearer tokens,
+GAE default credentials, and GCE default credentials are **not** supported (contributions to
+add support are particularly welcome!).
+
+To obtain Service Account credentials if you don't already have them:
+
+* Creating a new service account?
+ 1. Create the account
+ 2. Download the credentials when prompted
+
+* Existing service account?
+ 1. Open the Cloud Console (https://console.cloud.google.com)
+ 2. Navigate to `IAM -> Service accounts`
+ 3. Locate the service account row, click on its "Actions" menu (the three dots on the right)
+ 4. Choose `Create key` and `JSON` format
+ 5. Download the credentials when prompted
+
+Make note of the full path where the credentials are saved, it's needed in the next step.
+
+#### `roughenough-kms` Command line
+
+```bash
+# Set environment variable pointing to downloaded Service Account credentials
+$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json
+
+# Build roughenough with Google KMS support
+$ cargo build --release --features "gcpkms"
+
+# Encrypt the seed value
+$ target/release/roughenough-kms \
+ -k projects/PROJECT_NAME/locations/GCP_LOCATION/keyRings/KEYRING_NAME/cryptoKeys/KEY_NAME \
+ -s a0a31d76900080c3cdc42fe69de8dd0086d6b54de7814004befd0b9c4447757e
+
+# Output of above will be something like this
+kms_protection: "projects/PROJECT_NAME/locations/GCP_LOCATION/keyRings/KEYRING_NAME/cryptoKeys/KEY_NAME"
+seed: 71000c000a2400c7f2553954873ef29aeb37384c25d7a937d389221207c3368657870129d601d084c8da1249008d6fd4640f815596788e97bb3ce02fd007bc25a1019ca51945c3b99283d3945baacd77b1b991f5f6f8848c549a5767f57c9c999e97fe6d28fdb17db1d63c2ea966d8236d20c71e8e9c757c5bab62472c65b48376bc8951700aceb22545fce58d77e7cc147f7134da7a2cca790b54f29e4798442cee6e0d34e57f80ce983f7e5928cceff2
+```
+
+#### Configuration
+
+Copy and paste the output `kms_protection` and `seed` values into a config or
+set the corresponding environment variables. `roughenough-server` will detect that
+Google KMS is being used and decrypt the seed automatically. For example:
+
+```yaml
+interface: 127.0.0.1
+port: 8686
+kms_protection: "projects/PROJECT_NAME/locations/GCP_LOCATION/keyRings/KEYRING_NAME/cryptoKeys/KEY_NAME"
+seed: 71000c000a2400c7f2553954873ef29aeb37384c25d7a937d389221207c3368657870129d601d084c8da1249008d6fd4640f815596788e97bb3ce02fd007bc25a1019ca51945c3b99283d3945baacd77b1b991f5f6f8848c549a5767f57c9c999e97fe6d28fdb17db1d63c2ea966d8236d20c71e8e9c757c5bab62472c65b48376bc8951700aceb22545fce58d77e7cc147f7134da7a2cca790b54f29e4798442cee6e0d34e57f80ce983f7e5928cceff2
+```
+
+or using environment based configuration:
+
+```bash
+$ export ROUGHENOUGH_INTERFACE=127.0.0.1
+$ export ROUGHENOUGH_PORT=8686
+$ export ROUGHENOUGH_KMS_PROTECTION="projects/PROJECT_NAME/locations/GCP_LOCATION/keyRings/KEYRING_NAME/cryptoKeys/KEY_NAME"
+$ export ROUGHENOUGH_SEED=71000c000a2400c7f2553954873ef29aeb37384c25d7a937d389221207c3368657870129d601d084c8da1249008d6fd4640f815596788e97bb3ce02fd007bc25a1019ca51945c3b99283d3945baacd77b1b991f5f6f8848c549a5767f57c9c999e97fe6d28fdb17db1d63c2ea966d8236d20c71e8e9c757c5bab62472c65b48376bc8951700aceb22545fce58d77e7cc147f7134da7a2cca790b54f29e4798442cee6e0d34e57f80ce983f7e5928cceff2
+```
diff --git a/example.cfg b/example.cfg
index c271481..0cd87f8 100644
--- a/example.cfg
+++ b/example.cfg
@@ -1,3 +1,4 @@
port: 8686
interface: 127.0.0.1
seed: a32049da0ffde0ded92ce10a0230d35fe615ec8461c14986baa63fe3b3bac3db
+health_check_port: 8000
diff --git a/src/bin/roughenough-client.rs b/src/bin/roughenough-client.rs
index 75ebe14..55831e1 100644
--- a/src/bin/roughenough-client.rs
+++ b/src/bin/roughenough-client.rs
@@ -28,13 +28,17 @@ use chrono::offset::Utc;
use chrono::TimeZone;
use std::collections::HashMap;
+use std::fs::File;
+use std::io::Write;
use std::iter::Iterator;
-use std::net::{ToSocketAddrs, UdpSocket};
+use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
use clap::{App, Arg};
use roughenough::merkle::root_from_paths;
use roughenough::sign::Verifier;
-use roughenough::{RtMessage, Tag, CERTIFICATE_CONTEXT, SIGNED_RESPONSE_CONTEXT, VERSION};
+use roughenough::{
+ roughenough_version, RtMessage, Tag, CERTIFICATE_CONTEXT, SIGNED_RESPONSE_CONTEXT,
+};
fn create_nonce() -> [u8; 64] {
let rng = rand::SystemRandom::new();
@@ -59,6 +63,21 @@ fn receive_response(sock: &mut UdpSocket) -> RtMessage {
RtMessage::from_bytes(&buf[0..resp_len]).unwrap()
}
+fn stress_test_forever(addr: &SocketAddr) -> ! {
+ if !addr.ip().is_loopback() {
+ panic!("Cannot use non-loopback address {} for stress testing", addr.ip());
+ }
+
+ println!("Stress testing!");
+
+ let nonce = create_nonce();
+ let socket = UdpSocket::bind("0.0.0.0:0").expect("Couldn't open UDP socket");
+ let request = make_request(&nonce);
+ loop {
+ socket.send_to(&request, addr).unwrap();
+ }
+}
+
struct ResponseHandler {
pub_key: Option<Vec<u8>>,
msg: HashMap<Tag, Vec<u8>>,
@@ -106,15 +125,16 @@ impl ResponseHandler {
.as_slice()
.read_u32::<LittleEndian>()
.unwrap();
- let mut verified = false;
- if self.pub_key.is_some() {
+ let verified = if self.pub_key.is_some() {
self.validate_dele();
self.validate_srep();
self.validate_merkle();
self.validate_midpoint(midpoint);
- verified = true;
- }
+ true
+ } else {
+ false
+ };
ParsedResponse {
verified,
@@ -133,7 +153,7 @@ impl ResponseHandler {
&self.cert[&Tag::SIG],
&full_cert
),
- "Invalid signature on DELE tag!"
+ "Invalid signature on DELE tag, response may not be authentic"
);
}
@@ -143,7 +163,7 @@ impl ResponseHandler {
assert!(
self.validate_sig(&self.dele[&Tag::PUBK], &self.msg[&Tag::SIG], &full_srep),
- "Invalid signature on SREP tag!"
+ "Invalid signature on SREP tag, response may not be authentic"
);
}
@@ -160,9 +180,8 @@ impl ResponseHandler {
let hash = root_from_paths(index as usize, &self.nonce, paths);
assert_eq!(
- Vec::from(hash),
- srep[&Tag::ROOT],
- "Nonce not in merkle tree!"
+ hash, srep[&Tag::ROOT],
+ "Nonce is not present in the response's merkle tree"
);
}
@@ -178,11 +197,13 @@ impl ResponseHandler {
assert!(
midpoint >= mint,
- "Response midpoint {} lies before delegation span ({}, {})"
+ "Response midpoint {} lies *before* delegation span ({}, {})",
+ midpoint, mint, maxt
);
assert!(
midpoint <= maxt,
- "Response midpoint {} lies after delegation span ({}, {})"
+ "Response midpoint {} lies *after* delegation span ({}, {})",
+ midpoint, mint, maxt
);
}
@@ -195,7 +216,7 @@ impl ResponseHandler {
fn main() {
let matches = App::new("roughenough client")
- .version(VERSION)
+ .version(roughenough_version().as_ref())
.arg(Arg::with_name("host")
.required(true)
.help("The Roughtime server to connect to")
@@ -228,6 +249,12 @@ fn main() {
.long("stress")
.help("Stress-tests the server by sending the same request as fast as possible. Please only use this on your own server")
)
+ .arg(Arg::with_name("output")
+ .short("o")
+ .long("output")
+ .takes_value(true)
+ .help("Writes all requsts to the specified file, in addition to sending them to the server. Useful for generating fuzer inputs")
+ )
.get_matches();
let host = matches.value_of("host").unwrap();
@@ -238,42 +265,32 @@ fn main() {
let pub_key = matches
.value_of("public-key")
.map(|pkey| hex::decode(pkey).expect("Error parsing public key!"));
+ let out = matches.value_of("output");
println!("Requesting time from: {:?}:{:?}", host, port);
let addr = (host, port).to_socket_addrs().unwrap().next().unwrap();
if stress {
- if !addr.ip().is_loopback() {
- println!(
- "ERROR: Cannot use non-loopback address {} for stress testing",
- addr.ip()
- );
- return;
- }
-
- println!("Stress-testing!");
-
- let nonce = create_nonce();
- let socket = UdpSocket::bind("0.0.0.0:0").expect("Couldn't open UDP socket");
- let request = make_request(&nonce);
-
- loop {
- socket.send_to(&request, addr).unwrap();
- }
+ stress_test_forever(&addr)
}
let mut requests = Vec::with_capacity(num_requests);
+ let mut file = out.map(|o| File::create(o).expect("Failed to create file!"));
for _ in 0..num_requests {
let nonce = create_nonce();
let mut socket = UdpSocket::bind("0.0.0.0:0").expect("Couldn't open UDP socket");
let request = make_request(&nonce);
+ if let Some(f) = file.as_mut() {
+ f.write_all(&request).expect("Failed to write to file!")
+ }
+
requests.push((nonce, request, socket));
}
- for &mut (_, ref request, ref mut socket) in requests.iter_mut() {
+ for &mut (_, ref request, ref mut socket) in &mut requests {
socket.send_to(request, addr).unwrap();
}
@@ -296,10 +313,12 @@ fn main() {
let nsecs = (midpoint - (seconds * 10_u64.pow(6))) * 10_u64.pow(3);
let spec = Utc.timestamp(seconds as i64, nsecs as u32);
let out = spec.format(time_format).to_string();
+ let verify_str = if verified { "Yes" } else { "No" };
println!(
- "Received time from server: midpoint={:?}, radius={:?} (merkle_index={}, verified={})",
- out, radius, index, verified
+ "Received time from server: midpoint={:?}, radius={:?}, verified={} (merkle_index={})",
+ out, radius, verify_str, index
);
}
}
+
diff --git a/src/bin/roughenough-kms.rs b/src/bin/roughenough-kms.rs
new file mode 100644
index 0000000..d1cc4a6
--- /dev/null
+++ b/src/bin/roughenough-kms.rs
@@ -0,0 +1,114 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//!
+//! CLI used to encrypt the Roughenough long-term key using one of the KMS implementations
+//!
+
+extern crate clap;
+#[macro_use]
+extern crate log;
+extern crate hex;
+extern crate ring;
+extern crate roughenough;
+extern crate simple_logger;
+extern crate untrusted;
+
+use clap::{App, Arg};
+use roughenough::roughenough_version;
+
+#[cfg(feature = "awskms")]
+fn aws_kms(kms_key: &str, plaintext_seed: &[u8]) {
+ use roughenough::kms::{AwsKms, EnvelopeEncryption};
+
+ let client = AwsKms::from_arn(kms_key).unwrap();
+
+ match EnvelopeEncryption::encrypt_seed(&client, &plaintext_seed) {
+ Ok(encrypted_blob) => {
+ println!("kms_protection: \"{}\"", kms_key);
+ println!("seed: {}", hex::encode(&encrypted_blob));
+ }
+ Err(e) => {
+ error!("Error: {:?}", e);
+ }
+ }
+}
+
+#[cfg(feature = "gcpkms")]
+fn gcp_kms(kms_key: &str, plaintext_seed: &[u8]) {
+ use roughenough::kms::{EnvelopeEncryption, GcpKms};
+
+ let client = GcpKms::from_resource_id(kms_key).unwrap();
+
+ match EnvelopeEncryption::encrypt_seed(&client, &plaintext_seed) {
+ Ok(encrypted_blob) => {
+ println!("kms_protection: \"{}\"", kms_key);
+ println!("seed: {}", hex::encode(&encrypted_blob));
+ }
+ Err(e) => {
+ error!("Error: {:?}", e);
+ }
+ }
+}
+
+#[allow(unused_variables)]
+pub fn main() {
+ use log::Level;
+
+ simple_logger::init_with_level(Level::Info).unwrap();
+
+ let matches = App::new("roughenough-kms")
+ .version(roughenough_version().as_ref())
+ .long_about("Encrypt a Roughenough server's long-term seed using a KMS")
+ .arg(
+ Arg::with_name("KEY_ID")
+ .short("k")
+ .long("kms-key")
+ .takes_value(true)
+ .required(true)
+ .help("Identity of the KMS key to be used"),
+ ).arg(
+ Arg::with_name("SEED")
+ .short("s")
+ .long("seed")
+ .takes_value(true)
+ .required(true)
+ .help("32 byte hex seed for the server's long-term identity"),
+ ).get_matches();
+
+ let kms_key = matches.value_of("KEY_ID").unwrap();
+ let plaintext_seed = matches
+ .value_of("SEED")
+ .map(|seed| hex::decode(seed).expect("Error parsing seed value"))
+ .unwrap();
+
+ if plaintext_seed.len() != 32 {
+ error!(
+ "Seed must be 32 bytes long; provided seed is {}",
+ plaintext_seed.len()
+ );
+ return;
+ }
+
+ if cfg!(feature = "awskms") {
+ #[cfg(feature = "awskms")]
+ aws_kms(kms_key, &plaintext_seed);
+ } else if cfg!(feature = "gcpkms") {
+ #[cfg(feature = "gcpkms")]
+ gcp_kms(kms_key, &plaintext_seed);
+ } else {
+ warn!("KMS support was not compiled, nothing to do.");
+ warn!("For information on KMS support see the Roughenough documentation.");
+ }
+}
diff --git a/src/bin/roughenough-server.rs b/src/bin/roughenough-server.rs
index 13a7026..5893f12 100644
--- a/src/bin/roughenough-server.rs
+++ b/src/bin/roughenough-server.rs
@@ -16,14 +16,8 @@
//! Roughtime server
//!
//! # Configuration
-//! The `roughenough` server is configured via a YAML config file. See the documentation
-//! for [FileConfig](struct.FileConfig.html) for details.
-//!
-//! To run the server:
-//!
-//! ```bash
-//! $ cargo run --release --bin server /path/to/config.file
-//! ```
+//! The server has multiple ways it can be configured, see
+//! [`ServerConfig`](config/trait.ServerConfig.html) for details.
//!
extern crate byteorder;
@@ -41,199 +35,60 @@ extern crate untrusted;
extern crate yaml_rust;
use std::env;
-use std::io::ErrorKind;
use std::process;
-use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
-use std::sync::Arc;
-use std::time::Duration;
-
-use mio::net::UdpSocket;
-use mio::{Events, Poll, PollOpt, Ready, Token};
-use mio_extras::timer::Timer;
-
-use byteorder::{LittleEndian, WriteBytesExt};
+use std::sync::atomic::Ordering;
use roughenough::config;
use roughenough::config::ServerConfig;
-use roughenough::keys::{LongTermKey, OnlineKey};
-use roughenough::merkle::MerkleTree;
-use roughenough::{Error, RtMessage, Tag};
-use roughenough::{MIN_REQUEST_LENGTH, VERSION};
-
-const MESSAGE: Token = Token(0);
-const STATUS: Token = Token(1);
-
-fn make_response(srep: &RtMessage, cert_bytes: &[u8], path: &[u8], idx: u32) -> RtMessage {
- let mut index = [0; 4];
- (&mut index as &mut [u8])
- .write_u32::<LittleEndian>(idx)
- .unwrap();
-
- let sig_bytes = srep.get_field(Tag::SIG).unwrap();
- let srep_bytes = srep.get_field(Tag::SREP).unwrap();
-
- let mut response = RtMessage::new(5);
- response.add_field(Tag::SIG, sig_bytes).unwrap();
- response.add_field(Tag::PATH, path).unwrap();
- response.add_field(Tag::SREP, srep_bytes).unwrap();
- response.add_field(Tag::CERT, cert_bytes).unwrap();
- response.add_field(Tag::INDX, &index).unwrap();
-
- response
+use roughenough::roughenough_version;
+use roughenough::server::Server;
+
+macro_rules! check_ctrlc {
+ ($keep_running:expr) => {
+ if !$keep_running.load(Ordering::Acquire) {
+ warn!("Ctrl-C caught, exiting...");
+ return;
+ }
+ };
}
-// extract the client's nonce from its request
-fn nonce_from_request(buf: &[u8], num_bytes: usize) -> Result<&[u8], Error> {
- if num_bytes < MIN_REQUEST_LENGTH as usize {
- return Err(Error::RequestTooShort);
+fn polling_loop(config: Box<ServerConfig>) {
+ let mut server = Server::new(config);
+
+ info!("Long-term public key : {}", server.get_public_key());
+ info!("Online public key : {}", server.get_online_key());
+ info!(
+ "Max response batch size : {}",
+ server.get_config().batch_size()
+ );
+ info!(
+ "Status updates every : {} seconds",
+ server.get_config().status_interval().as_secs()
+ );
+ info!(
+ "Server listening on : {}:{}",
+ server.get_config().interface(),
+ server.get_config().port()
+ );
+
+ if let Some(hc_port) = server.get_config().health_check_port() {
+ info!(
+ "TCP health check : {}:{}",
+ server.get_config().interface(),
+ hc_port
+ );
}
- let tag_count = &buf[..4];
- let expected_nonc = &buf[8..12];
- let expected_pad = &buf[12..16];
-
- let tag_count_is_2 = tag_count == [0x02, 0x00, 0x00, 0x00];
- let tag1_is_nonc = expected_nonc == Tag::NONC.wire_value();
- let tag2_is_pad = expected_pad == Tag::PAD.wire_value();
-
- if tag_count_is_2 && tag1_is_nonc && tag2_is_pad {
- Ok(&buf[0x10..0x50])
- } else {
- Err(Error::InvalidRequest)
- }
-}
-
-fn polling_loop(config: &Box<ServerConfig>, online_key: &mut OnlineKey, cert_bytes: &[u8]) {
- let response_counter = AtomicUsize::new(0);
- let keep_running = Arc::new(AtomicBool::new(true));
- let kr = keep_running.clone();
+ let kr = server.get_keep_running();
+ let kr_new = kr.clone();
ctrlc::set_handler(move || kr.store(false, Ordering::Release))
.expect("failed setting Ctrl-C handler");
- let sock_addr = config.socket_addr().expect("");
- let socket = UdpSocket::bind(&sock_addr).expect("failed to bind to socket");
- let poll_duration = Some(Duration::from_millis(100));
-
- let mut timer: Timer<()> = Timer::default();
- timer.set_timeout(config.status_interval(), ());
-
- let mut buf = [0u8; 65_536];
- let mut events = Events::with_capacity(32);
- let mut num_bad_requests = 0u64;
-
- let poll = Poll::new().unwrap();
- poll.register(&socket, MESSAGE, Ready::readable(), PollOpt::edge())
- .unwrap();
- poll.register(&timer, STATUS, Ready::readable(), PollOpt::edge())
- .unwrap();
-
- let mut merkle = MerkleTree::new();
- let mut requests = Vec::with_capacity(config.batch_size() as usize);
-
- macro_rules! check_ctrlc {
- () => {
- if !keep_running.load(Ordering::Acquire) {
- warn!("Ctrl-C caught, exiting...");
- return;
- }
- };
- }
-
loop {
- check_ctrlc!();
-
- poll.poll(&mut events, poll_duration).expect("poll failed");
-
- for event in events.iter() {
- match event.token() {
- MESSAGE => {
- let mut done = false;
-
- 'process_batch: loop {
- check_ctrlc!();
-
- merkle.reset();
- requests.clear();
-
- let resp_start = response_counter.load(Ordering::SeqCst);
-
- for i in 0..config.batch_size() {
- match socket.recv_from(&mut buf) {
- Ok((num_bytes, src_addr)) => {
- if let Ok(nonce) = nonce_from_request(&buf, num_bytes) {
- requests.push((Vec::from(nonce), src_addr));
- merkle.push_leaf(nonce);
- } else {
- num_bad_requests += 1;
- info!(
- "Invalid request ({} bytes) from {} (#{} in batch, resp #{})",
- num_bytes, src_addr, i, resp_start + i as usize
- );
- }
- }
- Err(e) => match e.kind() {
- ErrorKind::WouldBlock => {
- done = true;
- break;
- }
- _ => {
- error!(
- "Error receiving from socket: {:?}: {:?}",
- e.kind(),
- e
- );
- break;
- }
- },
- };
- }
-
- if requests.is_empty() {
- break 'process_batch;
- }
-
- let merkle_root = merkle.compute_root();
- let srep = online_key.make_srep(time::get_time(), &merkle_root);
-
- for (i, &(ref nonce, ref src_addr)) in requests.iter().enumerate() {
- let paths = merkle.get_paths(i);
-
- let resp = make_response(&srep, cert_bytes, &paths, i as u32);
- let resp_bytes = resp.encode().unwrap();
-
- let bytes_sent = socket
- .send_to(&resp_bytes, &src_addr)
- .expect("send_to failed");
- let num_responses = response_counter.fetch_add(1, Ordering::SeqCst);
-
- info!(
- "Responded {} bytes to {} for '{}..' (#{} in batch, resp #{})",
- bytes_sent,
- src_addr,
- hex::encode(&nonce[0..4]),
- i,
- num_responses
- );
- }
- if done {
- break 'process_batch;
- }
- }
- }
-
- STATUS => {
- info!(
- "responses {}, invalid requests {}",
- response_counter.load(Ordering::SeqCst),
- num_bad_requests
- );
-
- timer.set_timeout(config.status_interval(), ());
- }
-
- _ => unreachable!(),
- }
+ check_ctrlc!(kr_new);
+ if server.process_events() {
+ return;
}
}
}
@@ -243,7 +98,7 @@ pub fn main() {
simple_logger::init_with_level(Level::Info).unwrap();
- info!("Roughenough server v{} starting", VERSION);
+ info!("Roughenough server v{} starting", roughenough_version());
let mut args = env::args();
if args.len() != 2 {
@@ -256,22 +111,12 @@ pub fn main() {
Err(e) => {
error!("{:?}", e);
process::exit(1)
- },
+ }
Ok(ref cfg) if !config::is_valid_config(&cfg) => process::exit(1),
Ok(cfg) => cfg,
};
- let mut online_key = OnlineKey::new();
- let mut long_term_key = LongTermKey::new(config.seed());
- let cert_bytes = long_term_key.make_cert(&online_key).encode().unwrap();
-
- info!("Long-term public key : {}", long_term_key);
- info!("Online public key : {}", online_key);
- info!("Max response batch size : {}", config.batch_size());
- info!("Status updates every : {} seconds", config.status_interval().as_secs());
- info!("Server listening on : {}:{}", config.interface(), config.port());
-
- polling_loop(&config, &mut online_key, &cert_bytes);
+ polling_loop(config);
info!("Done.");
process::exit(0);
diff --git a/src/config/environment.rs b/src/config/environment.rs
index 5053517..fa96185 100644
--- a/src/config/environment.rs
+++ b/src/config/environment.rs
@@ -15,24 +15,26 @@
extern crate hex;
use std::env;
-use std::net::SocketAddr;
use std::time::Duration;
use config::ServerConfig;
use config::{DEFAULT_BATCH_SIZE, DEFAULT_STATUS_INTERVAL};
+use key::KmsProtection;
use Error;
///
/// Obtain a Roughenough server configuration ([ServerConfig](trait.ServerConfig.html))
/// from environment variables.
///
-/// Config parameter | Environment Variable
-/// ---------------- | --------------------
-/// port | `ROUGHENOUGH_PORT`
-/// interface | `ROUGHENOUGH_INTERFACE`
-/// seed | `ROUGHENOUGH_SEED`
-/// batch_size | `ROUGHENOUGH_BATCH_SIZE`
-/// status_interval | `ROUGHENOUGH_STATUS_INTERVAL`
+/// Config parameter | Environment Variable
+/// ---------------- | --------------------
+/// port | `ROUGHENOUGH_PORT`
+/// interface | `ROUGHENOUGH_INTERFACE`
+/// seed | `ROUGHENOUGH_SEED`
+/// batch_size | `ROUGHENOUGH_BATCH_SIZE`
+/// status_interval | `ROUGHENOUGH_STATUS_INTERVAL`
+/// kms_protection | `ROUGHENOUGH_KMS_PROTECTION`
+/// health_check_port | `ROUGHENOUGH_HEALTH_CHECK_PORT`
///
pub struct EnvironmentConfig {
port: u16,
@@ -40,6 +42,8 @@ pub struct EnvironmentConfig {
seed: Vec<u8>,
batch_size: u8,
status_interval: Duration,
+ kms_protection: KmsProtection,
+ health_check_port: Option<u16>,
}
const ROUGHENOUGH_PORT: &str = "ROUGHENOUGH_PORT";
@@ -47,6 +51,8 @@ const ROUGHENOUGH_INTERFACE: &str = "ROUGHENOUGH_INTERFACE";
const ROUGHENOUGH_SEED: &str = "ROUGHENOUGH_SEED";
const ROUGHENOUGH_BATCH_SIZE: &str = "ROUGHENOUGH_BATCH_SIZE";
const ROUGHENOUGH_STATUS_INTERVAL: &str = "ROUGHENOUGH_STATUS_INTERVAL";
+const ROUGHENOUGH_KMS_PROTECTION: &str = "ROUGHENOUGH_KMS_PROTECTION";
+const ROUGHENOUGH_HEALTH_CHECK_PORT: &str = "ROUGHENOUGH_HEALTH_CHECK_PORT";
impl EnvironmentConfig {
pub fn new() -> Result<Self, Error> {
@@ -56,12 +62,14 @@ impl EnvironmentConfig {
seed: Vec::new(),
batch_size: DEFAULT_BATCH_SIZE,
status_interval: DEFAULT_STATUS_INTERVAL,
+ kms_protection: KmsProtection::Plaintext,
+ health_check_port: None,
};
if let Ok(port) = env::var(ROUGHENOUGH_PORT) {
cfg.port = port
.parse()
- .expect(format!("invalid port: {}", port).as_ref());
+ .unwrap_or_else(|_| panic!("invalid port: {}", port));
};
if let Ok(interface) = env::var(ROUGHENOUGH_INTERFACE) {
@@ -69,26 +77,36 @@ impl EnvironmentConfig {
};
if let Ok(seed) = env::var(ROUGHENOUGH_SEED) {
- cfg.seed = hex::decode(&seed).expect(
- format!(
- "invalid seed value: {}\n'seed' should be 32 byte hex value",
- seed
- ).as_ref(),
- );
+ cfg.seed =
+ hex::decode(&seed).expect("invalid seed value; 'seed' should be a hex value");
};
if let Ok(batch_size) = env::var(ROUGHENOUGH_BATCH_SIZE) {
cfg.batch_size = batch_size
.parse()
- .expect(format!("invalid batch_size: {}", batch_size).as_ref());
+ .unwrap_or_else(|_| panic!("invalid batch_size: {}", batch_size));
};
if let Ok(status_interval) = env::var(ROUGHENOUGH_STATUS_INTERVAL) {
let val: u16 = status_interval
.parse()
- .expect(format!("invalid status_interval: {}", status_interval).as_ref());
+ .unwrap_or_else(|_| panic!("invalid status_interval: {}", status_interval));
- cfg.status_interval = Duration::from_secs(val as u64);
+ cfg.status_interval = Duration::from_secs(u64::from(val));
+ };
+
+ if let Ok(kms_protection) = env::var(ROUGHENOUGH_KMS_PROTECTION) {
+ cfg.kms_protection = kms_protection
+ .parse()
+ .unwrap_or_else(|_| panic!("invalid kms_protection value: {}", kms_protection));
+ }
+
+ if let Ok(health_check_port) = env::var(ROUGHENOUGH_HEALTH_CHECK_PORT) {
+ let val: u16 = health_check_port
+ .parse()
+ .unwrap_or_else(|_| panic!("invalid health_check_port: {}", health_check_port));
+
+ cfg.health_check_port = Some(val);
};
Ok(cfg)
@@ -104,8 +122,8 @@ impl ServerConfig for EnvironmentConfig {
self.port
}
- fn seed(&self) -> &[u8] {
- &self.seed
+ fn seed(&self) -> Vec<u8> {
+ self.seed.clone()
}
fn batch_size(&self) -> u8 {
@@ -116,11 +134,11 @@ impl ServerConfig for EnvironmentConfig {
self.status_interval
}
- fn socket_addr(&self) -> Result<SocketAddr, Error> {
- let addr = format!("{}:{}", self.interface, self.port);
- match addr.parse() {
- Ok(v) => Ok(v),
- Err(_) => Err(Error::InvalidConfiguration(addr)),
- }
+ fn kms_protection(&self) -> &KmsProtection {
+ &self.kms_protection
+ }
+
+ fn health_check_port(&self) -> Option<u16> {
+ self.health_check_port
}
}
diff --git a/src/config/file.rs b/src/config/file.rs
index e93ee99..d3ec64a 100644
--- a/src/config/file.rs
+++ b/src/config/file.rs
@@ -16,12 +16,12 @@ extern crate hex;
use std::fs::File;
use std::io::Read;
-use std::net::SocketAddr;
use std::time::Duration;
use yaml_rust::YamlLoader;
use config::ServerConfig;
use config::{DEFAULT_BATCH_SIZE, DEFAULT_STATUS_INTERVAL};
+use key::KmsProtection;
use Error;
///
@@ -42,6 +42,8 @@ pub struct FileConfig {
seed: Vec<u8>,
batch_size: u8,
status_interval: Duration,
+ kms_protection: KmsProtection,
+ health_check_port: Option<u16>,
}
impl FileConfig {
@@ -67,6 +69,8 @@ impl FileConfig {
seed: Vec::new(),
batch_size: DEFAULT_BATCH_SIZE,
status_interval: DEFAULT_STATUS_INTERVAL,
+ kms_protection: KmsProtection::Plaintext,
+ health_check_port: None,
};
for (key, value) in cfg[0].as_hash().unwrap() {
@@ -83,6 +87,17 @@ impl FileConfig {
let val = value.as_i64().expect("status_interval value invalid");
config.status_interval = Duration::from_secs(val as u64)
}
+ "kms_protection" => {
+ let val =
+ value.as_str().unwrap().parse().unwrap_or_else(|_| {
+ panic!("invalid kms_protection value: {:?}", value)
+ });
+ config.kms_protection = val
+ }
+ "health_check_port" => {
+ let val = value.as_i64().unwrap() as u16;
+ config.health_check_port = Some(val);
+ }
unknown => {
return Err(Error::InvalidConfiguration(format!(
"unknown config key: {}",
@@ -105,8 +120,8 @@ impl ServerConfig for FileConfig {
self.port
}
- fn seed(&self) -> &[u8] {
- &self.seed
+ fn seed(&self) -> Vec<u8> {
+ self.seed.clone()
}
fn batch_size(&self) -> u8 {
@@ -117,11 +132,11 @@ impl ServerConfig for FileConfig {
self.status_interval
}
- fn socket_addr(&self) -> Result<SocketAddr, Error> {
- let addr = format!("{}:{}", self.interface, self.port);
- match addr.parse() {
- Ok(v) => Ok(v),
- Err(_) => Err(Error::InvalidConfiguration(addr)),
- }
+ fn kms_protection(&self) -> &KmsProtection {
+ &self.kms_protection
+ }
+
+ fn health_check_port(&self) -> Option<u16> {
+ self.health_check_port
}
}
diff --git a/src/config/memory.rs b/src/config/memory.rs
new file mode 100644
index 0000000..e3aae7e
--- /dev/null
+++ b/src/config/memory.rs
@@ -0,0 +1,78 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use config::ServerConfig;
+use config::{DEFAULT_BATCH_SIZE, DEFAULT_STATUS_INTERVAL};
+use key::KmsProtection;
+use std::time::Duration;
+
+use hex;
+
+/// A purely in-memory Roughenough config for testing purposes.
+///
+/// This is useful for testing or fuzzing a server without the need to create additional files.
+pub struct MemoryConfig {
+ pub port: u16,
+ pub interface: String,
+ pub seed: Vec<u8>,
+ pub batch_size: u8,
+ pub status_interval: Duration,
+ pub kms_protection: KmsProtection,
+ pub health_check_port: Option<u16>,
+}
+
+impl MemoryConfig {
+ pub fn new(port: u16) -> MemoryConfig {
+ MemoryConfig {
+ port,
+ interface: "127.0.0.1".to_string(),
+ seed: hex::decode("a32049da0ffde0ded92ce10a0230d35fe615ec8461c14986baa63fe3b3bac3db")
+ .unwrap(),
+ batch_size: DEFAULT_BATCH_SIZE,
+ status_interval: DEFAULT_STATUS_INTERVAL,
+ kms_protection: KmsProtection::Plaintext,
+ health_check_port: None,
+ }
+ }
+}
+
+impl ServerConfig for MemoryConfig {
+ fn interface(&self) -> &str {
+ self.interface.as_ref()
+ }
+
+ fn port(&self) -> u16 {
+ self.port
+ }
+
+ fn seed(&self) -> Vec<u8> {
+ self.seed.clone()
+ }
+
+ fn batch_size(&self) -> u8 {
+ self.batch_size
+ }
+
+ fn status_interval(&self) -> Duration {
+ self.status_interval
+ }
+
+ fn kms_protection(&self) -> &KmsProtection {
+ &self.kms_protection
+ }
+
+ fn health_check_port(&self) -> Option<u16> {
+ self.health_check_port
+ }
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 983338c..b73892f 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -18,7 +18,8 @@
//! The [ServerConfig](trait.ServerConfig.html) trait specifies the required and optional
//! parameters available for configuring a Roughenoguh server instance.
//!
-//! Implementations of `ServerConfig` obtain configurations from different back-end sources.
+//! Implementations of `ServerConfig` obtain configurations from different back-end sources
+//! such as files or environment variables.
//!
extern crate hex;
@@ -28,13 +29,15 @@ use std::net::SocketAddr;
use std::time::Duration;
mod file;
-
pub use self::file::FileConfig;
mod environment;
-
pub use self::environment::EnvironmentConfig;
+mod memory;
+pub use self::memory::MemoryConfig;
+
+use key::KmsProtection;
use Error;
/// Maximum number of requests to process in one batch and include the the Merkle tree.
@@ -53,15 +56,19 @@ pub const DEFAULT_STATUS_INTERVAL: Duration = Duration::from_secs(600);
/// --- | --- | --- | ---
/// `interface` | `ROUGHENOUGH_INTERFACE` | Required | IP address or interface name for listening to client requests
/// `port` | `ROUGHENOUGH_PORT` | Required | UDP port to listen for requests
-/// `seed` | `ROUGHENOUGH_SEED` | Required | A 32-byte hexadecimal value used to generate the server's long-term key pair. **This is a secret value and must be un-guessable**, treat it with care.
-/// `batch_size` | `ROUGHENOUGH_BATCH_SIZE` | Optional | The maximum number of requests to process in one batch. All nonces in a batch are used to build a Merkle tree, the root of which is signed. Defaults to [DEFAULT_BATCH_SIZE](constant.DEFAULT_BATCH_SIZE.html) requests per batch.
-/// `status_interval` | `ROUGHENOUGH_STATUS_INTERVAL` | Optional | Number of _seconds_ between each logged status update. Default value is [DEFAULT_STATUS_INTERVAL](constant.DEFAULT_STATUS_INTERVAL.html).
+/// `seed` | `ROUGHENOUGH_SEED` | Required | A 32-byte hexadecimal value used to generate the server's long-term key pair. **This is a secret value and must be un-guessable**, treat it with care. (If compiled with KMS support, length will vary)
+/// `batch_size` | `ROUGHENOUGH_BATCH_SIZE` | Optional | The maximum number of requests to process in one batch. All nonces in a batch are used to build a Merkle tree, the root of which is signed. Default is `64` requests per batch.
+/// `status_interval` | `ROUGHENOUGH_STATUS_INTERVAL` | Optional | Number of _seconds_ between each logged status update. Default is `600` seconds (10 minutes).
+/// `health_check_port` | `ROUGHENOUGH_HEALTH_CHECK_PORT` | Optional | If present, enable an HTTP health check responder on the provided port. **Use with caution**.
+/// `kms_protection` | `ROUGHENOUGH_KMS_PROTECTION` | Optional | If compiled with KMS support, the ID of the KMS key used to protect the long-term identity.
///
/// Implementations of this trait obtain a valid configuration from different back-end
/// sources. See:
/// * [FileConfig](struct.FileConfig.html) - configure via a YAML file
/// * [EnvironmentConfig](struct.EnvironmentConfig.html) - configure via environment vars
///
+/// The health check and KMS features require
+///
pub trait ServerConfig {
/// [Required] IP address or interface name to listen for client requests
fn interface(&self) -> &str;
@@ -72,7 +79,7 @@ pub trait ServerConfig {
/// [Required] A 32-byte hexadecimal value used to generate the server's
/// long-term key pair. **This is a secret value and must be un-guessable**,
/// treat it with care.
- fn seed(&self) -> &[u8];
+ fn seed(&self) -> Vec<u8>;
/// [Optional] The maximum number of requests to process in one batch. All
/// nonces in a batch are used to build a Merkle tree, the root of which is signed.
@@ -83,12 +90,30 @@ pub trait ServerConfig {
/// Defaults to [DEFAULT_STATUS_INTERVAL](constant.DEFAULT_STATUS_INTERVAL.html)
fn status_interval(&self) -> Duration;
+ /// [Optional] Method used to protect the seed for the server's long-term key pair.
+ /// Defaults to "`plaintext`" (no encryption, seed is in the clear).
+ fn kms_protection(&self) -> &KmsProtection;
+
+ /// [Optional] If present, the TCP port to respond to Google-style HTTP "legacy health check".
+ /// This is a *very* simplistic check, it emits a fixed HTTP response to all TCP connections.
+ /// https://cloud.google.com/load-balancing/docs/health-checks#legacy-health-checks
+ fn health_check_port(&self) -> Option<u16>;
+
/// Convenience function to create a `SocketAddr` from the provided `interface` and `port`
- fn socket_addr(&self) -> Result<SocketAddr, Error>;
+ fn udp_socket_addr(&self) -> Result<SocketAddr, Error> {
+ let addr = format!("{}:{}", self.interface(), self.port());
+ match addr.parse() {
+ Ok(v) => Ok(v),
+ Err(_) => Err(Error::InvalidConfiguration(addr)),
+ }
+ }
}
+/// Factory function to create a `ServerConfig` _trait object_ based on the value
+/// of the provided `arg`.
///
-/// Factory function to create a `ServerConfig` trait object based on the provided `arg`
+/// * `ENV` will return an [`EnvironmentConfig`](struct.EnvironmentConfig.html)
+/// * any other value returns a [`FileConfig`](struct.FileConfig.html)
///
pub fn make_config(arg: &str) -> Result<Box<ServerConfig>, Error> {
if arg == "ENV" {
@@ -105,7 +130,7 @@ pub fn make_config(arg: &str) -> Result<Box<ServerConfig>, Error> {
}
///
-/// Validate configuration settings
+/// Validate configuration settings. Returns `true` if the config is valid, `false` otherwise.
///
pub fn is_valid_config(cfg: &Box<ServerConfig>) -> bool {
let mut is_valid = true;
@@ -122,19 +147,31 @@ pub fn is_valid_config(cfg: &Box<ServerConfig>) -> bool {
error!("seed value is missing");
is_valid = false;
}
- if !cfg.seed().is_empty() && cfg.seed().len() != 32 {
- error!("seed value must be 32 characters long");
+ if *cfg.kms_protection() == KmsProtection::Plaintext && cfg.seed().len() != 32 {
+ error!("plaintext seed value must be 32 characters long");
+ is_valid = false;
+ }
+ if *cfg.kms_protection() != KmsProtection::Plaintext && cfg.seed().len() <= 32 {
+ error!("KMS use enabled but seed value is too short to be an encrypted blob");
is_valid = false;
}
if cfg.batch_size() < 1 || cfg.batch_size() > 64 {
- error!("batch_size {} is invalid; valid range 1-64", cfg.batch_size());
+ error!(
+ "batch_size {} is invalid; valid range 1-64",
+ cfg.batch_size()
+ );
is_valid = false;
}
if is_valid {
- match cfg.socket_addr() {
+ match cfg.udp_socket_addr() {
Err(e) => {
- error!("failed to create socket {}:{} {:?}", cfg.interface(), cfg.port(), e);
+ error!(
+ "failed to create UDP socket {}:{} {:?}",
+ cfg.interface(),
+ cfg.port(),
+ e
+ );
is_valid = false;
}
_ => (),
diff --git a/src/error.rs b/src/error.rs
index b681f33..e91a340 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -14,6 +14,7 @@
use std;
+use kms::KmsError;
use tag::Tag;
/// Error types generated by this implementation
@@ -58,3 +59,22 @@ impl From<std::io::Error> for Error {
Error::EncodingFailure(err)
}
}
+
+impl From<KmsError> for Error {
+ fn from(err: KmsError) -> Self {
+ match err {
+ KmsError::OperationFailed(m) => {
+ Error::InvalidConfiguration(format!("KMS operation failed: {}", m))
+ }
+ KmsError::InvalidConfiguration(m) => {
+ Error::InvalidConfiguration(format!("invalid KMS config: {}", m))
+ }
+ KmsError::InvalidData(m) => {
+ Error::InvalidConfiguration(format!("invalid KMS data: {}", m))
+ }
+ KmsError::InvalidKey(m) => {
+ Error::InvalidConfiguration(format!("invalid KMS key: {}", m))
+ }
+ }
+ }
+}
diff --git a/src/key/longterm.rs b/src/key/longterm.rs
new file mode 100644
index 0000000..ddac6ea
--- /dev/null
+++ b/src/key/longterm.rs
@@ -0,0 +1,69 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//!
+//! Represents the server's long-term identity.
+//!
+
+use std::fmt;
+use std::fmt::Formatter;
+
+use key::OnlineKey;
+use message::RtMessage;
+use sign::Signer;
+use tag::Tag;
+use CERTIFICATE_CONTEXT;
+
+///
+/// Represents the server's long-term identity.
+///
+pub struct LongTermKey {
+ signer: Signer,
+}
+
+impl LongTermKey {
+ pub fn new(seed: &[u8]) -> Self {
+ LongTermKey {
+ signer: Signer::from_seed(seed),
+ }
+ }
+
+ /// Create a CERT message with a DELE containing the provided online key
+ /// and a SIG of the DELE value signed by the long-term key
+ pub fn make_cert(&mut self, online_key: &OnlineKey) -> RtMessage {
+ let dele_bytes = online_key.make_dele().encode().unwrap();
+
+ self.signer.update(CERTIFICATE_CONTEXT.as_bytes());
+ self.signer.update(&dele_bytes);
+
+ let dele_signature = self.signer.sign();
+
+ let mut cert_msg = RtMessage::new(2);
+ cert_msg.add_field(Tag::SIG, &dele_signature).unwrap();
+ cert_msg.add_field(Tag::DELE, &dele_bytes).unwrap();
+
+ cert_msg
+ }
+
+ /// Return the public key for the provided seed
+ pub fn public_key(&self) -> &[u8] {
+ self.signer.public_key_bytes()
+ }
+}
+
+impl fmt::Display for LongTermKey {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{}", self.signer)
+ }
+}
diff --git a/src/key/mod.rs b/src/key/mod.rs
new file mode 100644
index 0000000..634d252
--- /dev/null
+++ b/src/key/mod.rs
@@ -0,0 +1,99 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//!
+//! Representations and management of Roughtime's online and long-term Ed25519 keys
+//!
+
+extern crate hex;
+extern crate log;
+extern crate ring;
+extern crate std;
+
+mod longterm;
+mod online;
+
+use std::fmt::Display;
+use std::fmt::Formatter;
+use std::str::FromStr;
+
+pub use self::longterm::LongTermKey;
+pub use self::online::OnlineKey;
+
+/// Methods for protecting the server's long-term identity
+#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone)]
+pub enum KmsProtection {
+ /// No protection, seed is in plaintext
+ Plaintext,
+
+ /// Envelope encryption of the seed using AWS Key Management Service
+ AwsKmsEnvelope(String),
+
+ /// Envelope encryption of the seed using Google Cloud Key Management Service
+ GoogleKmsEnvelope(String),
+}
+
+impl Display for KmsProtection {
+ fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
+ match self {
+ KmsProtection::Plaintext => write!(f, "Plaintext"),
+ KmsProtection::AwsKmsEnvelope(key_id) => write!(f, "AwsKms({})", key_id),
+ KmsProtection::GoogleKmsEnvelope(key_id) => write!(f, "GoogleKms({})", key_id),
+ }
+ }
+}
+
+impl FromStr for KmsProtection {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<KmsProtection, String> {
+ match s {
+ "plaintext" => Ok(KmsProtection::Plaintext),
+ s if s.starts_with("arn:") => Ok(KmsProtection::AwsKmsEnvelope(s.to_string())),
+ s if s.starts_with("projects/") => Ok(KmsProtection::GoogleKmsEnvelope(s.to_string())),
+ s => Err(format!("unknown KmsProtection '{}'", s)),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use key::KmsProtection;
+ use std::str::FromStr;
+
+ #[test]
+ fn convert_from_string() {
+ let arn =
+ "arn:aws:kms:some-aws-region:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+ let resource_id =
+ "projects/key-project/locations/global/keyRings/key-ring/cryptoKeys/my-key";
+
+ match KmsProtection::from_str("plaintext") {
+ Ok(KmsProtection::Plaintext) => (),
+ e => panic!("unexpected result {:?}", e),
+ };
+ match KmsProtection::from_str(arn) {
+ Ok(KmsProtection::AwsKmsEnvelope(msg)) => assert_eq!(msg, arn),
+ e => panic!("unexpected result {:?}", e),
+ }
+ match KmsProtection::from_str(resource_id) {
+ Ok(KmsProtection::GoogleKmsEnvelope(msg)) => assert_eq!(msg, resource_id),
+ e => panic!("unexpected result {:?}", e),
+ }
+ match KmsProtection::from_str("frobble") {
+ Err(msg) => assert!(msg.contains("unknown KmsProtection")),
+ e => panic!("unexpected result {:?}", e),
+ }
+ }
+}
diff --git a/src/keys.rs b/src/key/online.rs
index 2fadb00..18c8b8f 100644
--- a/src/keys.rs
+++ b/src/key/online.rs
@@ -12,10 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-//!
-//! Representations of Roughtime's online and long-term Ed25519 keys
-//!
-
use message::RtMessage;
use sign::Signer;
use tag::Tag;
@@ -23,10 +19,11 @@ use time::Timespec;
use byteorder::{LittleEndian, WriteBytesExt};
-use super::{CERTIFICATE_CONTEXT, SIGNED_RESPONSE_CONTEXT};
use std::fmt;
use std::fmt::Formatter;
+use SIGNED_RESPONSE_CONTEXT;
+
///
/// Represents the delegated Roughtime ephemeral online key.
///
@@ -107,41 +104,3 @@ impl fmt::Display for OnlineKey {
write!(f, "{}", self.signer)
}
}
-
-///
-/// Represents the server's long-term identity.
-///
-pub struct LongTermKey {
- signer: Signer,
-}
-
-impl LongTermKey {
- pub fn new(seed: &[u8]) -> Self {
- LongTermKey {
- signer: Signer::from_seed(seed),
- }
- }
-
- /// Create a CERT message with a DELE containing the provided online key
- /// and a SIG of the DELE value signed by the long-term key
- pub fn make_cert(&mut self, online_key: &OnlineKey) -> RtMessage {
- let dele_bytes = online_key.make_dele().encode().unwrap();
-
- self.signer.update(CERTIFICATE_CONTEXT.as_bytes());
- self.signer.update(&dele_bytes);
-
- let dele_signature = self.signer.sign();
-
- let mut cert_msg = RtMessage::new(2);
- cert_msg.add_field(Tag::SIG, &dele_signature).unwrap();
- cert_msg.add_field(Tag::DELE, &dele_bytes).unwrap();
-
- cert_msg
- }
-}
-
-impl fmt::Display for LongTermKey {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "{}", self.signer)
- }
-}
diff --git a/src/kms/awskms.rs b/src/kms/awskms.rs
new file mode 100644
index 0000000..4a244db
--- /dev/null
+++ b/src/kms/awskms.rs
@@ -0,0 +1,130 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate hex;
+extern crate log;
+
+#[cfg(feature = "awskms")]
+pub mod inner {
+ extern crate rusoto_core;
+ extern crate rusoto_kms;
+
+ use std::collections::HashMap;
+ use std::default::Default;
+ use std::error::Error;
+ use std::fmt;
+ use std::fmt::Formatter;
+ use std::str::FromStr;
+
+ use self::rusoto_core::Region;
+ use self::rusoto_kms::{DecryptRequest, EncryptRequest, Kms, KmsClient};
+ use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK, AD, DEK_SIZE_BYTES};
+
+ /// Amazon Web Services Key Management Service
+ /// https://aws.amazon.com/kms/
+ pub struct AwsKms {
+ kms_client: KmsClient,
+ key_id: String,
+ }
+
+ impl AwsKms {
+ /// Create a new instance from the full ARN of a AWS KMS key. The ARN is expected
+ /// to be of the form `arn:aws:kms:some-aws-region:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab`
+ pub fn from_arn(arn: &str) -> Result<Self, KmsError> {
+ let parts: Vec<&str> = arn.split(':').collect();
+
+ if parts.len() != 6 {
+ return Err(KmsError::InvalidConfiguration(format!(
+ "invalid KMS arn: too few parts {}",
+ parts.len()
+ )));
+ }
+
+ let region_part = parts.get(3).expect("region is missing");
+ let region = match Region::from_str(region_part) {
+ Ok(r) => r,
+ Err(e) => return Err(KmsError::InvalidConfiguration(e.description().to_string())),
+ };
+
+ Ok(AwsKms {
+ kms_client: KmsClient::new(region),
+ key_id: arn.to_string(),
+ })
+ }
+ }
+
+ impl KmsProvider for AwsKms {
+ fn encrypt_dek(&self, plaintext_dek: &PlaintextDEK) -> Result<EncryptedDEK, KmsError> {
+ if plaintext_dek.len() != DEK_SIZE_BYTES {
+ return Err(KmsError::InvalidKey(format!(
+ "provided DEK wrong length: {}",
+ plaintext_dek.len()
+ )));
+ }
+
+ let mut encrypt_req: EncryptRequest = Default::default();
+ encrypt_req.key_id = self.key_id.clone();
+ encrypt_req.plaintext = plaintext_dek.clone();
+
+ let mut enc_context = HashMap::new();
+ enc_context.insert("AD".to_string(), AD.to_string());
+ encrypt_req.encryption_context = Some(enc_context);
+
+ match self.kms_client.encrypt(encrypt_req).sync() {
+ Ok(result) => {
+ if let Some(ciphertext) = result.ciphertext_blob {
+ Ok(ciphertext)
+ } else {
+ Err(KmsError::OperationFailed(
+ "no ciphertext despite successful response".to_string(),
+ ))
+ }
+ }
+ Err(e) => Err(KmsError::OperationFailed(e.description().to_string())),
+ }
+ }
+
+ fn decrypt_dek(&self, encrypted_dek: &EncryptedDEK) -> Result<PlaintextDEK, KmsError> {
+ let mut decrypt_req: DecryptRequest = Default::default();
+ decrypt_req.ciphertext_blob = encrypted_dek.clone();
+
+ match self.kms_client.decrypt(decrypt_req).sync() {
+ Ok(result) => {
+ if let Some(plaintext_dek) = result.plaintext {
+ if plaintext_dek.len() == DEK_SIZE_BYTES {
+ Ok(plaintext_dek)
+ } else {
+ Err(KmsError::InvalidKey(format!(
+ "decrypted DEK wrong length: {}",
+ plaintext_dek.len()
+ )))
+ }
+ } else {
+ Err(KmsError::OperationFailed(
+ "decrypted payload is empty".to_string(),
+ ))
+ }
+ }
+ Err(e) => Err(KmsError::OperationFailed(e.description().to_string())),
+ }
+ }
+ }
+
+ #[cfg(feature = "awskms")]
+ impl fmt::Display for AwsKms {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{}", self.key_id)
+ }
+ }
+}
diff --git a/src/kms/envelope.rs b/src/kms/envelope.rs
new file mode 100644
index 0000000..49f8d79
--- /dev/null
+++ b/src/kms/envelope.rs
@@ -0,0 +1,289 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate hex;
+
+use std::io::{Cursor, Read, Write};
+
+use ring::aead::{open_in_place, seal_in_place, OpeningKey, SealingKey, AES_256_GCM};
+use ring::rand::{SecureRandom, SystemRandom};
+
+use super::super::MIN_SEED_LENGTH;
+use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
+use kms::{KmsError, KmsProvider, AD, DEK_SIZE_BYTES, NONCE_SIZE_BYTES, TAG_SIZE_BYTES};
+
+const DEK_LEN_FIELD: usize = 2;
+const NONCE_LEN_FIELD: usize = 2;
+
+// 2 bytes - encrypted DEK length
+// 2 bytes - nonce length
+// n bytes - encrypted DEK
+// n bytes - nonce
+// n bytes - opaque (AEAD encrypted seed + tag)
+const MIN_PAYLOAD_SIZE: usize = DEK_LEN_FIELD
+ + NONCE_LEN_FIELD
+ + DEK_SIZE_BYTES
+ + NONCE_SIZE_BYTES
+ + MIN_SEED_LENGTH as usize
+ + TAG_SIZE_BYTES;
+
+// No input prefix to skip, consume entire buffer
+const IN_PREFIX_LEN: usize = 0;
+
+// Convenience function to create zero-filled Vec of given size
+fn vec_zero_filled(len: usize) -> Vec<u8> {
+ (0..len).into_iter().map(|_| 0).collect()
+}
+
+/// Envelope encryption of the long-term key seed value.
+///
+/// The seed is encrypted using AES-GCM-256 with:
+///
+/// * 32 byte (256 bit) random key
+/// * 12 byte (96 bit) random nonce
+/// * 16 byte (128 bit) authentication tag
+///
+/// Randomness obtained from
+/// [`ring::rand::SecureRandom`](https://briansmith.org/rustdoc/ring/rand/trait.SecureRandom.html).
+///
+/// The key used to encrypt the seed is wrapped (encrypted) using a
+/// [`KmsProvider`](trait.KmsProvider.html) implementation.
+///
+pub struct EnvelopeEncryption;
+
+impl EnvelopeEncryption {
+ /// Decrypt a seed previously encrypted with `encrypt_seed()`
+ pub fn decrypt_seed(kms: &KmsProvider, ciphertext_blob: &[u8]) -> Result<Vec<u8>, KmsError> {
+ if ciphertext_blob.len() < MIN_PAYLOAD_SIZE {
+ return Err(KmsError::InvalidData(format!(
+ "ciphertext too short: min {}, found {}",
+ MIN_PAYLOAD_SIZE,
+ ciphertext_blob.len()
+ )));
+ }
+
+ let mut tmp = Cursor::new(ciphertext_blob);
+
+ // Read the lengths of the wrapped DEK and of the nonce
+ let dek_len = tmp.read_u16::<LittleEndian>()? as usize;
+ let nonce_len = tmp.read_u16::<LittleEndian>()? as usize;
+
+ if nonce_len != NONCE_SIZE_BYTES || dek_len > ciphertext_blob.len() {
+ return Err(KmsError::InvalidData(format!(
+ "invalid DEK ({}) or nonce ({}) length",
+ dek_len, nonce_len
+ )));
+ }
+
+ // Consume the wrapped DEK
+ let mut encrypted_dek = vec_zero_filled(dek_len);
+ tmp.read_exact(&mut encrypted_dek)?;
+
+ // Consume the nonce
+ let mut nonce = vec_zero_filled(nonce_len);
+ tmp.read_exact(&mut nonce)?;
+
+ // Consume the encrypted seed + tag
+ let mut encrypted_seed = Vec::new();
+ tmp.read_to_end(&mut encrypted_seed)?;
+
+ // Invoke KMS to decrypt the DEK
+ let dek = kms.decrypt_dek(&encrypted_dek)?;
+
+ // Decrypt the seed value using the DEK
+ let dek_open_key = OpeningKey::new(&AES_256_GCM, &dek)?;
+ match open_in_place(
+ &dek_open_key,
+ &nonce,
+ AD.as_bytes(),
+ IN_PREFIX_LEN,
+ &mut encrypted_seed,
+ ) {
+ Ok(plaintext_seed) => Ok(plaintext_seed.to_vec()),
+ Err(_) => Err(KmsError::OperationFailed(
+ "failed to decrypt plaintext seed".to_string(),
+ )),
+ }
+ }
+
+ ///
+ /// Encrypt the seed value and protect the seed's encryption key using a
+ /// [`KmsProvider`](trait.KmsProvider.html).
+ ///
+ /// The returned encrypted byte blob is safe to store on unsecured media.
+ ///
+ pub fn encrypt_seed(kms: &KmsProvider, plaintext_seed: &[u8]) -> Result<Vec<u8>, KmsError> {
+ // Generate random DEK and nonce
+ let rng = SystemRandom::new();
+ let mut dek = [0u8; DEK_SIZE_BYTES];
+ let mut nonce = [0u8; NONCE_SIZE_BYTES];
+ rng.fill(&mut dek)?;
+ rng.fill(&mut nonce)?;
+
+ // Ring will overwrite plaintext with ciphertext in this buffer
+ let mut plaintext_buf = plaintext_seed.to_vec();
+
+ // Reserve space for the authentication tag which will be appended after the ciphertext
+ plaintext_buf.reserve(TAG_SIZE_BYTES);
+ for _ in 0..TAG_SIZE_BYTES {
+ plaintext_buf.push(0);
+ }
+
+ // Encrypt the plaintext seed using the DEK
+ let dek_seal_key = SealingKey::new(&AES_256_GCM, &dek)?;
+ let encrypted_seed = match seal_in_place(
+ &dek_seal_key,
+ &nonce,
+ AD.as_bytes(),
+ &mut plaintext_buf,
+ TAG_SIZE_BYTES,
+ ) {
+ Ok(enc_len) => plaintext_buf[..enc_len].to_vec(),
+ Err(_) => {
+ return Err(KmsError::OperationFailed(
+ "failed to encrypt plaintext seed".to_string(),
+ ))
+ }
+ };
+
+ // Use the KMS to wrap the DEK
+ let wrapped_dek = kms.encrypt_dek(&dek.to_vec())?;
+
+ // And coalesce everything together
+ let mut output = Vec::new();
+ output.write_u16::<LittleEndian>(wrapped_dek.len() as u16)?;
+ output.write_u16::<LittleEndian>(nonce.len() as u16)?;
+ output.write_all(&wrapped_dek)?;
+ output.write_all(&nonce)?;
+ output.write_all(&encrypted_seed)?;
+
+ Ok(output)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use kms::envelope::{DEK_LEN_FIELD, MIN_PAYLOAD_SIZE, NONCE_LEN_FIELD};
+ use kms::EnvelopeEncryption;
+ use kms::{KmsError, KmsProvider};
+
+ struct MockKmsProvider {}
+
+ // Mock provider that returns a copy of the input
+ impl KmsProvider for MockKmsProvider {
+ fn encrypt_dek(&self, plaintext_dek: &Vec<u8>) -> Result<Vec<u8>, KmsError> {
+ Ok(plaintext_dek.to_vec())
+ }
+
+ fn decrypt_dek(&self, encrypted_dek: &Vec<u8>) -> Result<Vec<u8>, KmsError> {
+ Ok(encrypted_dek.to_vec())
+ }
+ }
+
+ #[test]
+ fn decryption_reject_input_too_short() {
+ let ciphertext_blob = "1234567890";
+ assert!(ciphertext_blob.len() < MIN_PAYLOAD_SIZE);
+
+ let kms = MockKmsProvider {};
+ let result = EnvelopeEncryption::decrypt_seed(&kms, ciphertext_blob.as_bytes());
+
+ match result.expect_err("expected KmsError") {
+ KmsError::InvalidData(msg) => assert!(msg.contains("ciphertext too short")),
+ e => panic!("Unexpected error {:?}", e),
+ }
+ }
+
+ #[test]
+ fn encrypt_decrypt_round_trip() {
+ let kms = MockKmsProvider {};
+ let plaintext = Vec::from("This is the plaintext used for this test 1");
+
+ let enc_result = EnvelopeEncryption::encrypt_seed(&kms, &plaintext);
+ assert_eq!(enc_result.is_ok(), true);
+
+ let ciphertext = enc_result.unwrap();
+ assert_ne!(plaintext, ciphertext);
+
+ let dec_result = EnvelopeEncryption::decrypt_seed(&kms, &ciphertext);
+ assert_eq!(dec_result.is_ok(), true);
+
+ let new_plaintext = dec_result.unwrap();
+ assert_eq!(plaintext, new_plaintext);
+ }
+
+ #[test]
+ fn invalid_dek_length_detected() {
+ let kms = MockKmsProvider {};
+ let plaintext = Vec::from("This is the plaintext used for this test 2");
+
+ let enc_result = EnvelopeEncryption::encrypt_seed(&kms, &plaintext);
+ assert_eq!(enc_result.is_ok(), true);
+
+ let ciphertext = enc_result.unwrap();
+ let mut ciphertext_copy = ciphertext.clone();
+
+ ciphertext_copy[1] = 99;
+ let dec_result = EnvelopeEncryption::decrypt_seed(&kms, &ciphertext_copy);
+ match dec_result.expect_err("expected an error") {
+ KmsError::InvalidData(msg) => assert!(msg.contains("invalid DEK")),
+ e => panic!("unexpected error {:?}", e),
+ }
+ }
+
+ #[test]
+ fn invalid_nonce_length_detected() {
+ let kms = MockKmsProvider {};
+ let plaintext = Vec::from("This is the plaintext used for this test 3");
+
+ let enc_result = EnvelopeEncryption::encrypt_seed(&kms, &plaintext);
+ assert_eq!(enc_result.is_ok(), true);
+
+ let ciphertext = enc_result.unwrap();
+ let mut ciphertext_copy = ciphertext.clone();
+
+ ciphertext_copy[2] = 1;
+ let dec_result = EnvelopeEncryption::decrypt_seed(&kms, &ciphertext_copy);
+ match dec_result.expect_err("expected an error") {
+ KmsError::InvalidData(msg) => assert!(msg.contains("nonce (1)")),
+ e => panic!("unexpected error {:?}", e),
+ }
+ }
+
+ #[test]
+ fn modified_ciphertext_is_detected() {
+ let kms = MockKmsProvider {};
+ let plaintext = Vec::from("This is the plaintext used for this test 4");
+
+ let enc_result = EnvelopeEncryption::encrypt_seed(&kms, &plaintext);
+ assert_eq!(enc_result.is_ok(), true);
+
+ let ciphertext = enc_result.unwrap();
+ assert_ne!(plaintext, ciphertext);
+
+ // start corruption 4 bytes in, after the DEK and NONCE length fields
+ for i in (DEK_LEN_FIELD + NONCE_LEN_FIELD)..ciphertext.len() {
+ let mut ciphertext_copy = ciphertext.clone();
+ // flip some bits
+ ciphertext_copy[i] = ciphertext[i].wrapping_add(1);
+
+ let dec_result = EnvelopeEncryption::decrypt_seed(&kms, &ciphertext_copy);
+
+ match dec_result.expect_err("Expected a KmsError error here") {
+ KmsError::OperationFailed(msg) => assert!(msg.contains("failed to decrypt")),
+ e => panic!("unexpected result {:?}", e),
+ }
+ }
+ }
+}
diff --git a/src/kms/gcpkms.rs b/src/kms/gcpkms.rs
new file mode 100644
index 0000000..1401925
--- /dev/null
+++ b/src/kms/gcpkms.rs
@@ -0,0 +1,169 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate hex;
+extern crate log;
+
+#[cfg(feature = "gcpkms")]
+pub mod inner {
+ extern crate base64;
+ extern crate google_cloudkms1 as cloudkms1;
+ extern crate hyper;
+ extern crate hyper_rustls;
+ extern crate yup_oauth2 as oauth2;
+
+ use std::default::Default;
+ use std::env;
+ use std::path::Path;
+ use std::result::Result;
+
+ use self::cloudkms1::CloudKMS;
+ use self::cloudkms1::{DecryptRequest, EncryptRequest};
+ use self::hyper::net::HttpsConnector;
+ use self::hyper::status::StatusCode;
+ use self::hyper_rustls::TlsClient;
+ use self::oauth2::{ServiceAccountAccess, ServiceAccountKey};
+
+ use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK, AD};
+
+ const GOOGLE_APP_CREDS: &str = &"GOOGLE_APPLICATION_CREDENTIALS";
+
+ /// Google Cloud Key Management Service
+ /// https://cloud.google.com/kms/
+ pub struct GcpKms {
+ key_resource_id: String,
+ service_account: ServiceAccountKey,
+ }
+
+ impl GcpKms {
+ ///
+ /// Create a new GcpKms from a Google Cloud KMS key resource ID of the form
+ /// `projects/*/locations/*/keyRings/*/cryptoKeys/*`
+ ///
+ pub fn from_resource_id(resource_id: &str) -> Result<Self, KmsError> {
+ let svc_acct = load_gcp_credential()?;
+
+ Ok(GcpKms {
+ key_resource_id: resource_id.to_string(),
+ service_account: svc_acct,
+ })
+ }
+
+ fn new_hub(&self) -> CloudKMS<hyper::Client, ServiceAccountAccess<hyper::Client>> {
+ let client1 = hyper::Client::with_connector(HttpsConnector::new(TlsClient::new()));
+ let access = oauth2::ServiceAccountAccess::new(self.service_account.clone(), client1);
+
+ let client2 = hyper::Client::with_connector(HttpsConnector::new(TlsClient::new()));
+ CloudKMS::new(client2, access)
+ }
+
+ fn pretty_http_error(&self, resp: &hyper::client::Response) -> KmsError {
+ let code = resp.status;
+ let url = &resp.url;
+
+ KmsError::OperationFailed(format!("Response {} from {}", code, url))
+ }
+ }
+
+ impl KmsProvider for GcpKms {
+ fn encrypt_dek(&self, plaintext_dek: &PlaintextDEK) -> Result<EncryptedDEK, KmsError> {
+ let mut request = EncryptRequest::default();
+ request.plaintext = Some(base64::encode(plaintext_dek));
+ request.additional_authenticated_data = Some(base64::encode(AD));
+
+ let hub = self.new_hub();
+ let result = hub
+ .projects()
+ .locations_key_rings_crypto_keys_encrypt(request, &self.key_resource_id)
+ .doit();
+
+ match result {
+ Ok((http_resp, enc_resp)) => {
+ if http_resp.status == StatusCode::Ok {
+ let ciphertext = enc_resp.ciphertext.unwrap();
+ let ct = base64::decode(&ciphertext)?;
+ Ok(ct)
+ } else {
+ Err(self.pretty_http_error(&http_resp))
+ }
+ }
+ Err(e) => Err(KmsError::OperationFailed(format!("encrypt_dek() {:?}", e))),
+ }
+ }
+
+ fn decrypt_dek(&self, encrypted_dek: &EncryptedDEK) -> Result<PlaintextDEK, KmsError> {
+ let mut request = DecryptRequest::default();
+ request.ciphertext = Some(base64::encode(encrypted_dek));
+ request.additional_authenticated_data = Some(base64::encode(AD));
+
+ let hub = self.new_hub();
+ let result = hub
+ .projects()
+ .locations_key_rings_crypto_keys_decrypt(request, &self.key_resource_id)
+ .doit();
+
+ match result {
+ Ok((http_resp, enc_resp)) => {
+ if http_resp.status == StatusCode::Ok {
+ let plaintext = enc_resp.plaintext.unwrap();
+ let ct = base64::decode(&plaintext)?;
+ Ok(ct)
+ } else {
+ Err(self.pretty_http_error(&http_resp))
+ }
+ }
+ Err(e) => Err(KmsError::OperationFailed(format!("decrypt_dek() {:?}", e))),
+ }
+ }
+ }
+
+ /// Minimal implementation of Application Default Credentials.
+ /// https://cloud.google.com/docs/authentication/production
+ ///
+ /// 1. Look for GOOGLE_APPLICATION_CREDENTIALS and load service account
+ /// credentials if found.
+ /// 2. If not, error
+ ///
+ /// TODO attempt to load GCE default credentials from metadata server.
+ /// This will be a bearer token instead of service account credential.
+
+ fn load_gcp_credential() -> Result<ServiceAccountKey, KmsError> {
+ if let Ok(gac) = env::var(GOOGLE_APP_CREDS.to_string()) {
+ if Path::new(&gac).exists() {
+ match oauth2::service_account_key_from_file(&gac) {
+ Ok(svc_acct_key) => return Ok(svc_acct_key),
+ Err(e) => {
+ return Err(KmsError::InvalidConfiguration(format!(
+ "Can't load service account credential '{}': {:?}",
+ gac, e
+ )))
+ }
+ }
+ } else {
+ return Err(KmsError::InvalidConfiguration(format!(
+ "{} ='{}' does not exist",
+ GOOGLE_APP_CREDS, gac
+ )));
+ }
+ }
+
+ // TODO: call to GCE metadata service to get default credential from
+ // http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
+
+ panic!(
+ "Failed to load service account credential. Is {} set?",
+ GOOGLE_APP_CREDS
+ );
+ }
+}
diff --git a/src/kms/mod.rs b/src/kms/mod.rs
new file mode 100644
index 0000000..464d06a
--- /dev/null
+++ b/src/kms/mod.rs
@@ -0,0 +1,226 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//!
+//! Protect the server's long-term key with envelope encryption and a key management system.
+//!
+//! Note: KMS support must be enabled at compile time, see the Roughenough's [documentation
+//! on optional features](https://github.com/int08h/roughenough/blob/doc/OPTIONAL-FEATURES.md#key-management-system-kms-support)
+//! for instructions.
+//!
+//! ## Motivation
+//!
+//! The seed for the server's [long-term key](../key/struct.LongTermKey.html) is subject to
+//! contradictory requirements:
+//!
+//! 1. The seed must be kept secret, but
+//! 2. The seed must be available at server start-up to create the
+//! [delegated on-line key](../key/struct.OnlineKey.html)
+//!
+//! ## Plaintext seed
+//!
+//! The default option is to store the seed in plaintext as part of the server's configuration.
+//! This usually means the seed is present in the clear: on disk, in a repository, or otherwise
+//! durably persisted where it can be compromised (accidentally or maliciously).
+//!
+//! ## Encrypting the seed
+//!
+//! Envelope encryption protects the seed by encrypting it with a locally generated 256-bit
+//! Data Encryption Key (DEK). The DEK itself is then encrypted using a cloud key management
+//! system (KMS). The resulting opaque encrypted "blob" (encrypted seed + encrypted DEK) is
+//! stored in the Roughenough configuration.
+//!
+//! At server start-up the KMS is used to decrypt the DEK, which is then used to (in memory)
+//! decrypt the seed. The seed is used to generate the
+//! [delegated on-line key](../key/struct.OnlineKey.html) after which the seed and DEK are erased
+//! from memory.
+//!
+//! See
+//! * [`EnvelopeEncryption`](struct.EnvelopeEncryption.html) for Roughenough's implementation.
+//! * [Google](https://cloud.google.com/kms/docs/envelope-encryption) or
+//! [Amazon](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
+//! for more in-depth explanations of envelope encryption.
+//!
+
+mod envelope;
+
+use base64;
+use ring;
+use std;
+
+use config::ServerConfig;
+use error;
+use key::KmsProtection;
+
+pub use self::envelope::EnvelopeEncryption;
+
+/// Errors generated by KMS operations
+#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone)]
+pub enum KmsError {
+ OperationFailed(String),
+ InvalidConfiguration(String),
+ InvalidData(String),
+ InvalidKey(String),
+}
+
+impl From<std::io::Error> for KmsError {
+ fn from(error: std::io::Error) -> Self {
+ KmsError::OperationFailed(format!("{:?}", error))
+ }
+}
+
+impl From<ring::error::Unspecified> for KmsError {
+ fn from(_: ring::error::Unspecified) -> Self {
+ KmsError::OperationFailed("unspecified ring cryptographic failure".to_string())
+ }
+}
+
+impl From<base64::DecodeError> for KmsError {
+ fn from(error: base64::DecodeError) -> Self {
+ KmsError::OperationFailed(format!("base64: {}", error))
+ }
+}
+
+// Size of the AEAD nonce in bytes.
+const NONCE_SIZE_BYTES: usize = 12;
+
+// Size of the AEAD authentication tag in bytes.
+const TAG_SIZE_BYTES: usize = 16;
+
+// Size of the 256-bit Data Encryption Key (DEK) in bytes.
+const DEK_SIZE_BYTES: usize = 32;
+
+// Trivial domain separation to guard against KMS key reuse
+const AD: &str = "roughenough";
+
+/// An unencrypted (plaintext) 256-bit Data Encryption Key (DEK).
+pub type PlaintextDEK = Vec<u8>;
+
+/// A Data Encryption Key (DEK) that has been encrypted (wrapped) by a Key Management System (KMS).
+///
+/// This is an opaque, implementation-specific value. AEAD tag size, nonce size,
+/// provider metadata, and so on will vary between [`KmsProvider`](trait.KmsProvider.html)
+/// implementations.
+pub type EncryptedDEK = Vec<u8>;
+
+///
+/// A key management system that wraps/unwraps a data encryption key (DEK).
+///
+pub trait KmsProvider {
+ /// Make a blocking request to encrypt (wrap) the provided plaintext data encryption key.
+ fn encrypt_dek(&self, plaintext_dek: &PlaintextDEK) -> Result<EncryptedDEK, KmsError>;
+
+ /// Make a blocking request to decrypt (unwrap) a previously encrypted data encryption key.
+ fn decrypt_dek(&self, encrypted_dek: &EncryptedDEK) -> Result<PlaintextDEK, KmsError>;
+}
+
+#[cfg(feature = "awskms")]
+mod awskms;
+
+#[cfg(feature = "awskms")]
+pub use kms::awskms::inner::AwsKms;
+
+/// Load the seed value for the long-term key.
+///
+/// Loading behavior depends on the value of `config.kms_protection()`:
+///
+/// * If `config.kms_protection() == Plaintext` then the value returned from `config.seed()`
+/// is used as-is and assumed to be a 32-byte hexadecimal value.
+///
+/// * Otherwise `config.seed()` is assumed to be an encrypted opaque blob generated from
+/// a prior `EnvelopeEncryption::encrypt_seed` call. The value of `config.kms_protection()`
+/// is parsed as a KMS key id and `EnvelopeEncryption::decrypt_seed` is called to obtain
+/// the plaintext seed value.
+///
+#[cfg(feature = "awskms")]
+pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> {
+ use kms::envelope::EnvelopeEncryption;
+
+ match config.kms_protection() {
+ KmsProtection::Plaintext => Ok(config.seed()),
+ KmsProtection::AwsKmsEnvelope(key_id) => {
+ info!("Unwrapping seed via AWS KMS key '{}'", key_id);
+ let kms = AwsKms::from_arn(key_id)?;
+ let seed = EnvelopeEncryption::decrypt_seed(&kms, &config.seed())?;
+ Ok(seed)
+ }
+ _ => Err(error::Error::InvalidConfiguration(
+ "Google KMS not supported".to_string(),
+ )),
+ }
+}
+
+#[cfg(feature = "gcpkms")]
+mod gcpkms;
+
+#[cfg(feature = "gcpkms")]
+pub use kms::gcpkms::inner::GcpKms;
+
+/// Load the seed value for the long-term key.
+///
+/// Loading behavior depends on the value of `config.kms_protection()`:
+///
+/// * If `config.kms_protection() == Plaintext` then the value returned from `config.seed()`
+/// is used as-is and assumed to be a 32-byte hexadecimal value.
+///
+/// * Otherwise `config.seed()` is assumed to be an encrypted opaque blob generated from
+/// a prior `EnvelopeEncryption::encrypt_seed` call. The value of `config.kms_protection()`
+/// is parsed as a KMS key id and `EnvelopeEncryption::decrypt_seed` is called to obtain
+/// the plaintext seed value.
+///
+#[cfg(feature = "gcpkms")]
+pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> {
+ use kms::envelope::EnvelopeEncryption;
+
+ match config.kms_protection() {
+ KmsProtection::Plaintext => Ok(config.seed()),
+ KmsProtection::GoogleKmsEnvelope(resource_id) => {
+ info!("Unwrapping seed via Google KMS key '{}'", resource_id);
+ let kms = GcpKms::from_resource_id(resource_id)?;
+ let seed = EnvelopeEncryption::decrypt_seed(&kms, &config.seed())?;
+ Ok(seed)
+ }
+ _ => Err(error::Error::InvalidConfiguration(
+ "AWS KMS not supported".to_string(),
+ )),
+ }
+}
+
+/// Load the seed value for the long-term key.
+///
+/// Loading behavior depends on the value of `config.kms_protection()`:
+///
+/// * If `config.kms_protection() == Plaintext` then the value returned from `config.seed()`
+/// is used as-is and assumed to be a 32-byte hexadecimal value.
+///
+/// * Otherwise `config.seed()` is assumed to be an encrypted opaque blob generated from
+/// a prior `EnvelopeEncryption::encrypt_seed` call. The value of `config.kms_protection()`
+/// is parsed as a KMS key id and `EnvelopeEncryption::decrypt_seed` is called to obtain
+/// the plaintext seed value.
+///
+/// ## KMS Disabled
+///
+/// The KMS feature is *disabled* in this build of Roughenough. The only
+/// supported `kms_protection` value is `plaintext`. Any other value is an error.
+///
+#[cfg(not(any(feature = "awskms", feature = "gcpkms")))]
+pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> {
+ match config.kms_protection() {
+ KmsProtection::Plaintext => Ok(config.seed()),
+ v => Err(error::Error::InvalidConfiguration(format!(
+ "kms_protection '{}' requires KMS, but server was not compiled with KMS support",
+ v
+ ))),
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index d513e4c..b87f800 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -25,6 +25,16 @@
//! Roughtime messages are represented by [`RtMessage`](struct.RtMessage.html) which
//! implements the mapping of Roughtime `u32` [`tags`](enum.Tag.html) to byte-strings.
//!
+//! # Keys and Signing
+//!
+//! Roughtime uses an [Ed25519](https://ed25519.cr.yp.to/) key pair as the server's
+//! long-term identity and a second key pair (signed by the long-term key) as a
+//! delegated on-line (ephemeral) key.
+//!
+//! [`LongTermKey`](key/struct.LongTermKey.html) and [`OnlineKey`](key/struct.OnlineKey.html)
+//! implement these elements of the protocol. The [`sign`](sign/index.html) module provides
+//! signing and verification operations.
+//!
//! # Client
//!
//! A Roughtime client can be found in `src/bin/client.rs`. To run the client:
@@ -37,32 +47,36 @@
//!
//! # Server
//!
-//! The Roughtime server implementation is in `src/bin/server.rs`. The server is
-//! configured via a YAML config file. See [FileConfig](config/struct.FileConfig.html)
-//! for details of the configuration parameters.
+//! The core Roughtime server implementation is in `src/server.rs` and the server's CLI can
+//! be found in `src/bin/roughenough-server.rs`.
//!
-//! To run the server:
+//! The server has multiple ways it can be configured,
+//! see [`ServerConfig`](config/trait.ServerConfig.html) for the configuration trait and
//!
-//! ```bash
-//! $ cargo run --release --bin server /path/to/config.file
-//! ```
//!
+extern crate base64;
extern crate byteorder;
extern crate core;
+extern crate hex;
+extern crate mio;
+extern crate mio_extras;
extern crate time;
extern crate yaml_rust;
#[macro_use]
extern crate log;
+extern crate ring;
mod error;
mod message;
mod tag;
pub mod config;
-pub mod keys;
+pub mod key;
+pub mod kms;
pub mod merkle;
+pub mod server;
pub mod sign;
pub use error::Error;
@@ -70,7 +84,20 @@ pub use message::RtMessage;
pub use tag::Tag;
/// Version of Roughenough
-pub const VERSION: &str = "1.0.6";
+pub const VERSION: &str = "1.1.0";
+
+/// Roughenough version string enriched with any compile-time optional features
+pub fn roughenough_version() -> String {
+ let kms_str = if cfg!(feature = "awskms") {
+ " (+AWS KMS)"
+ } else if cfg!(feature = "gcpkms") {
+ " (+GCP KMS)"
+ } else {
+ ""
+ };
+
+ format!("{}{}", VERSION, kms_str)
+}
// Constants and magic numbers of the Roughtime protocol
diff --git a/src/merkle.rs b/src/merkle.rs
index e34a5b4..69e6c00 100644
--- a/src/merkle.rs
+++ b/src/merkle.rs
@@ -62,7 +62,7 @@ impl MerkleTree {
pub fn compute_root(&mut self) -> Hash {
assert!(
- self.levels[0].len() > 0,
+ !self.levels[0].is_empty(),
"Must have at least one leaf to hash!"
);
diff --git a/src/message.rs b/src/message.rs
index bd11bf7..429a5e5 100644
--- a/src/message.rs
+++ b/src/message.rs
@@ -192,7 +192,8 @@ impl RtMessage {
return Some(&self.values[i]);
}
}
- return None;
+
+ None
}
/// Returns the number of tag/value pairs in the message
diff --git a/src/server.rs b/src/server.rs
new file mode 100644
index 0000000..755fd4f
--- /dev/null
+++ b/src/server.rs
@@ -0,0 +1,389 @@
+// Copyright 2017-2018 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//!
+//! Implements the Roughenough server functionality.
+//!
+
+use hex;
+use std::io::ErrorKind;
+use std::net::SocketAddr;
+use std::process;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+use std::time::Duration;
+use time;
+
+use byteorder::{LittleEndian, WriteBytesExt};
+
+use mio::net::{TcpListener, UdpSocket};
+use mio::{Events, Poll, PollOpt, Ready, Token};
+use mio_extras::timer::Timer;
+
+use config::ServerConfig;
+use key::{LongTermKey, OnlineKey};
+use kms;
+use merkle::MerkleTree;
+use mio::tcp::Shutdown;
+use std::io::Write;
+use {Error, RtMessage, Tag, MIN_REQUEST_LENGTH};
+
+macro_rules! check_ctrlc {
+ ($keep_running:expr) => {
+ if !$keep_running.load(Ordering::Acquire) {
+ warn!("Ctrl-C caught, exiting...");
+ return true;
+ }
+ };
+}
+
+// mio event registrations
+const MESSAGE: Token = Token(0);
+const STATUS: Token = Token(1);
+const HEALTH_CHECK: Token = Token(2);
+
+// Canned response to health check request
+const HTTP_RESPONSE: &str = "HTTP/1.1 200 OK\nContent-Length: 0\nConnection: close\n\n";
+
+/// The main Roughenough server instance.
+///
+/// The [ServerConfig](../config/trait.ServerConfig.html) trait specifies the required and optional
+/// parameters available for configuring a Roughenoguh server instance.
+///
+/// Implementations of `ServerConfig` obtain configurations from different back-end sources
+/// such as files or environment variables.
+///
+/// See [the config module](../config/index.html) for more information.
+///
+pub struct Server {
+ config: Box<ServerConfig>,
+ online_key: OnlineKey,
+ cert_bytes: Vec<u8>,
+
+ response_counter: u64,
+ num_bad_requests: u64,
+
+ socket: UdpSocket,
+ health_listener: Option<TcpListener>,
+ keep_running: Arc<AtomicBool>,
+ poll_duration: Option<Duration>,
+ timer: Timer<()>,
+ poll: Poll,
+ events: Events,
+ merkle: MerkleTree,
+ requests: Vec<(Vec<u8>, SocketAddr)>,
+ buf: [u8; 65_536],
+
+ public_key: String,
+
+ // Used to send requests to ourselves in fuzzing mode
+ #[cfg(fuzzing)]
+ fake_client_socket: UdpSocket,
+}
+
+impl Server {
+ ///
+ /// Create a new server instance from the provided
+ /// [`ServerConfig`](../config/trait.ServerConfig.html) trait object instance.
+ ///
+ pub fn new(config: Box<ServerConfig>) -> Server {
+ let online_key = OnlineKey::new();
+ let public_key: String;
+
+ let cert_bytes = {
+ let seed = match kms::load_seed(&config) {
+ Ok(seed) => seed,
+ Err(e) => {
+ error!("Failed to load seed: {:#?}", e);
+ process::exit(1);
+ }
+ };
+ let mut long_term_key = LongTermKey::new(&seed);
+ public_key = hex::encode(long_term_key.public_key());
+
+ long_term_key.make_cert(&online_key).encode().unwrap()
+ };
+
+ let keep_running = Arc::new(AtomicBool::new(true));
+
+ let sock_addr = config.udp_socket_addr().expect("udp sock addr");
+ let socket = UdpSocket::bind(&sock_addr).expect("failed to bind to socket");
+
+ let poll_duration = Some(Duration::from_millis(100));
+
+ let mut timer: Timer<()> = Timer::default();
+ timer.set_timeout(config.status_interval(), ());
+
+ let poll = Poll::new().unwrap();
+ poll.register(&socket, MESSAGE, Ready::readable(), PollOpt::edge())
+ .unwrap();
+ poll.register(&timer, STATUS, Ready::readable(), PollOpt::edge())
+ .unwrap();
+
+ let health_listener = if let Some(hc_port) = config.health_check_port() {
+ let hc_sock_addr: SocketAddr = format!("{}:{}", config.interface(), hc_port)
+ .parse()
+ .unwrap();
+
+ let tcp_listener = TcpListener::bind(&hc_sock_addr)
+ .expect("failed to bind TCP listener for health check");
+
+ poll.register(&tcp_listener, HEALTH_CHECK, Ready::readable(), PollOpt::edge())
+ .unwrap();
+
+ Some(tcp_listener)
+ } else {
+ None
+ };
+
+ let merkle = MerkleTree::new();
+ let requests = Vec::with_capacity(config.batch_size() as usize);
+
+ Server {
+ config,
+ online_key,
+ cert_bytes,
+
+ response_counter: 0,
+ num_bad_requests: 0,
+ socket,
+ health_listener,
+
+ keep_running,
+ poll_duration,
+ timer,
+ poll,
+ events: Events::with_capacity(32),
+ merkle,
+ requests,
+ buf: [0u8; 65_536],
+
+ public_key,
+
+ #[cfg(fuzzing)]
+ fake_client_socket: UdpSocket::bind(&"127.0.0.1:0".parse().unwrap()).unwrap(),
+ }
+ }
+
+ /// Returns a reference counted pointer the this server's `keep_running` value.
+ pub fn get_keep_running(&self) -> Arc<AtomicBool> {
+ self.keep_running.clone()
+ }
+
+ // extract the client's nonce from its request
+ fn nonce_from_request<'a>(&self, buf: &'a [u8], num_bytes: usize) -> Result<&'a [u8], Error> {
+ if num_bytes < MIN_REQUEST_LENGTH as usize {
+ return Err(Error::RequestTooShort);
+ }
+
+ let tag_count = &buf[..4];
+ let expected_nonc = &buf[8..12];
+ let expected_pad = &buf[12..16];
+
+ let tag_count_is_2 = tag_count == [0x02, 0x00, 0x00, 0x00];
+ let tag1_is_nonc = expected_nonc == Tag::NONC.wire_value();
+ let tag2_is_pad = expected_pad == Tag::PAD.wire_value();
+
+ if tag_count_is_2 && tag1_is_nonc && tag2_is_pad {
+ Ok(&buf[0x10..0x50])
+ } else {
+ Err(Error::InvalidRequest)
+ }
+ }
+
+ fn make_response(
+ &self,
+ srep: &RtMessage,
+ cert_bytes: &[u8],
+ path: &[u8],
+ idx: u32,
+ ) -> RtMessage {
+ let mut index = [0; 4];
+ (&mut index as &mut [u8])
+ .write_u32::<LittleEndian>(idx)
+ .unwrap();
+
+ let sig_bytes = srep.get_field(Tag::SIG).unwrap();
+ let srep_bytes = srep.get_field(Tag::SREP).unwrap();
+
+ let mut response = RtMessage::new(5);
+ response.add_field(Tag::SIG, sig_bytes).unwrap();
+ response.add_field(Tag::PATH, path).unwrap();
+ response.add_field(Tag::SREP, srep_bytes).unwrap();
+ response.add_field(Tag::CERT, cert_bytes).unwrap();
+ response.add_field(Tag::INDX, &index).unwrap();
+
+ response
+ }
+
+ /// The main processing function for incoming connections. This method should be
+ /// called repeatedly in a loop to process requests. It returns 'true' when the
+ /// server has shutdown (due to keep_running being set to 'false').
+ ///
+ pub fn process_events(&mut self) -> bool {
+ self.poll
+ .poll(&mut self.events, self.poll_duration)
+ .expect("poll failed");
+
+ for event in self.events.iter() {
+ match event.token() {
+ MESSAGE => {
+ let mut done = false;
+
+ 'process_batch: loop {
+ check_ctrlc!(self.keep_running);
+
+ let resp_start = self.response_counter;
+
+ for i in 0..self.config.batch_size() {
+ match self.socket.recv_from(&mut self.buf) {
+ Ok((num_bytes, src_addr)) => {
+ match self.nonce_from_request(&self.buf, num_bytes) {
+ Ok(nonce) => {
+ self.requests.push((Vec::from(nonce), src_addr));
+ self.merkle.push_leaf(nonce);
+ }
+ Err(e) => {
+ self.num_bad_requests += 1;
+
+ info!(
+ "Invalid request: '{:?}' ({} bytes) from {} (#{} in batch, resp #{})",
+ e, num_bytes, src_addr, i, resp_start + i as u64
+ );
+ }
+ }
+ }
+ Err(e) => match e.kind() {
+ ErrorKind::WouldBlock => {
+ done = true;
+ break;
+ }
+ _ => {
+ error!(
+ "Error receiving from socket: {:?}: {:?}",
+ e.kind(),
+ e
+ );
+ break;
+ }
+ },
+ };
+ }
+
+ if self.requests.is_empty() {
+ break 'process_batch;
+ }
+
+ let merkle_root = self.merkle.compute_root();
+ let srep = self.online_key.make_srep(time::get_time(), &merkle_root);
+
+ for (i, &(ref nonce, ref src_addr)) in self.requests.iter().enumerate() {
+ let paths = self.merkle.get_paths(i);
+
+ let resp =
+ self.make_response(&srep, &self.cert_bytes, &paths, i as u32);
+ let resp_bytes = resp.encode().unwrap();
+
+ let bytes_sent = self
+ .socket
+ .send_to(&resp_bytes, &src_addr)
+ .expect("send_to failed");
+
+ self.response_counter += 1;
+
+ info!(
+ "Responded {} bytes to {} for '{}..' (#{} in batch, resp #{})",
+ bytes_sent,
+ src_addr,
+ hex::encode(&nonce[0..4]),
+ i,
+ self.response_counter
+ );
+ }
+
+ self.merkle.reset();
+ self.requests.clear();
+
+ if done {
+ break 'process_batch;
+ }
+ }
+ }
+
+ HEALTH_CHECK => {
+ let listener = self.health_listener.as_ref().unwrap();
+
+ match listener.accept() {
+ Ok((ref mut stream, src_addr)) => {
+ info!("health check from {}", src_addr);
+
+ match stream.write(HTTP_RESPONSE.as_bytes()) {
+ Ok(_) => (),
+ Err(e) => warn!("error writing health check {}", e),
+ }
+
+ match stream.shutdown(Shutdown::Both) {
+ Ok(_) => (),
+ Err(e) => warn!("error in health check socket shutdown {}", e),
+ }
+ }
+ Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
+ debug!("blocking in TCP health check");
+ }
+ Err(e) => {
+ warn!("unexpected health check error {}", e);
+ }
+ }
+ }
+
+ STATUS => {
+ info!(
+ "responses {}, invalid requests {}",
+ self.response_counter, self.num_bad_requests
+ );
+
+ self.timer.set_timeout(self.config.status_interval(), ());
+ }
+
+ _ => unreachable!(),
+ }
+ }
+ false
+ }
+
+ /// Returns a reference to the server's long-term public key
+ pub fn get_public_key(&self) -> &str {
+ &self.public_key
+ }
+
+ /// Returns a reference to the server's on-line (delegated) key
+ pub fn get_online_key(&self) -> &OnlineKey {
+ &self.online_key
+ }
+
+ /// Returns a reference to the `ServerConfig` this server was configured with
+ pub fn get_config(&self) -> &Box<ServerConfig> {
+ &self.config
+ }
+
+ #[cfg(fuzzing)]
+ pub fn send_to_self(&mut self, data: &[u8]) {
+ self.response_counter = 0;
+ self.num_bad_requests = 0;
+ let res = self
+ .fake_client_socket
+ .send_to(data, &self.socket.local_addr().unwrap());
+ info!("Sent to self: {:?}", res);
+ }
+}
diff --git a/src/sign.rs b/src/sign.rs
index bd141b0..5fca564 100644
--- a/src/sign.rs
+++ b/src/sign.rs
@@ -20,16 +20,18 @@ extern crate hex;
extern crate ring;
extern crate untrusted;
-use self::ring::signature;
-use self::ring::signature::Ed25519KeyPair;
use self::ring::rand;
use self::ring::rand::SecureRandom;
+use self::ring::signature;
+use self::ring::signature::Ed25519KeyPair;
use self::untrusted::Input;
use std::fmt;
use std::fmt::Formatter;
+const INITIAL_BUF_SIZE: usize = 1024;
+
/// A multi-step (init-update-finish) interface for verifying an Ed25519 signature
#[derive(Debug)]
pub struct Verifier<'a> {
@@ -41,7 +43,7 @@ impl<'a> Verifier<'a> {
pub fn new(pubkey: &'a [u8]) -> Self {
Verifier {
pubkey: Input::from(pubkey),
- buf: Vec::with_capacity(256),
+ buf: Vec::with_capacity(INITIAL_BUF_SIZE),
}
}
@@ -80,7 +82,7 @@ impl Signer {
let seed_input = Input::from(seed);
Signer {
key_pair: Ed25519KeyPair::from_seed_unchecked(seed_input).unwrap(),
- buf: Vec::with_capacity(256),
+ buf: Vec::with_capacity(INITIAL_BUF_SIZE),
}
}
diff --git a/src/tag.rs b/src/tag.rs
index 14fd39e..14d6b04 100644
--- a/src/tag.rs
+++ b/src/tag.rs
@@ -15,7 +15,7 @@
use error::Error;
/// An unsigned 32-bit value (key) that maps to a byte-string (value).
-#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone)]
+#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone, Copy)]
pub enum Tag {
// Enforcement of the "tags in strictly increasing order" rule is done using the
// little-endian encoding of the ASCII tag value; e.g. 'SIG\x00' is 0x00474953 and
@@ -40,8 +40,8 @@ pub enum Tag {
impl Tag {
/// Translates a tag into its on-the-wire representation
- pub fn wire_value(&self) -> &'static [u8] {
- match *self {
+ pub fn wire_value(self) -> &'static [u8] {
+ match self {
Tag::CERT => b"CERT",
Tag::DELE => b"DELE",
Tag::INDX => b"INDX",