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/kms | |
parent | ed89d98692ac273ec7dfc39c19008334077779a3 (diff) | |
download | roughenough-fec19a7d65c9dca293056f40b4a1983b82a0e68d.zip |
Refactor to kms module; add documentation
Diffstat (limited to 'src/kms')
-rw-r--r-- | src/kms/awskms.rs | 128 | ||||
-rw-r--r-- | src/kms/envelope.rs | 171 | ||||
-rw-r--r-- | src/kms/mod.rs | 145 |
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 + ))), + } +} |