summaryrefslogtreecommitdiff
path: root/src/kms
diff options
context:
space:
mode:
authorStuart Stock <stuart@int08h.com>2018-10-12 22:39:37 -0500
committerStuart Stock <stuart@int08h.com>2018-10-12 22:39:37 -0500
commitfec19a7d65c9dca293056f40b4a1983b82a0e68d (patch)
tree0e02ecc6174804a5be46f4fba19b7f98be6ab3d2 /src/kms
parented89d98692ac273ec7dfc39c19008334077779a3 (diff)
downloadroughenough-fec19a7d65c9dca293056f40b4a1983b82a0e68d.zip
Refactor to kms module; add documentation
Diffstat (limited to 'src/kms')
-rw-r--r--src/kms/awskms.rs128
-rw-r--r--src/kms/envelope.rs171
-rw-r--r--src/kms/mod.rs145
3 files changed, 444 insertions, 0 deletions
diff --git a/src/kms/awskms.rs b/src/kms/awskms.rs
new file mode 100644
index 0000000..396490f
--- /dev/null
+++ b/src/kms/awskms.rs
@@ -0,0 +1,128 @@
+// 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 = "kms")]
+extern crate rusoto_core;
+#[cfg(feature = "kms")]
+extern crate rusoto_kms;
+
+#[cfg(feature = "kms")]
+use self::rusoto_core::Region;
+#[cfg(feature = "kms")]
+use self::rusoto_kms::{DecryptRequest, EncryptRequest, Kms, KmsClient};
+
+use std::default::Default;
+use std::error::Error;
+use std::fmt;
+use std::fmt::Formatter;
+use std::str::FromStr;
+
+use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK, DEK_SIZE_BYTES};
+
+/// Amazon Key Management Service
+#[cfg(feature = "kms")]
+pub struct AwsKms {
+ kms_client: KmsClient,
+ key_id: String,
+}
+
+#[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();
+
+ 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();
+
+ 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 = "kms")]
+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..b6b536d
--- /dev/null
+++ b/src/kms/envelope.rs
@@ -0,0 +1,171 @@
+// 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;
+use ring::rand::SecureRandom;
+
+use super::super::MIN_SEED_LENGTH;
+use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
+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;
+
+// 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;
+
+// Trivial domain separation to guard against KMS key reuse
+static AD: &[u8; 11] = b"roughenough";
+
+// Convenience function to create zero-filled Vec of given size
+fn vec_zero_filled(len: usize) -> Vec<u8> {
+ let mut v = Vec::with_capacity(len);
+ for _ in 0..len {
+ v.push(0);
+ }
+ 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!(
+ "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>()?;
+ let nonce_len = tmp.read_u16::<LittleEndian>()?;
+
+ // Consume the wrapped DEK
+ let mut encrypted_dek = vec_zero_filled(dek_len as usize);
+ tmp.read_exact(&mut encrypted_dek)?;
+
+ // Consume the nonce
+ let mut nonce = vec_zero_filled(nonce_len as usize);
+ 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, 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 = rand::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,
+ &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)
+ }
+}
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
+ ))),
+ }
+}