diff options
author | Stuart Stock <stuart@int08h.com> | 2018-10-12 22:39:37 -0500 |
---|---|---|
committer | Stuart Stock <stuart@int08h.com> | 2018-10-12 22:39:37 -0500 |
commit | fec19a7d65c9dca293056f40b4a1983b82a0e68d (patch) | |
tree | 0e02ecc6174804a5be46f4fba19b7f98be6ab3d2 /src | |
parent | ed89d98692ac273ec7dfc39c19008334077779a3 (diff) | |
download | roughenough-fec19a7d65c9dca293056f40b4a1983b82a0e68d.zip |
Refactor to kms module; add documentation
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/roughenough-kms.rs | 14 | ||||
-rw-r--r-- | src/bin/roughenough-server.rs | 4 | ||||
-rw-r--r-- | src/config/environment.rs | 2 | ||||
-rw-r--r-- | src/config/file.rs | 2 | ||||
-rw-r--r-- | src/config/mod.rs | 20 | ||||
-rw-r--r-- | src/error.rs | 2 | ||||
-rw-r--r-- | src/key/mod.rs | 88 | ||||
-rw-r--r-- | src/kms/awskms.rs (renamed from src/key/awskms.rs) | 5 | ||||
-rw-r--r-- | src/kms/envelope.rs (renamed from src/key/envelope.rs) | 26 | ||||
-rw-r--r-- | src/kms/mod.rs | 145 | ||||
-rw-r--r-- | src/lib.rs | 19 |
11 files changed, 217 insertions, 110 deletions
diff --git a/src/bin/roughenough-kms.rs b/src/bin/roughenough-kms.rs index 072f451..cb9a904 100644 --- a/src/bin/roughenough-kms.rs +++ b/src/bin/roughenough-kms.rs @@ -28,14 +28,11 @@ extern crate untrusted; use clap::{App, Arg}; use roughenough::VERSION; -#[allow(unused_imports)] -use roughenough::key::EnvelopeEncryption; - -#[cfg(feature = "kms")] -use roughenough::key::awskms::AwsKms; - #[cfg(feature = "kms")] fn aws_kms(kms_key: &str, plaintext_seed: &[u8]) { + use roughenough::kms::EnvelopeEncryption; + use roughenough::kms::AwsKms; + let client = AwsKms::from_arn(kms_key).unwrap(); match EnvelopeEncryption::encrypt_seed(&client, &plaintext_seed) { @@ -55,8 +52,9 @@ pub fn main() { simple_logger::init_with_level(Level::Info).unwrap(); - let matches = App::new("Roughenough key management") + let matches = App::new("roughenough-kms") .version(VERSION) + .long_about("Encrypt a Roughenough server's long-term seed using a KMS") .arg( Arg::with_name("KEY_ID") .short("k") @@ -70,7 +68,7 @@ pub fn main() { .long("seed") .takes_value(true) .required(true) - .help("Seed for the server's long-term identity"), + .help("32 byte hex seed for the server's long-term identity"), ).get_matches(); let kms_key = matches.value_of("KEY_ID").unwrap(); diff --git a/src/bin/roughenough-server.rs b/src/bin/roughenough-server.rs index 5be8620..52ae904 100644 --- a/src/bin/roughenough-server.rs +++ b/src/bin/roughenough-server.rs @@ -55,7 +55,7 @@ use byteorder::{LittleEndian, WriteBytesExt}; use roughenough::config; use roughenough::config::ServerConfig; -use roughenough::key; +use roughenough::kms; use roughenough::key::{LongTermKey, OnlineKey}; use roughenough::merkle::MerkleTree; use roughenough::{Error, RtMessage, Tag}; @@ -266,7 +266,7 @@ pub fn main() { let public_key: String; let cert_bytes = { - let seed = key::load_seed(&config).unwrap(); + let seed = kms::load_seed(&config).unwrap(); let mut long_term_key = LongTermKey::new(&seed); public_key = hex::encode(long_term_key.public_key()); diff --git a/src/config/environment.rs b/src/config/environment.rs index b7fe0da..2385b28 100644 --- a/src/config/environment.rs +++ b/src/config/environment.rs @@ -21,7 +21,7 @@ use std::time::Duration; use config::ServerConfig; use config::{DEFAULT_BATCH_SIZE, DEFAULT_STATUS_INTERVAL}; use Error; -use KeyProtection; +use key::KeyProtection; /// /// Obtain a Roughenough server configuration ([ServerConfig](trait.ServerConfig.html)) diff --git a/src/config/file.rs b/src/config/file.rs index 602baa1..440c78c 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -23,7 +23,7 @@ use yaml_rust::YamlLoader; use config::ServerConfig; use config::{DEFAULT_BATCH_SIZE, DEFAULT_STATUS_INTERVAL}; use Error; -use KeyProtection; +use key::KeyProtection; /// /// Read a Roughenough server configuration ([ServerConfig](trait.ServerConfig.html)) diff --git a/src/config/mod.rs b/src/config/mod.rs index f0e2a1a..f05578b 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; @@ -34,7 +35,7 @@ mod environment; pub use self::environment::EnvironmentConfig; use Error; -use KeyProtection; +use key::KeyProtection; /// Maximum number of requests to process in one batch and include the the Merkle tree. pub const DEFAULT_BATCH_SIZE: u8 = 64; @@ -55,6 +56,7 @@ pub const DEFAULT_STATUS_INTERVAL: Duration = Duration::from_secs(600); /// `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). +/// `key_protection` | `ROUGHENOUGH_KEY_PROTECTION` | Optional | Encryption method (if any) applied to the `seed`. Defaults to "`plaintext`" (no encryption, `seed` is in the clear). /// /// Implementations of this trait obtain a valid configuration from different back-end /// sources. See: @@ -82,15 +84,19 @@ 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 key_protection(&self) -> &KeyProtection; + /// Convenience function to create a `SocketAddr` from the provided `interface` and `port` fn socket_addr(&self) -> Result<SocketAddr, Error>; - - /// Method used to protect the long-term key pair. - fn key_protection(&self) -> &KeyProtection; } +/// 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" { @@ -107,7 +113,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; diff --git a/src/error.rs b/src/error.rs index 7ffe1eb..e91a340 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,7 +14,7 @@ use std; -use key::KmsError; +use kms::KmsError; use tag::Tag; /// Error types generated by this implementation diff --git a/src/key/mod.rs b/src/key/mod.rs index e59bfda..bd06931 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -21,29 +21,26 @@ extern crate log; extern crate ring; extern crate std; -mod envelope; mod longterm; mod online; -use std::error::Error; +use std::fmt::Display; +use std::fmt::Formatter; use std::str::FromStr; -pub use self::envelope::EnvelopeEncryption; pub use self::longterm::LongTermKey; pub use self::online::OnlineKey; -use super::config::ServerConfig; -use super::error; - +/// Methods for protecting the server's long-term identity #[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone)] pub enum KeyProtection { /// No protection, seed is in plaintext Plaintext, - /// Envelope encryption using AWS Key Management Service + /// Envelope encryption of the seed using AWS Key Management Service AwsKmsEnvelope(String), - /// Envelope encryption using Google Cloud Key Management Service + /// Envelope encryption of the seed using Google Cloud Key Management Service GoogleKmsEnvelope(String), } @@ -70,78 +67,3 @@ impl FromStr for KeyProtection { } } -#[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(error.description().to_string()) - } -} - -impl From<ring::error::Unspecified> for KmsError { - fn from(_: ring::error::Unspecified) -> Self { - KmsError::OperationFailed("unspecified ring cryptographic failure".to_string()) - } -} - -/// Size of the Data Encryption Key (DEK) in bytes -pub const DEK_SIZE_BYTES: usize = 32; - -/// Size of the AEAD nonce in bytes -pub const NONCE_SIZE_BYTES: usize = 12; - -/// Size of the AEAD authentication tag in bytes -pub const TAG_SIZE_BYTES: usize = 16; - -/// An unencrypted (plaintext) 256-bit Data Encryption Key (DEK). -type PlaintextDEK = Vec<u8>; - -/// A Data Encryption Key (DEK) that has been encrypted (wrapped) by a Key Encryption Key (KEK). -/// Size of the encrypted DEK is implementation specific (things like AEAD tag size, nonce size, -/// provider metadata, and so on will cause it to vary). -type EncryptedDEK = Vec<u8>; - -pub trait KmsProvider { - fn encrypt_dek(&self, plaintext_dek: &PlaintextDEK) -> Result<EncryptedDEK, KmsError>; - fn decrypt_dek(&self, encrypted_dek: &EncryptedDEK) -> Result<PlaintextDEK, KmsError>; -} - -#[cfg(feature = "kms")] -pub mod awskms; - -#[cfg(feature = "kms")] -use key::awskms::AwsKms; -use std::fmt::Display; -use std::fmt::Formatter; - -#[cfg(feature = "kms")] -pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> { - match config.key_protection() { - KeyProtection::Plaintext => Ok(config.seed()), - KeyProtection::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(not(feature = "kms"))] -pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> { - match config.key_protection() { - KeyProtection::Plaintext => Ok(config.seed()), - v => Err(error::Error::InvalidConfiguration(format!( - "key_protection '{}' implies KMS but server was not compiled with KMS support", v - ))), - } -} diff --git a/src/key/awskms.rs b/src/kms/awskms.rs index 878eff9..396490f 100644 --- a/src/key/awskms.rs +++ b/src/kms/awskms.rs @@ -31,8 +31,9 @@ use std::fmt; use std::fmt::Formatter; use std::str::FromStr; -use key::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK, DEK_SIZE_BYTES}; +use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK, DEK_SIZE_BYTES}; +/// Amazon Key Management Service #[cfg(feature = "kms")] pub struct AwsKms { kms_client: KmsClient, @@ -41,6 +42,8 @@ pub struct AwsKms { #[cfg(feature = "kms")] impl AwsKms { + + /// Create a new instance from the ARN of a AWS KMS key. pub fn from_arn(arn: &str) -> Result<Self, KmsError> { let parts: Vec<&str> = arn.split(':').collect(); diff --git a/src/key/envelope.rs b/src/kms/envelope.rs index ed774ae..b6b536d 100644 --- a/src/key/envelope.rs +++ b/src/kms/envelope.rs @@ -22,7 +22,8 @@ use ring::rand::SecureRandom; use super::super::MIN_SEED_LENGTH; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use key::{KmsError, KmsProvider, DEK_SIZE_BYTES, NONCE_SIZE_BYTES, TAG_SIZE_BYTES}; +use kms::{KmsError, KmsProvider, DEK_SIZE_BYTES, NONCE_SIZE_BYTES, TAG_SIZE_BYTES}; +use std::string::ToString; const DEK_LEN_FIELD: usize = 2; const NONCE_LEN_FIELD: usize = 2; @@ -54,9 +55,26 @@ fn vec_zero_filled(len: usize) -> Vec<u8> { return v; } +/// +/// 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!( @@ -97,6 +115,12 @@ impl EnvelopeEncryption { } } + /// + /// 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 = rand::SystemRandom::new(); diff --git a/src/kms/mod.rs b/src/kms/mod.rs new file mode 100644 index 0000000..b47202a --- /dev/null +++ b/src/kms/mod.rs @@ -0,0 +1,145 @@ +// 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. +//! +//! ## 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 +//! +//! Using envelope encryption the seed is protected by encrypting it with a locally generated +//! Data Encryption Key (DEK) and then encrypting the DEK using a cloud key management system +//! (KMS). See [`EnvelopeEncryption`](struct.EnvelopeEncryption.html) for the implementation. +//! +//! The resulting opaque encrypted "blob" (encrypted seed + encrypted DEK) is safely stored +//! in the Roughenough configuration. At server start-up the KMS is used to decrypt the DEK, +//! and the DEK is used to (temporarily in memory) decrypt the seed. The seed is used to +//! generate the [delegated on-line key](../key/struct.OnlineKey.html) after which the seed +//! is erased. +//! +//! For details see explanations from +//! [Google](https://cloud.google.com/kms/docs/envelope-encryption) or +//! [Amazon](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping). +//! + +mod envelope; + +pub use self::envelope::EnvelopeEncryption; + +use std; +use std::error::Error; +use ring; + +use config::ServerConfig; +use error; +use key::KeyProtection; + +/// 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(error.description().to_string()) + } +} + +impl From<ring::error::Unspecified> for KmsError { + fn from(_: ring::error::Unspecified) -> Self { + KmsError::OperationFailed("unspecified ring cryptographic failure".to_string()) + } +} + +// 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. +pub const DEK_SIZE_BYTES: usize = 32; + +/// 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 value and the size of the encrypted DEK is implementation specific. +/// Things like 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 = "kms")] +mod awskms; + +#[cfg(feature = "kms")] +pub use kms::awskms::AwsKms; + +#[cfg(feature = "kms")] +pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> { + use kms::envelope::EnvelopeEncryption; + + match config.key_protection() { + KeyProtection::Plaintext => Ok(config.seed()), + KeyProtection::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(not(feature = "kms"))] +pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> { + match config.key_protection() { + KeyProtection::Plaintext => Ok(config.seed()), + v => Err(error::Error::InvalidConfiguration(format!( + "key_protection '{}' implies 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,11 +47,10 @@ //! //! # 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 Roughtime server implementation is in `src/bin/server.rs`. The server has multiple +//! ways it can be configured, see [ServerConfig](config/trait.ServerConfig.html) for details. //! -//! To run the server: +//! To run the server with a config file: //! //! ```bash //! $ cargo run --release --bin server /path/to/config.file @@ -63,11 +72,11 @@ mod tag; pub mod config; pub mod key; +pub mod kms; pub mod merkle; pub mod sign; pub use error::Error; -pub use key::KeyProtection; pub use message::RtMessage; pub use tag::Tag; |