diff options
author | Stuart Stock <stuart@int08h.com> | 2018-10-14 14:47:09 -0500 |
---|---|---|
committer | Stuart Stock <stuart@int08h.com> | 2018-10-14 14:47:09 -0500 |
commit | ac71e92f11d91cca566dd38c78bcdef2db6c1811 (patch) | |
tree | d388a60b9bb743a50d185496756995dfe7605b52 /src | |
parent | b1acebdbc53bf3a8fab45e6f059ae35acb235a01 (diff) | |
download | roughenough-ac71e92f11d91cca566dd38c78bcdef2db6c1811.zip |
one-way GCP KMS working
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/roughenough-kms.rs | 27 | ||||
-rw-r--r-- | src/bin/roughenough-server.rs | 12 | ||||
-rw-r--r-- | src/kms/awskms.rs | 178 | ||||
-rw-r--r-- | src/kms/envelope.rs | 2 | ||||
-rw-r--r-- | src/kms/gcpkms.rs | 95 | ||||
-rw-r--r-- | src/kms/mod.rs | 31 | ||||
-rw-r--r-- | src/lib.rs | 3 |
7 files changed, 242 insertions, 106 deletions
diff --git a/src/bin/roughenough-kms.rs b/src/bin/roughenough-kms.rs index cb9a904..6563224 100644 --- a/src/bin/roughenough-kms.rs +++ b/src/bin/roughenough-kms.rs @@ -28,7 +28,7 @@ extern crate untrusted; use clap::{App, Arg}; use roughenough::VERSION; -#[cfg(feature = "kms")] +#[cfg(feature = "awskms")] fn aws_kms(kms_key: &str, plaintext_seed: &[u8]) { use roughenough::kms::EnvelopeEncryption; use roughenough::kms::AwsKms; @@ -46,6 +46,24 @@ 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; + + let client = GcpKms::from_resource_id(kms_key).unwrap(); + + match EnvelopeEncryption::encrypt_seed(&client, &plaintext_seed) { + Ok(encrypted_blob) => { + println!("key_protection: \"{}\"", kms_key); + println!("seed: {}", hex::encode(&encrypted_blob)); + } + Err(e) => { + error!("Error: {:?}", e); + } + } +} + #[allow(unused_variables)] pub fn main() { use log::Level; @@ -85,9 +103,12 @@ pub fn main() { return; } - if cfg!(feature = "kms") { - #[cfg(feature = "kms")] + if cfg!(feature = "awskms") { + #[cfg(feature = "awskms")] aws_kms(kms_key, &plaintext_seed); + } else if cfg!(feature = "gcpkms") { + #[cfg(feature = "gcpkms")] + gcp_kms(kms_key, &plaintext_seed); } else { warn!("KMS not enabled, nothing to do"); } diff --git a/src/bin/roughenough-server.rs b/src/bin/roughenough-server.rs index 52ae904..6a9cb5c 100644 --- a/src/bin/roughenough-server.rs +++ b/src/bin/roughenough-server.rs @@ -239,12 +239,22 @@ fn polling_loop(config: &Box<ServerConfig>, online_key: &mut OnlineKey, cert_byt } } +fn kms_support_str() -> &'static str { + if cfg!(feature = "awskms") { + " (+AWS KMS)" + } else if cfg!(feature = "gcpkms") { + " (+GCP KMS)" + } else { + "" + } +} + pub fn main() { use log::Level; simple_logger::init_with_level(Level::Info).unwrap(); - info!("Roughenough server v{} starting", VERSION); + info!("Roughenough server v{}{} starting", VERSION, kms_support_str()); let mut args = env::args(); if args.len() != 2 { diff --git a/src/kms/awskms.rs b/src/kms/awskms.rs index 396490f..96d4a38 100644 --- a/src/kms/awskms.rs +++ b/src/kms/awskms.rs @@ -15,114 +15,110 @@ 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 { +#[cfg(feature = "awskms")] +pub mod inner { + extern crate rusoto_core; + extern crate rusoto_kms; + + use std::default::Default; + use std::error::Error; + use std::fmt; + use std::fmt::Formatter; + use std::str::FromStr; + + use self::rusoto_core::Region; + use self::rusoto_kms::{DecryptRequest, EncryptRequest, Kms, KmsClient}; + use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK, DEK_SIZE_BYTES}; + + /// Amazon Key Management Service + pub struct AwsKms { + kms_client: KmsClient, + key_id: String, + } - /// 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(); + 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() - ))); - } + 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())), - }; + 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(), - }) + 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() - ))); - } + 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(), - )) + 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())), } - 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) + 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::InvalidKey(format!( - "decrypted DEK wrong length: {}", - plaintext_dek.len() - ))) + Err(KmsError::OperationFailed( + "decrypted payload is empty".to_string(), + )) } - } else { - Err(KmsError::OperationFailed( - "decrypted payload is empty".to_string(), - )) } + Err(e) => Err(KmsError::OperationFailed(e.description().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) + #[cfg(feature = "awskms")] + 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 index b6b536d..1f6d615 100644 --- a/src/kms/envelope.rs +++ b/src/kms/envelope.rs @@ -117,7 +117,7 @@ impl EnvelopeEncryption { /// /// Encrypt the seed value and protect the seed's encryption key using a - /// [KmsProvider](trait.KmsProvider.html). + /// [`KmsProvider`](trait.KmsProvider.html). /// /// The returned encrypted byte blob is safe to store on unsecured media. /// diff --git a/src/kms/gcpkms.rs b/src/kms/gcpkms.rs new file mode 100644 index 0000000..0dfdb29 --- /dev/null +++ b/src/kms/gcpkms.rs @@ -0,0 +1,95 @@ +// 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 = "gcpkms")] +pub mod inner { + extern crate base64; + extern crate hyper; + extern crate hyper_rustls; + extern crate yup_oauth2 as oauth2; + extern crate google_cloudkms1 as cloudkms1; + + use std::result::Result; + use std::default::Default; + use std::error::Error; + use std::fmt; + use std::fmt::Formatter; + use std::str::FromStr; + + use self::oauth2::{service_account_key_from_file, ServiceAccountAccess, ServiceAccountKey}; + use self::cloudkms1::CloudKMS; + use self::cloudkms1::{Result as CloudKmsResult, Error as CloudKmsError, EncryptRequest, DecryptRequest}; + use self::hyper::net::HttpsConnector; + use self::hyper::status::StatusCode; + use self::hyper_rustls::TlsClient; + + use kms::{EncryptedDEK, KmsError, KmsProvider, PlaintextDEK}; + + pub struct GcpKms { + key_resource_id: String, + service_account: ServiceAccountKey, + } + + impl GcpKms { + pub fn from_resource_id(resource_id: &str) -> Result<Self, KmsError> { + let client_secret = oauth2::service_account_key_from_file(&"creds.json".to_string()) + .unwrap(); + + Ok(GcpKms { + key_resource_id: resource_id.to_string(), + service_account: client_secret + }) + } + } + + impl KmsProvider for GcpKms { + fn encrypt_dek(&self, plaintext_dek: &PlaintextDEK) -> Result<EncryptedDEK, KmsError> { + let client1 = hyper::Client::with_connector(HttpsConnector::new(TlsClient::new())); + let access = oauth2::ServiceAccountAccess::new(self.service_account.clone(), client1); + + let client2 = hyper::Client::with_connector(HttpsConnector::new(TlsClient::new())); + let hub = CloudKMS::new(client2, access); + + let mut request = EncryptRequest::default(); + request.plaintext = Some(base64::encode(plaintext_dek)); + + let result = hub + .projects() + .locations_key_rings_crypto_keys_encrypt(request, &self.key_resource_id) + .doit(); + + match result { + Ok((http_resp, enc_resp)) => { + if http_resp.status == StatusCode::Ok { + let ciphertext = enc_resp.ciphertext.unwrap(); + let ct = base64::decode(&ciphertext)?; + Ok(ct) + } else { + Err(KmsError::OperationFailed(format!("{:?}", http_resp))) + } + } + Err(e) => Err(KmsError::OperationFailed(e.description().to_string())) + } + } + + fn decrypt_dek(&self, encrypted_dek: &EncryptedDEK) -> Result<PlaintextDEK, KmsError> { + Ok(Vec::new()) + } + } +} + + diff --git a/src/kms/mod.rs b/src/kms/mod.rs index 60c5a86..efc1def 100644 --- a/src/kms/mod.rs +++ b/src/kms/mod.rs @@ -34,8 +34,8 @@ //! //! Envelope encryption protects the seed by encrypting it with a locally generated 256-bit //! Data Encryption Key (DEK). The DEK itself is then encrypted using a cloud key management -//! system (KMS). The resulting opaque encrypted "blob" (encrypted seed + encrypted DEK) is safe -//! to store in the Roughenough configuration. +//! system (KMS). The resulting opaque encrypted "blob" (encrypted seed + encrypted DEK) is +//! stored in the Roughenough configuration. //! //! At server start-up the KMS is used to decrypt the DEK, which is then used to (in memory) //! decrypt the seed. The seed is used to generate the @@ -51,8 +51,7 @@ mod envelope; -pub use self::envelope::EnvelopeEncryption; - +use base64; use std; use std::error::Error; use ring; @@ -61,6 +60,8 @@ use config::ServerConfig; use error; use key::KeyProtection; +pub use self::envelope::EnvelopeEncryption; + /// Errors generated by KMS operations #[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone)] pub enum KmsError { @@ -82,6 +83,12 @@ impl From<ring::error::Unspecified> for KmsError { } } +impl From<base64::DecodeError> for KmsError { + fn from(error: base64::DecodeError) -> Self { + KmsError::OperationFailed(format!("base64: {}", error)) + } +} + // Size of the AEAD nonce in bytes. const NONCE_SIZE_BYTES: usize = 12; @@ -112,11 +119,17 @@ pub trait KmsProvider { fn decrypt_dek(&self, encrypted_dek: &EncryptedDEK) -> Result<PlaintextDEK, KmsError>; } -#[cfg(feature = "kms")] +#[cfg(feature = "gcpkms")] +mod gcpkms; + +#[cfg(feature = "gcpkms")] +pub use kms::gcpkms::inner::GcpKms; + +#[cfg(feature = "awskms")] mod awskms; -#[cfg(feature = "kms")] -pub use kms::awskms::AwsKms; +#[cfg(feature = "awskms")] +pub use kms::awskms::inner::AwsKms; /// Load the seed value for the long-term key. /// @@ -130,7 +143,7 @@ pub use kms::awskms::AwsKms; /// is parsed as a KMS key id and `EnvelopeEncryption::decrypt_seed` is called to obtain /// the plaintext seed value. /// -#[cfg(feature = "kms")] +#[cfg(feature = "awskms")] pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> { use kms::envelope::EnvelopeEncryption; @@ -154,7 +167,7 @@ pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> { /// The KMS feature was disabled in this build of Roughenough. The only supported `key_protection` /// value is `plaintext`. Any other value is an error. /// -#[cfg(not(feature = "kms"))] +#[cfg(not(feature = "awskms"))] pub fn load_seed(config: &Box<ServerConfig>) -> Result<Vec<u8>, error::Error> { match config.key_protection() { KeyProtection::Plaintext => Ok(config.seed()), @@ -48,7 +48,7 @@ //! # Server //! //! 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. +//! ways it can be configured, see [`ServerConfig`](config/trait.ServerConfig.html) for details. //! //! To run the server with a config file: //! @@ -57,6 +57,7 @@ //! ``` //! +extern crate base64; extern crate byteorder; extern crate core; extern crate time; |