diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/roughenough-kms.rs | 10 | ||||
-rw-r--r-- | src/bin/roughenough-server.rs | 10 | ||||
-rw-r--r-- | src/config/memory.rs | 1 | ||||
-rw-r--r-- | src/key/mod.rs | 37 | ||||
-rw-r--r-- | src/kms/envelope.rs | 121 | ||||
-rw-r--r-- | src/kms/gcpkms.rs | 58 | ||||
-rw-r--r-- | src/kms/mod.rs | 4 | ||||
-rw-r--r-- | src/lib.rs | 10 | ||||
-rw-r--r-- | src/server.rs | 1 |
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 ))), } } @@ -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; |