diff options
author | Stuart Stock <stuart@int08h.com> | 2018-10-27 14:20:00 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-27 14:20:00 -0500 |
commit | ff90f0d8a4648048d94a8b13ced0f697c8df0b71 (patch) | |
tree | 890c4e1d0a60f9cf5c22eedc62521061266c38b8 | |
parent | b43bcb27ad303afd56cfe1d767e95c10cf3d1cb2 (diff) | |
parent | 86345e4538cedccae80811fd6d165a3a7a948485 (diff) | |
download | roughenough-ff90f0d8a4648048d94a8b13ced0f697c8df0b71.zip |
Merge pull request #12 from int08h/1.1
Merge 1.1.0
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | CHANGELOG.md | 46 | ||||
-rw-r--r-- | Cargo.toml | 19 | ||||
-rw-r--r-- | README.md | 36 | ||||
-rw-r--r-- | doc/OPTIONAL-FEATURES.md | 217 | ||||
-rw-r--r-- | example.cfg | 1 | ||||
-rw-r--r-- | src/bin/roughenough-client.rs | 87 | ||||
-rw-r--r-- | src/bin/roughenough-kms.rs | 114 | ||||
-rw-r--r-- | src/bin/roughenough-server.rs | 247 | ||||
-rw-r--r-- | src/config/environment.rs | 70 | ||||
-rw-r--r-- | src/config/file.rs | 33 | ||||
-rw-r--r-- | src/config/memory.rs | 78 | ||||
-rw-r--r-- | src/config/mod.rs | 67 | ||||
-rw-r--r-- | src/error.rs | 20 | ||||
-rw-r--r-- | src/key/longterm.rs | 69 | ||||
-rw-r--r-- | src/key/mod.rs | 99 | ||||
-rw-r--r-- | src/key/online.rs (renamed from src/keys.rs) | 45 | ||||
-rw-r--r-- | src/kms/awskms.rs | 130 | ||||
-rw-r--r-- | src/kms/envelope.rs | 289 | ||||
-rw-r--r-- | src/kms/gcpkms.rs | 169 | ||||
-rw-r--r-- | src/kms/mod.rs | 226 | ||||
-rw-r--r-- | src/lib.rs | 45 | ||||
-rw-r--r-- | src/merkle.rs | 2 | ||||
-rw-r--r-- | src/message.rs | 3 | ||||
-rw-r--r-- | src/server.rs | 389 | ||||
-rw-r--r-- | src/sign.rs | 10 | ||||
-rw-r--r-- | src/tag.rs | 6 |
27 files changed, 2162 insertions, 358 deletions
@@ -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 @@ -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 } @@ -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 + ))), + } +} @@ -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), } } @@ -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", |