From a072d3c67efdcaf4a9d969150268ed0151febb60 Mon Sep 17 00:00:00 2001 From: Stuart Stock Date: Sun, 7 Oct 2018 21:44:53 -0500 Subject: wip checkpoint; nearly round-trip kms --- src/bin/kms.rs | 64 ------------------------- src/bin/roughenough-kms.rs | 81 +++++++++++++++++++++++++++++++ src/key/awskms.rs | 29 +++++++----- src/key/envelope.rs | 115 ++++++++++++++++++++++++++++++++++++++++++--- src/key/mod.rs | 29 ++++++++++-- src/lib.rs | 1 - 6 files changed, 231 insertions(+), 88 deletions(-) delete mode 100644 src/bin/kms.rs create mode 100644 src/bin/roughenough-kms.rs (limited to 'src') diff --git a/src/bin/kms.rs b/src/bin/kms.rs deleted file mode 100644 index 3661e48..0000000 --- a/src/bin/kms.rs +++ /dev/null @@ -1,64 +0,0 @@ -// 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. - -//! -//! Work with Roughenough long-term key -//! - -#[macro_use] -extern crate clap; -#[macro_use] -extern crate log; -extern crate ring; -extern crate roughenough; -extern crate simple_logger; -extern crate untrusted; - -#[cfg(feature = "kms")] -use roughenough::key::awskms::AwsKms; - -use std::default::Default; - -use clap::{App, Arg}; -use roughenough::VERSION; - -pub fn main() { - use log::Level; - - simple_logger::init_with_level(Level::Info).unwrap(); - - let matches = App::new("Roughenough key management") - .version(VERSION) - .arg( - Arg::with_name("operation") - .required(true) - .help("The operation to perform") - .takes_value(true), - ).get_matches(); - - if cfg!(feature = "kms") { - info!("KMS feature enabled"); - let client = AwsKms::from_arn( - "arn:aws:kms:us-east-2:927891522318:key/1c96fb2c-d417-48f4-bf24-8e7173a587f5", - ).unwrap(); - - let ciphertext = client.encrypt("This is a test".as_ref()).unwrap(); - info!("Ciphertext: {:?}", ciphertext); - - let plaintext = String::from_utf8(client.decrypt(ciphertext.as_ref()).unwrap()).unwrap(); - info!("Plaintext : {:?}", plaintext); - } - - info!("Done"); -} diff --git a/src/bin/roughenough-kms.rs b/src/bin/roughenough-kms.rs new file mode 100644 index 0000000..671aa75 --- /dev/null +++ b/src/bin/roughenough-kms.rs @@ -0,0 +1,81 @@ +// 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. + +//! +//! Work with Roughenough long-term key +//! + +#[macro_use] +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 std::default::Default; + +use clap::{App, Arg}; +use roughenough::key::{EnvelopeEncryption, KmsProvider}; +use roughenough::VERSION; + +#[cfg(feature = "kms")] +use roughenough::key::awskms::AwsKms; + +#[cfg(feature = "kms")] +fn aws_kms() { + let client = AwsKms::from_arn( + "arn:aws:kms:us-east-2:927891522318:key/1c96fb2c-d417-48f4-bf24-8e7173a587f5", + ).unwrap(); + + let plaintext_seed = [b'a'; 32]; + match EnvelopeEncryption::encrypt_seed(&client, &plaintext_seed) { + Ok(bundle) => { + info!("Bundle len={}", bundle.len()); + info!("{}", hex::encode(&bundle)); + + EnvelopeEncryption::decrypt_seed(&client, &bundle); + } + Err(e) => { + error!("Error: {:?}", e); + } + } +} + +pub fn main() { + use log::Level; + + simple_logger::init_with_level(Level::Info).unwrap(); + + let matches = App::new("Roughenough key management") + .version(VERSION) + .arg( + Arg::with_name("operation") + .required(true) + .help("The operation to perform") + .takes_value(true), + ).get_matches(); + + if cfg!(feature = "kms") { + info!("KMS feature enabled"); + #[cfg(feature = "kms")] + { + aws_kms(); + } + } + + info!("Done"); +} diff --git a/src/key/awskms.rs b/src/key/awskms.rs index bb8d215..234864b 100644 --- a/src/key/awskms.rs +++ b/src/key/awskms.rs @@ -46,9 +46,10 @@ impl AwsKms { let parts: Vec<&str> = arn.split(':').collect(); if parts.len() != 6 { - return Err(KmsError::InvalidConfiguration( - format!("invalid KMS arn: too few parts {}", parts.len()) - )); + return Err(KmsError::InvalidConfiguration(format!( + "invalid KMS arn: too few parts {}", + parts.len() + ))); } let region_part = parts.get(3).expect("region is missing"); @@ -67,9 +68,10 @@ impl AwsKms { impl KmsProvider for AwsKms { fn encrypt_dek(&self, plaintext_dek: &PlaintextDEK) -> Result { if plaintext_dek.len() != DEK_SIZE_BYTES { - return Err(KmsError::InvalidKey( - format!("provided DEK wrong length: {}", plaintext_dek.len()), - )); + return Err(KmsError::InvalidKey(format!( + "provided DEK wrong length: {}", + plaintext_dek.len() + ))); } let mut encrypt_req: EncryptRequest = Default::default(); @@ -81,12 +83,12 @@ impl KmsProvider for AwsKms { if let Some(ciphertext) = result.ciphertext_blob { Ok(ciphertext) } else { - Err(KmsError::EncryptionFailed( + Err(KmsError::OperationFailed( "no ciphertext despite successful response".to_string(), )) } } - Err(e) => Err(KmsError::EncryptionFailed(e.description().to_string())), + Err(e) => Err(KmsError::OperationFailed(e.description().to_string())), } } @@ -100,17 +102,18 @@ impl KmsProvider for AwsKms { if plaintext_dek.len() == DEK_SIZE_BYTES { Ok(plaintext_dek) } else { - Err(KmsError::InvalidKey( - format!("decrypted DEK wrong length: {}", plaintext_dek.len()) - )) + Err(KmsError::InvalidKey(format!( + "decrypted DEK wrong length: {}", + plaintext_dek.len() + ))) } } else { - Err(KmsError::DecryptionFailed( + Err(KmsError::OperationFailed( "decrypted payload is empty".to_string(), )) } } - Err(e) => Err(KmsError::DecryptionFailed(e.description().to_string())), + Err(e) => Err(KmsError::OperationFailed(e.description().to_string())), } } } diff --git a/src/key/envelope.rs b/src/key/envelope.rs index 5c869c5..1bcc077 100644 --- a/src/key/envelope.rs +++ b/src/key/envelope.rs @@ -14,22 +14,123 @@ extern crate hex; -use ring::aead::AES_256_GCM; +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 key::KmsProvider; +use super::super::MIN_SEED_LENGTH; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use key::{KmsError, KmsProvider, DEK_SIZE_BYTES, NONCE_SIZE_BYTES, TAG_SIZE_BYTES}; -/// /// 2 bytes - encrypted DEK length +/// 2 bytes - nonce length /// n bytes - encrypted DEK -/// 2 bytes - encrypted +/// n bytes - nonce +/// n bytes - opaque (encrypted seed + tag) pub struct EnvelopeEncryption; +static AD: &[u8; 11] = b"roughenough"; + +const DEK_LEN_FIELD: usize = 2; +const NONCE_LEN_FIELD: usize = 2; + +fn zero_filled(len: usize) -> Vec { + let mut v = Vec::with_capacity(len); + for _ in 0..len { + v.push(0); + } + return v; +} + impl EnvelopeEncryption { - pub fn encrypt(kms: &KmsProvider, plaintext: &[u8]) -> Vec { + pub fn decrypt_seed(kms: &KmsProvider, ciphertext_blob: &[u8]) -> Result, KmsError> { + let min_size = DEK_LEN_FIELD + + NONCE_LEN_FIELD + + DEK_SIZE_BYTES + + NONCE_SIZE_BYTES + + TAG_SIZE_BYTES + + MIN_SEED_LENGTH as usize; + + if ciphertext_blob.len() < min_size { + return Err(KmsError::InvalidData( + format!("ciphertext too short: min {}, found {}", min_size, ciphertext_blob.len()) + )); + } + /// 2 bytes - encrypted DEK length + /// 2 bytes - nonce length + /// n bytes - encrypted DEK + /// n bytes - nonce + /// n bytes - encrypted seed + + let mut tmp = Cursor::new(ciphertext_blob.clone()); + let dek_len = tmp.read_u16::()?; + let nonce_len = tmp.read_u16::()?; + + let mut encrypted_dek = zero_filled(usize::from(dek_len)); + tmp.read_exact(&mut encrypted_dek)?; + + let mut nonce = zero_filled(usize::from(nonce_len)); + tmp.read_exact(&mut nonce)?; + + let mut encrypted_blob = zero_filled(ciphertext_blob.len() - tmp.position() as usize); + tmp.read_to_end(&mut encrypted_blob)?; + + info!("dek len {}", dek_len); + info!("nonce len {}", nonce_len); + info!("enc dec {}", hex::encode(encrypted_dek)); + info!("nonce {}", hex::encode(nonce)); + info!("blob {}", hex::encode(encrypted_blob)); + + Ok(Vec::new()) + } + + pub fn encrypt_seed(kms: &KmsProvider, plaintext_seed: &[u8]) -> Result, KmsError> { + // Generate random DEK and nonce let rng = rand::SystemRandom::new(); - let mut dek = [0u8; 16]; - rng.fill(&mut dek).unwrap(); + 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 + 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(e) => { + return Err(KmsError::OperationFailed( + "failed to encrypt plaintext seed".to_string(), + )) + } + }; + + // Wrap the DEK + let wrapped_dek = kms.encrypt_dek(&dek.to_vec())?; + + // And coalesce everything together + let mut output = Vec::new(); + output.write_u16::(wrapped_dek.len() as u16)?; + output.write_u16::(nonce.len() as u16)?; + output.write_all(&wrapped_dek)?; + output.write_all(&nonce)?; + output.write_all(&encrypted_seed)?; + + info!("dek {}", hex::encode(&dek)); + info!("enc dek {}", hex::encode(&wrapped_dek)); + info!("nonce {}", hex::encode(&nonce)); + info!("blob {}", hex::encode(&encrypted_seed)); + + Ok(output) } } diff --git a/src/key/mod.rs b/src/key/mod.rs index b269af2..62c37a9 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -18,11 +18,16 @@ extern crate hex; extern crate log; +extern crate ring; +extern crate std; mod envelope; mod longterm; mod online; +use std::error::Error; + +pub use self::envelope::EnvelopeEncryption; pub use self::longterm::LongTermKey; pub use self::online::OnlineKey; @@ -41,17 +46,35 @@ pub enum KeyProtection { GoogleKmsEnvelope, } -#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone)] pub enum KmsError { - DecryptionFailed(String), - EncryptionFailed(String), + OperationFailed(String), InvalidConfiguration(String), + InvalidData(String), InvalidKey(String), } +impl From for KmsError { + fn from(error: std::io::Error) -> Self { + KmsError::OperationFailed(error.description().to_string()) + } +} + +impl From for KmsError { + fn from(error: 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; diff --git a/src/lib.rs b/src/lib.rs index 4bfbd81..0903f95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,7 +49,6 @@ //! extern crate byteorder; -extern crate clear_on_drop; extern crate core; extern crate time; extern crate yaml_rust; -- cgit v1.2.3