From c3688a4fd2e0cef2c7e727898ebbf2ad7177dc4f Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Mon, 24 Apr 2023 19:13:00 -0400 Subject: [PATCH 1/2] RUST-1417 Add support for GCP attached service accounts when using GCP KMS --- .evergreen/config.yml | 138 ++++++++++++++++-- .evergreen/env.sh | 1 + Cargo.toml | 4 + src/client/csfle/state_machine.rs | 71 ++++++++- src/runtime/http.rs | 6 +- .../atlas_planned_maintenance_testing/mod.rs | 9 +- src/test/csfle.rs | 42 +++++- 7 files changed, 245 insertions(+), 26 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index ceec42a61..040129e9a 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -645,6 +645,61 @@ functions: -v \ --fault revoked + "build and upload gcp kms test": + - command: shell.exec + params: + shell: bash + working_dir: "src" + script: | + ${PREPARE_SHELL} + + set +o xtrace + export GCPKMS_GCLOUD=${GCPKMS_GCLOUD} + export GCPKMS_PROJECT=${GCPKMS_PROJECT} + export GCPKMS_ZONE=${GCPKMS_ZONE} + export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME} + set -o xtrace + + mkdir test-contents + cp -r $MONGOCRYPT_LIB_DIR test-contents + + echo "Building test ... begin" + . ${PROJECT_DIRECTORY}/.evergreen/configure-rust.sh + cargo test get_exe_name --features in-use-encryption-unstable,gcp-kms -- --ignored + cp $(cat exe_name.txt) test-contents/test-exe + echo "Building test ... end" + + echo "Copying test contents ... begin" + tar czf test-contents.tgz test-contents + GCPKMS_SRC=test-contents.tgz GCPKMS_DST=$GCPKMS_INSTANCENAME: $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/copy-file.sh + echo "Copying test contents ... end" + + echo "Untarring test contents ... begin" + GCPKMS_CMD="tar xf test-contents.tgz" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh + echo "Untarring test contents ... end" + + "run gcp kms test": + - command: shell.exec + type: test + params: + shell: bash + working_dir: "src" + script: | + ${PREPARE_SHELL} + + set +o xtrace + export GCPKMS_GCLOUD=${GCPKMS_GCLOUD} + export GCPKMS_PROJECT=${GCPKMS_PROJECT} + export GCPKMS_ZONE=${GCPKMS_ZONE} + export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME} + set -o xtrace + + export GCPKMS_CMD="ON_DEMAND_GCP_CREDS_SHOULD_SUCCEED=1 \ + RUST_BACKTRACE=1 LD_LIBRARY_PATH=./test-contents/lib \ + ./test-contents/test-exe on_demand_gcp_credentials --nocapture" + $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh + + "compile only": - command: shell.exec type: test @@ -1274,6 +1329,11 @@ tasks: commands: - func: "run plain tests" + - name: "test-gcp-kms" + commands: + - func: "build and upload gcp kms test" + - func: "run gcp kms test" + - name: test-ocsp-rsa-valid-cert-server-staples tags: ["ocsp", "ocsp-rsa", "ocsp-staple"] commands: @@ -1796,6 +1856,11 @@ axes: variables: VENV_BIN_DIR: "Scripts" LIBMONGOCRYPT_OS: "windows-test" + - id: debian-11 + display_name: "Debian 11" + run_on: debian11-small + variables: + LIBMONGOCRYPT_OS: "debian11" - id: "versioned-api" display_name: "Versioned API" @@ -1926,6 +1991,49 @@ task_groups: tasks: - test-azure-kms + - name: testgcpkms_task_group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: "fetch source" + - func: "prepare resources" + - func: "windows fix" + - func: "fix absolute paths" + - func: "init test-results" + - func: "make files executable" + - func: "install rust" + - func: "install libmongocrypt" + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + set +o xtrace + echo '${testgcpkms_key_file}' > /tmp/testgcpkms_key_file.json + export GCPKMS_KEYFILE=/tmp/testgcpkms_key_file.json + export GCPKMS_DRIVERS_TOOLS=$DRIVERS_TOOLS + export GCPKMS_SERVICEACCOUNT="${testgcpkms_service_account}" + set -o xtrace + $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/create-and-setup-instance.sh + - command: expansions.update + params: + file: testgcpkms-expansions.yml + teardown_group: + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + set +o xtrace + export GCPKMS_GCLOUD=${GCPKMS_GCLOUD} + export GCPKMS_PROJECT=${GCPKMS_PROJECT} + export GCPKMS_ZONE=${GCPKMS_ZONE} + export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME} + set -o xtrace + $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/delete-instance.sh + tasks: + - test-gcp-kms + buildvariants: - matrix_name: "tests" @@ -2210,6 +2318,7 @@ buildvariants: # tasks: # # Windows MongoDB servers do not staple OCSP responses and only support RSA. # - name: ".ocsp-rsa !.ocsp-staple" + - matrix_name: "compile-only" matrix_spec: os: @@ -2232,6 +2341,24 @@ buildvariants: - ".6.0 .standalone" - ".5.0 .standalone" +- matrix_name: "azure-kms" + display_name: "Azure KMS" + matrix_spec: + os: + - ubuntu-20.04 + tasks: + - name: "azurekms_task_group" + batchtime: 20160 + +- matrix_name: "gcp-kms" + display_name: "GCP KMS" + matrix_spec: + os: + - debian-11 + tasks: + - name: testgcpkms_task_group + batchtime: 20160 + - name: "lint" display_name: "! Lint" run_on: @@ -2241,13 +2368,4 @@ buildvariants: - name: "check-rustfmt" - name: "check-rustdoc" - name: "check-manual" - - name: "check-cargo-deny" - -- matrix_name: "azure-kms" - display_name: "Azure KMS" - matrix_spec: - os: - - ubuntu-20.04 - tasks: - - name: "azurekms_task_group" - batchtime: 20160 \ No newline at end of file + - name: "check-cargo-deny" \ No newline at end of file diff --git a/.evergreen/env.sh b/.evergreen/env.sh index 9b2de44e9..37741b89e 100644 --- a/.evergreen/env.sh +++ b/.evergreen/env.sh @@ -22,4 +22,5 @@ else # Turn off tracing for the very-spammy nvm script. set +o xtrace [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" + set -o xtrace fi diff --git a/Cargo.toml b/Cargo.toml index e97439fad..cefff938b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,10 @@ aws-auth = ["reqwest"] # This can only be used with the tokio-runtime feature flag. azure-kms = ["reqwest"] +# Enable support for on-demand GCP KMS credentials. +# This can only be used with the tokio-runtime feature flag. +gcp-kms = ["reqwest"] + zstd-compression = ["zstd"] zlib-compression = ["flate2"] snappy-compression = ["snap"] diff --git a/src/client/csfle/state_machine.rs b/src/client/csfle/state_machine.rs index b0e1fd397..54eaa1557 100644 --- a/src/client/csfle/state_machine.rs +++ b/src/client/csfle/state_machine.rs @@ -214,7 +214,7 @@ impl CryptExecutor { State::NeedKmsCredentials => { let ctx = result_mut(&mut ctx)?; #[allow(unused_mut)] - let mut out = rawdoc! {}; + let mut kms_providers = rawdoc! {}; let credentials = self.kms_providers.credentials(); if credentials .get(&KmsProvider::Aws) @@ -234,7 +234,7 @@ impl CryptExecutor { if let Some(token) = aws_creds.session_token() { creds.append("sessionToken", token); } - out.append("aws", creds); + kms_providers.append("aws", creds); } #[cfg(not(feature = "aws-auth"))] { @@ -249,7 +249,7 @@ impl CryptExecutor { { #[cfg(feature = "azure-kms")] { - out.append("azure", self.azure.get_token().await?); + kms_providers.append("azure", self.azure.get_token().await?); } #[cfg(not(feature = "azure-kms"))] { @@ -258,7 +258,70 @@ impl CryptExecutor { )); } } - ctx.provide_kms_providers(&out)?; + if credentials + .get(&KmsProvider::Gcp) + .map_or(false, Document::is_empty) + { + #[cfg(feature = "gcp-kms")] + { + use crate::runtime::HttpClient; + use reqwest::Method; + use serde::Deserialize; + + #[derive(Deserialize)] + struct ResponseBody { + access_token: String, + } + + fn kms_error(error: String) -> Error { + let message = format!( + "An error occurred when obtaining GCP credentials: {}", + error + ); + let error = mongocrypt::error::Error { + kind: mongocrypt::error::ErrorKind::Kms, + message: Some(message), + code: None, + }; + error.into() + } + + let http_client = HttpClient::default(); + let host = std::env::var("GCE_METADATA_HOST") + .unwrap_or_else(|_| "metadata.google.internal".into()); + let uri = format!( + "http://{}/computeMetadata/v1/instance/service-accounts/default/token", + host + ); + let headers = vec![("Metadata-Flavor", "Google")]; + let response = http_client + .request(Method::GET, &uri, &headers) + .await + .map_err(|e| kms_error(e.to_string()))?; + + if response.status().as_u16() != 200 { + let error = match response.text().await { + Ok(text) => text, + Err(e) => format!("could not parse HTTP response: {}", e), + }; + return Err(kms_error(error)); + } + + let body: ResponseBody = response.json().await.map_err(|e| { + let error = format!("could not parse HTTP response: {}", e); + kms_error(error) + })?; + kms_providers + .append("gcp", rawdoc! { "accessToken": body.access_token }); + } + #[cfg(not(feature = "gcp-kms"))] + { + return Err(Error::invalid_argument( + "On-demand GCP KMS credentials require the `gcp-kms` feature.", + )); + } + } + ctx.provide_kms_providers(&kms_providers)?; } State::Ready => { let (tx, rx) = oneshot::channel(); diff --git a/src/runtime/http.rs b/src/runtime/http.rs index 912aa4bd1..2a773424c 100644 --- a/src/runtime/http.rs +++ b/src/runtime/http.rs @@ -1,3 +1,6 @@ +// Suppress noisy warnings when a method is not used under a certain feature flag. +#![allow(unused)] + use reqwest::{IntoUrl, Method, Response}; use serde::Deserialize; @@ -36,7 +39,6 @@ impl HttpClient { } /// Executes an HTTP GET request and returns the response body as a string. - #[allow(unused)] pub(crate) async fn get_and_read_string<'a>( &self, uri: &str, @@ -47,7 +49,6 @@ impl HttpClient { } /// Executes an HTTP PUT request and returns the response body as a string. - #[allow(unused)] pub(crate) async fn put_and_read_string<'a>( &self, uri: &str, @@ -58,7 +59,6 @@ impl HttpClient { } /// Executes an HTTP request and returns the response body as a string. - #[allow(unused)] pub(crate) async fn request_and_read_string<'a>( &self, method: Method, diff --git a/src/test/atlas_planned_maintenance_testing/mod.rs b/src/test/atlas_planned_maintenance_testing/mod.rs index 7172fe4b7..c5c6bc17b 100644 --- a/src/test/atlas_planned_maintenance_testing/mod.rs +++ b/src/test/atlas_planned_maintenance_testing/mod.rs @@ -29,15 +29,8 @@ use json_models::{Events, Results}; use super::spec::unified_runner::EntityMap; #[test] +#[ignore] fn get_exe_name() { - if env::var("ATLAS_PLANNED_MAINTENANCE_TESTING").is_err() { - // This test should only be run from the workload-executor script. - log_uncaptured( - "Skipping get_exe_name due to being run outside of planned maintenance testing", - ); - return; - } - let mut file = File::create("exe_name.txt").expect("Failed to create file"); let exe_name = env::current_exe() .expect("Failed to determine name of test executable") diff --git a/src/test/csfle.rs b/src/test/csfle.rs index 468dc1abc..3e772bcdb 100644 --- a/src/test/csfle.rs +++ b/src/test/csfle.rs @@ -2856,7 +2856,47 @@ async fn on_demand_aws_success() -> Result<()> { // TODO RUST-1441: implement prose test 16. Rewrap -// TODO RUST-1417: implement prose test 17. On-demand GCP Credentials +// Prose test 17. On-demand GCP Credentials +#[cfg(feature = "gcp-kms")] +#[cfg_attr(feature = "tokio-runtime", tokio::test)] +#[cfg_attr(feature = "async-std-runtime", async_std::test)] +async fn on_demand_gcp_credentials() -> Result<()> { + let _guard = LOCK.run_exclusively().await; + + let util_client = TestClient::new().await.into_client(); + let client_encryption = ClientEncryption::new( + util_client, + KV_NAMESPACE.clone(), + [(KmsProvider::Gcp, doc! {}, None)], + )?; + + let result = client_encryption + .create_data_key(MasterKey::Gcp { + project_id: "devprod-drivers".into(), + location: "global".into(), + key_ring: "key-ring-csfle".into(), + key_name: "key-name-csfle".into(), + key_version: None, + endpoint: None, + }) + .run() + .await; + + if std::env::var("ON_DEMAND_GCP_CREDS_SHOULD_SUCCEED").is_ok() { + result.unwrap(); + } else { + let error = result.unwrap_err(); + match *error.kind { + ErrorKind::Encryption(e) => { + assert!(matches!(e.kind, mongocrypt::error::ErrorKind::Kms)); + assert!(e.message.unwrap().contains("GCP credentials")); + } + other => panic!("Expected encryption error, got {:?}", other), + } + } + + Ok(()) +} // Prose test 18. Azure IMDS Credentials #[cfg(feature = "azure-kms")] From 1557eff281d10e0ea55f573a3721cc6e152f69a3 Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Tue, 16 May 2023 15:34:23 -0600 Subject: [PATCH 2/2] fix indent --- src/client/csfle/state_machine.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/csfle/state_machine.rs b/src/client/csfle/state_machine.rs index 54eaa1557..0d6e9023f 100644 --- a/src/client/csfle/state_machine.rs +++ b/src/client/csfle/state_machine.rs @@ -290,9 +290,9 @@ impl CryptExecutor { let host = std::env::var("GCE_METADATA_HOST") .unwrap_or_else(|_| "metadata.google.internal".into()); let uri = format!( - "http://{}/computeMetadata/v1/instance/service-accounts/default/token", - host - ); + "http://{}/computeMetadata/v1/instance/service-accounts/default/token", + host + ); let headers = vec![("Metadata-Flavor", "Google")]; let response = http_client .request(Method::GET, &uri, &headers)