summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStuart Stock <stuart@int08h.com>2018-10-21 15:04:56 -0500
committerStuart Stock <stuart@int08h.com>2018-10-21 15:04:56 -0500
commit44e6212e3480d2f3b15f30434abc892adcf3836f (patch)
treeddd3e4a80243acb008319c8c8b76bd37fc6516f9
parentb22a6f055f4578b96224ff2372a0ba0c5942a00f (diff)
downloadroughenough-44e6212e3480d2f3b15f30434abc892adcf3836f.zip
Add tests for envelope cypto and some enums
-rw-r--r--src/bin/roughenough-kms.rs10
-rw-r--r--src/bin/roughenough-server.rs10
-rw-r--r--src/config/memory.rs1
-rw-r--r--src/key/mod.rs37
-rw-r--r--src/kms/envelope.rs121
-rw-r--r--src/kms/gcpkms.rs58
-rw-r--r--src/kms/mod.rs4
-rw-r--r--src/lib.rs10
-rw-r--r--src/server.rs1
9 files changed, 196 insertions, 56 deletions
diff --git a/src/bin/roughenough-kms.rs b/src/bin/roughenough-kms.rs
index 1cea22e..8b3b26a 100644
--- a/src/bin/roughenough-kms.rs
+++ b/src/bin/roughenough-kms.rs
@@ -13,7 +13,7 @@
// limitations under the License.
//!
-//! Work with Roughenough long-term key
+//! CLI used to encrypt the Roughenough long-term key using one of the KMS implementations
//!
extern crate clap;
@@ -30,8 +30,7 @@ use roughenough::VERSION;
#[cfg(feature = "awskms")]
fn aws_kms(kms_key: &str, plaintext_seed: &[u8]) {
- use roughenough::kms::AwsKms;
- use roughenough::kms::EnvelopeEncryption;
+ use roughenough::kms::{AwsKms, EnvelopeEncryption};
let client = AwsKms::from_arn(kms_key).unwrap();
@@ -48,8 +47,7 @@ fn aws_kms(kms_key: &str, plaintext_seed: &[u8]) {
#[cfg(feature = "gcpkms")]
fn gcp_kms(kms_key: &str, plaintext_seed: &[u8]) {
- use roughenough::kms::EnvelopeEncryption;
- use roughenough::kms::GcpKms;
+ use roughenough::kms::{EnvelopeEncryption, GcpKms};
let client = GcpKms::from_resource_id(kms_key).unwrap();
@@ -110,6 +108,6 @@ pub fn main() {
#[cfg(feature = "gcpkms")]
gcp_kms(kms_key, &plaintext_seed);
} else {
- warn!("KMS not enabled, nothing to do");
+ warn!("KMS support is not enabled, nothing to do");
}
}
diff --git a/src/bin/roughenough-server.rs b/src/bin/roughenough-server.rs
index 6c6a118..c908133 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;
diff --git a/src/config/memory.rs b/src/config/memory.rs
index abca5a5..0f65be1 100644
--- a/src/config/memory.rs
+++ b/src/config/memory.rs
@@ -12,7 +12,6 @@
// 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::KeyProtection;
diff --git a/src/key/mod.rs b/src/key/mod.rs
index 6bb3eb5..5ce0296 100644
--- a/src/key/mod.rs
+++ b/src/key/mod.rs
@@ -55,14 +55,45 @@ impl Display for KeyProtection {
}
impl FromStr for KeyProtection {
- type Err = ();
+ type Err = String;
- fn from_str(s: &str) -> Result<KeyProtection, ()> {
+ fn from_str(s: &str) -> Result<KeyProtection, String> {
match s {
"plaintext" => Ok(KeyProtection::Plaintext),
s if s.starts_with("arn:") => Ok(KeyProtection::AwsKmsEnvelope(s.to_string())),
s if s.starts_with("projects/") => Ok(KeyProtection::GoogleKmsEnvelope(s.to_string())),
- _ => Err(()),
+ s => Err(format!("unknown KeyProtection '{}'", s)),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use key::KeyProtection;
+ 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 KeyProtection::from_str("plaintext") {
+ Ok(KeyProtection::Plaintext) => (),
+ e => panic!("unexpected result {:?}", e),
+ };
+ match KeyProtection::from_str(arn) {
+ Ok(KeyProtection::AwsKmsEnvelope(msg)) => assert_eq!(msg, arn),
+ e => panic!("unexpected result {:?}", e),
+ }
+ match KeyProtection::from_str(resource_id) {
+ Ok(KeyProtection::GoogleKmsEnvelope(msg)) => assert_eq!(msg, resource_id),
+ e => panic!("unexpected result {:?}", e),
+ }
+ match KeyProtection::from_str("frobble") {
+ Err(msg) => assert!(msg.contains("unknown KeyProtection")),
+ e => panic!("unexpected result {:?}", e),
}
}
}
diff --git a/src/kms/envelope.rs b/src/kms/envelope.rs
index 2dc7262..00206de 100644
--- a/src/kms/envelope.rs
+++ b/src/kms/envelope.rs
@@ -126,7 +126,7 @@ impl EnvelopeEncryption {
///
pub fn encrypt_seed(kms: &KmsProvider, plaintext_seed: &[u8]) -> Result<Vec<u8>, KmsError> {
// Generate random DEK and nonce
- let rng = rand::SystemRandom::new();
+ let rng = SystemRandom::new();
let mut dek = [0u8; DEK_SIZE_BYTES];
let mut nonce = [0u8; NONCE_SIZE_BYTES];
rng.fill(&mut dek)?;
@@ -172,3 +172,122 @@ impl EnvelopeEncryption {
Ok(output)
}
}
+
+#[cfg(test)]
+mod test {
+ use hex;
+ use kms::envelope::{DEK_LEN_FIELD, MIN_PAYLOAD_SIZE, NONCE_LEN_FIELD};
+ use kms::EnvelopeEncryption;
+ use kms::{KmsError, KmsProvider};
+ use std::str::FromStr;
+ use std::string::ToString;
+
+ 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[0] = 1;
+ 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
index d2590f5..1401925 100644
--- a/src/kms/gcpkms.rs
+++ b/src/kms/gcpkms.rs
@@ -17,48 +17,46 @@ 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::fmt;
- use std::env;
- use std::fmt::Formatter;
- use std::str::FromStr;
- use std::result::Result;
use std::default::Default;
- use std::error::Error;
+ use std::env;
use std::path::Path;
- use std::time::Duration;
+ use std::result::Result;
use self::cloudkms1::CloudKMS;
- use self::cloudkms1::{
- DecryptRequest, EncryptRequest, Error as CloudKmsError, Result as CloudKmsResult,
- };
+ use self::cloudkms1::{DecryptRequest, EncryptRequest};
use self::hyper::net::HttpsConnector;
- use self::hyper::header::Headers;
use self::hyper::status::StatusCode;
use self::hyper_rustls::TlsClient;
- use self::oauth2::{service_account_key_from_file, ServiceAccountAccess, ServiceAccountKey};
+ use self::oauth2::{ServiceAccountAccess, ServiceAccountKey};
+
+ use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK, AD};
- use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK};
+ 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
+ service_account: svc_acct,
})
}
@@ -125,10 +123,9 @@ pub mod inner {
Err(self.pretty_http_error(&http_resp))
}
}
- Err(e) => Err(KmsError::OperationFailed(format!("decrypt_dek() {:?}", e)))
+ Err(e) => Err(KmsError::OperationFailed(format!("decrypt_dek() {:?}", e))),
}
}
-
}
/// Minimal implementation of Application Default Credentials.
@@ -137,27 +134,36 @@ pub mod inner {
/// 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_APPLICATION_CREDENTIALS") {
+ 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)))
+ return Err(KmsError::InvalidConfiguration(format!(
+ "Can't load service account credential '{}': {:?}",
+ gac, e
+ )))
}
}
} else {
- return Err(KmsError::InvalidConfiguration(
- format!("GOOGLE_APPLICATION_CREDENTIALS='{}' does not exist", gac)))
+ return Err(KmsError::InvalidConfiguration(format!(
+ "{} ='{}' does not exist",
+ GOOGLE_APP_CREDS, gac
+ )));
}
-
}
- // TODO: call to metadata service to get default credential from
+ // 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 GOOGLE_APPLICATION_CREDENTIALS set?");
+ panic!(
+ "Failed to load service account credential. Is {} set?",
+ GOOGLE_APP_CREDS
+ );
}
}
diff --git a/src/kms/mod.rs b/src/kms/mod.rs
index aa609b5..b411578 100644
--- a/src/kms/mod.rs
+++ b/src/kms/mod.rs
@@ -54,7 +54,6 @@ mod envelope;
use base64;
use ring;
use std;
-use std::error::Error;
use config::ServerConfig;
use error;
@@ -205,7 +204,8 @@ 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 '{}' requires KMS, but server was not compiled with KMS support", v
+ "key_protection '{}' requires KMS, but server was not compiled with KMS support",
+ v
))),
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 369caad..25d4730 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -50,21 +50,15 @@
//! 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 with a config file:
-//!
-//! ```bash
-//! $ cargo run --release --bin server /path/to/config.file
-//! ```
-//!
extern crate base64;
extern crate byteorder;
extern crate core;
-extern crate time;
-extern crate yaml_rust;
extern crate hex;
extern crate mio;
extern crate mio_extras;
+extern crate time;
+extern crate yaml_rust;
#[macro_use]
extern crate log;
diff --git a/src/server.rs b/src/server.rs
index 0fd5f1a..fa26b34 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-
use hex;
use std::io::ErrorKind;
use std::net::SocketAddr;