-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(object_store): add support for server-side encryption with customer-provided keys (SSE-C) #6230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(object_store): add support for server-side encryption with customer-provided keys (SSE-C) #6230
Changes from all commits
f30b0e0
4b852e1
a45e559
52fbdeb
8137c31
839ef03
6e6f354
d3747fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,6 +101,54 @@ export AWS_SERVER_SIDE_ENCRYPTION=aws:kms:dsse | |
| cargo test --features aws | ||
| ``` | ||
|
|
||
| #### SSE-C Encryption tests | ||
|
|
||
| Unfortunately, localstack does not support SSE-C encryption (https://github.com/localstack/localstack/issues/11356). | ||
|
|
||
| We will use [MinIO](https://min.io/docs/minio/container/operations/server-side-encryption.html) to test SSE-C encryption. | ||
|
|
||
| First, create a self-signed certificate to enable HTTPS for MinIO, as SSE-C requires HTTPS. | ||
|
|
||
| ```shell | ||
| mkdir ~/certs | ||
| cd ~/certs | ||
| openssl genpkey -algorithm RSA -out private.key | ||
| openssl req -new -key private.key -out request.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=example.com/[email protected]" | ||
| openssl x509 -req -days 365 -in request.csr -signkey private.key -out public.crt | ||
| rm request.csr | ||
| ``` | ||
|
|
||
| Second, start MinIO with the self-signed certificate. | ||
|
|
||
| ```shell | ||
| docker run -d \ | ||
| -p 9000:9000 \ | ||
| --name minio \ | ||
| -v ${HOME}/certs:/root/.minio/certs \ | ||
| -e "MINIO_ROOT_USER=minio" \ | ||
| -e "MINIO_ROOT_PASSWORD=minio123" \ | ||
| minio/minio server /data | ||
| ``` | ||
|
|
||
| Create a test bucket. | ||
|
|
||
| ```shell | ||
| export AWS_BUCKET_NAME=test-bucket | ||
| export AWS_ACCESS_KEY_ID=minio | ||
| export AWS_SECRET_ACCESS_KEY=minio123 | ||
| export AWS_ENDPOINT=https://localhost:9000 | ||
| aws s3 mb s3://test-bucket --endpoint-url=https://localhost:9000 --no-verify-ssl | ||
| ``` | ||
|
|
||
| Run the tests. The real test is `test_s3_ssec_encryption_with_minio()` | ||
|
|
||
| ```shell | ||
| export TEST_S3_SSEC_ENCRYPTION=1 | ||
| cargo test --features aws --package object_store --lib aws::tests::test_s3_ssec_encryption_with_minio -- --exact --nocapture | ||
| ``` | ||
|
|
||
|
|
||
|
|
||
| ### Azure | ||
|
|
||
| To test the Azure integration | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -26,7 +26,10 @@ use crate::aws::{ | |||||
| use crate::client::TokenCredentialProvider; | ||||||
| use crate::config::ConfigValue; | ||||||
| use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider}; | ||||||
| use base64::prelude::BASE64_STANDARD; | ||||||
| use base64::Engine; | ||||||
| use itertools::Itertools; | ||||||
| use md5::{Digest, Md5}; | ||||||
| use reqwest::header::{HeaderMap, HeaderValue}; | ||||||
| use serde::{Deserialize, Serialize}; | ||||||
| use snafu::{OptionExt, ResultExt, Snafu}; | ||||||
|
|
@@ -73,7 +76,7 @@ enum Error { | |||||
| #[snafu(display("Invalid Zone suffix for bucket '{bucket}'"))] | ||||||
| ZoneSuffix { bucket: String }, | ||||||
|
|
||||||
| #[snafu(display("Invalid encryption type: {}. Valid values are \"AES256\", \"sse:kms\", and \"sse:kms:dsse\".", passed))] | ||||||
| #[snafu(display("Invalid encryption type: {}. Valid values are \"AES256\", \"sse:kms\", \"sse:kms:dsse\" and \"sse-c\".", passed))] | ||||||
| InvalidEncryptionType { passed: String }, | ||||||
|
|
||||||
| #[snafu(display( | ||||||
|
|
@@ -166,6 +169,8 @@ pub struct AmazonS3Builder { | |||||
| encryption_type: Option<ConfigValue<S3EncryptionType>>, | ||||||
| encryption_kms_key_id: Option<String>, | ||||||
| encryption_bucket_key_enabled: Option<ConfigValue<bool>>, | ||||||
| /// base64-encoded 256-bit customer encryption key for SSE-C. | ||||||
| encryption_customer_key_base64: Option<String>, | ||||||
| } | ||||||
|
|
||||||
| /// Configuration keys for [`AmazonS3Builder`] | ||||||
|
|
@@ -394,6 +399,9 @@ impl FromStr for AmazonS3ConfigKey { | |||||
| "aws_sse_bucket_key_enabled" => { | ||||||
| Ok(Self::Encryption(S3EncryptionConfigKey::BucketKeyEnabled)) | ||||||
| } | ||||||
| "aws_sse_customer_key_base64" => Ok(Self::Encryption( | ||||||
| S3EncryptionConfigKey::CustomerEncryptionKey, | ||||||
| )), | ||||||
| _ => match s.parse() { | ||||||
| Ok(key) => Ok(Self::Client(key)), | ||||||
| Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()), | ||||||
|
|
@@ -511,6 +519,9 @@ impl AmazonS3Builder { | |||||
| S3EncryptionConfigKey::BucketKeyEnabled => { | ||||||
| self.encryption_bucket_key_enabled = Some(ConfigValue::Deferred(value.into())) | ||||||
| } | ||||||
| S3EncryptionConfigKey::CustomerEncryptionKey => { | ||||||
| self.encryption_customer_key_base64 = Some(value.into()) | ||||||
| } | ||||||
| }, | ||||||
| }; | ||||||
| self | ||||||
|
|
@@ -566,6 +577,9 @@ impl AmazonS3Builder { | |||||
| .encryption_bucket_key_enabled | ||||||
| .as_ref() | ||||||
| .map(ToString::to_string), | ||||||
| S3EncryptionConfigKey::CustomerEncryptionKey => { | ||||||
| self.encryption_customer_key_base64.clone() | ||||||
| } | ||||||
| }, | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -813,6 +827,14 @@ impl AmazonS3Builder { | |||||
| self | ||||||
| } | ||||||
|
|
||||||
| /// Use SSE-C for server side encryption. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably say whether it should already be base64 encoded.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment |
||||||
| /// Must pass the *base64-encoded* 256-bit customer encryption key. | ||||||
| pub fn with_ssec_encryption(mut self, customer_key_base64: impl Into<String>) -> Self { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused, should this parameter already be base64 encoded? I ask because internally it looks like you are encoding it?
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. My intention is to make this API takes an already base64 encoded key. The reason is because I checked the AWS SDK examples in
So now I removed the base64 encoding inside this method, and changed the test to encode first. |
||||||
| self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseC)); | ||||||
| self.encryption_customer_key_base64 = customer_key_base64.into().into(); | ||||||
| self | ||||||
| } | ||||||
|
|
||||||
| /// Set whether to enable bucket key for server side encryption. This overrides | ||||||
| /// the bucket default setting for bucket keys. | ||||||
| /// | ||||||
|
|
@@ -953,6 +975,7 @@ impl AmazonS3Builder { | |||||
| self.encryption_bucket_key_enabled | ||||||
| .map(|val| val.get()) | ||||||
| .transpose()?, | ||||||
| self.encryption_customer_key_base64, | ||||||
| )? | ||||||
| } else { | ||||||
| S3EncryptionHeaders::default() | ||||||
|
|
@@ -994,22 +1017,25 @@ fn parse_bucket_az(bucket: &str) -> Option<&str> { | |||||
| /// These options are used to configure server-side encryption for S3 objects. | ||||||
| /// To configure them, pass them to [`AmazonS3Builder::with_config`]. | ||||||
| /// | ||||||
| /// Both [SSE-KMS] and [DSSE-KMS] are supported. [SSE-C] is not yet supported. | ||||||
| /// | ||||||
| /// [SSE-S3]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html | ||||||
| /// [SSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html | ||||||
| /// [DSSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingDSSEncryption.html | ||||||
| /// [SSE-C]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html | ||||||
| #[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)] | ||||||
| #[non_exhaustive] | ||||||
| pub enum S3EncryptionConfigKey { | ||||||
| /// Type of encryption to use. If set, must be one of "AES256", "aws:kms", or "aws:kms:dsse". | ||||||
| /// Type of encryption to use. If set, must be one of "AES256" (SSE-S3), "aws:kms" (SSE-KMS), "aws:kms:dsse" (DSSE-KMS) or "sse-c". | ||||||
| ServerSideEncryption, | ||||||
| /// The KMS key ID to use for server-side encryption. If set, ServerSideEncryption | ||||||
| /// must be "aws:kms" or "aws:kms:dsse". | ||||||
| KmsKeyId, | ||||||
| /// If set to true, will use the bucket's default KMS key for server-side encryption. | ||||||
| /// If set to false, will disable the use of the bucket's default KMS key for server-side encryption. | ||||||
| BucketKeyEnabled, | ||||||
|
|
||||||
| /// The base64 encoded, 256-bit customer encryption key to use for server-side encryption. | ||||||
| /// If set, ServerSideEncryption must be "sse-c". | ||||||
| CustomerEncryptionKey, | ||||||
| } | ||||||
|
|
||||||
| impl AsRef<str> for S3EncryptionConfigKey { | ||||||
|
|
@@ -1018,6 +1044,7 @@ impl AsRef<str> for S3EncryptionConfigKey { | |||||
| Self::ServerSideEncryption => "aws_server_side_encryption", | ||||||
| Self::KmsKeyId => "aws_sse_kms_key_id", | ||||||
| Self::BucketKeyEnabled => "aws_sse_bucket_key_enabled", | ||||||
| Self::CustomerEncryptionKey => "aws_sse_customer_key_base64", | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -1027,6 +1054,7 @@ enum S3EncryptionType { | |||||
| S3, | ||||||
| SseKms, | ||||||
| DsseKms, | ||||||
| SseC, | ||||||
| } | ||||||
|
|
||||||
| impl crate::config::Parse for S3EncryptionType { | ||||||
|
|
@@ -1035,6 +1063,7 @@ impl crate::config::Parse for S3EncryptionType { | |||||
| "AES256" => Ok(Self::S3), | ||||||
| "aws:kms" => Ok(Self::SseKms), | ||||||
| "aws:kms:dsse" => Ok(Self::DsseKms), | ||||||
| "sse-c" => Ok(Self::SseC), | ||||||
| _ => Err(Error::InvalidEncryptionType { passed: s.into() }.into()), | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -1046,6 +1075,7 @@ impl From<&S3EncryptionType> for &'static str { | |||||
| S3EncryptionType::S3 => "AES256", | ||||||
| S3EncryptionType::SseKms => "aws:kms", | ||||||
| S3EncryptionType::DsseKms => "aws:kms:dsse", | ||||||
| S3EncryptionType::SseC => "sse-c", | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -1062,37 +1092,87 @@ impl std::fmt::Display for S3EncryptionType { | |||||
| /// Whether these headers are sent depends on both the kind of encryption set | ||||||
| /// and the kind of request being made. | ||||||
| #[derive(Default, Clone, Debug)] | ||||||
| pub struct S3EncryptionHeaders(HeaderMap); | ||||||
| pub(super) struct S3EncryptionHeaders(pub HeaderMap); | ||||||
|
|
||||||
| impl S3EncryptionHeaders { | ||||||
| fn try_new( | ||||||
| encryption_type: &S3EncryptionType, | ||||||
| key_id: Option<String>, | ||||||
| encryption_kms_key_id: Option<String>, | ||||||
| bucket_key_enabled: Option<bool>, | ||||||
| encryption_customer_key_base64: Option<String>, | ||||||
| ) -> Result<Self> { | ||||||
| let mut headers = HeaderMap::new(); | ||||||
| // Note: if we later add support for SSE-C, we should be sure to use | ||||||
| // HeaderValue::set_sensitive to prevent the key from being logged. | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption", | ||||||
| HeaderValue::from_static(encryption_type.into()), | ||||||
| ); | ||||||
| if let Some(key_id) = key_id { | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption-aws-kms-key-id", | ||||||
| key_id | ||||||
| .try_into() | ||||||
| .map_err(|err| Error::InvalidEncryptionHeader { | ||||||
| header: "kms-key-id", | ||||||
| source: Box::new(err), | ||||||
| })?, | ||||||
| ); | ||||||
| } | ||||||
| if let Some(bucket_key_enabled) = bucket_key_enabled { | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption-bucket-key-enabled", | ||||||
| HeaderValue::from_static(if bucket_key_enabled { "true" } else { "false" }), | ||||||
| ); | ||||||
| match encryption_type { | ||||||
| S3EncryptionType::S3 | S3EncryptionType::SseKms | S3EncryptionType::DsseKms => { | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption", | ||||||
| HeaderValue::from_static(encryption_type.into()), | ||||||
| ); | ||||||
| if let Some(key_id) = encryption_kms_key_id { | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption-aws-kms-key-id", | ||||||
| key_id | ||||||
| .try_into() | ||||||
| .map_err(|err| Error::InvalidEncryptionHeader { | ||||||
| header: "kms-key-id", | ||||||
| source: Box::new(err), | ||||||
| })?, | ||||||
| ); | ||||||
| } | ||||||
| if let Some(bucket_key_enabled) = bucket_key_enabled { | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption-bucket-key-enabled", | ||||||
| HeaderValue::from_static(if bucket_key_enabled { "true" } else { "false" }), | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
| S3EncryptionType::SseC => { | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption-customer-algorithm", | ||||||
| HeaderValue::from_static("AES256"), | ||||||
| ); | ||||||
| if let Some(key) = encryption_customer_key_base64 { | ||||||
| let mut header_value: HeaderValue = | ||||||
| key.clone() | ||||||
| .try_into() | ||||||
| .map_err(|err| Error::InvalidEncryptionHeader { | ||||||
| header: "x-amz-server-side-encryption-customer-key", | ||||||
| source: Box::new(err), | ||||||
| })?; | ||||||
| header_value.set_sensitive(true); | ||||||
| headers.insert("x-amz-server-side-encryption-customer-key", header_value); | ||||||
|
|
||||||
| let decoded_key = BASE64_STANDARD.decode(key.as_bytes()).map_err(|err| { | ||||||
| Error::InvalidEncryptionHeader { | ||||||
| header: "x-amz-server-side-encryption-customer-key", | ||||||
| source: Box::new(err), | ||||||
| } | ||||||
| })?; | ||||||
| let mut hasher = Md5::new(); | ||||||
| hasher.update(decoded_key); | ||||||
| let md5 = BASE64_STANDARD.encode(hasher.finalize()); | ||||||
| let mut md5_header_value: HeaderValue = | ||||||
| md5.try_into() | ||||||
| .map_err(|err| Error::InvalidEncryptionHeader { | ||||||
| header: "x-amz-server-side-encryption-customer-key-MD5", | ||||||
| source: Box::new(err), | ||||||
| })?; | ||||||
| md5_header_value.set_sensitive(true); | ||||||
| headers.insert( | ||||||
| "x-amz-server-side-encryption-customer-key-MD5", | ||||||
| md5_header_value, | ||||||
| ); | ||||||
| } else { | ||||||
| return Err(Error::InvalidEncryptionHeader { | ||||||
| header: "x-amz-server-side-encryption-customer-key", | ||||||
| source: Box::new(std::io::Error::new( | ||||||
| std::io::ErrorKind::InvalidInput, | ||||||
| "Missing customer key", | ||||||
| )), | ||||||
| } | ||||||
| .into()); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Ok(Self(headers)) | ||||||
| } | ||||||
|
|
@@ -1162,7 +1242,11 @@ mod tests { | |||||
| .with_config(AmazonS3ConfigKey::UnsignedPayload, "true") | ||||||
| .with_config("aws_server_side_encryption".parse().unwrap(), "AES256") | ||||||
| .with_config("aws_sse_kms_key_id".parse().unwrap(), "some_key_id") | ||||||
| .with_config("aws_sse_bucket_key_enabled".parse().unwrap(), "true"); | ||||||
| .with_config("aws_sse_bucket_key_enabled".parse().unwrap(), "true") | ||||||
| .with_config( | ||||||
| "aws_sse_customer_key_base64".parse().unwrap(), | ||||||
| "some_customer_key", | ||||||
| ); | ||||||
|
|
||||||
| assert_eq!( | ||||||
| builder | ||||||
|
|
@@ -1216,6 +1300,12 @@ mod tests { | |||||
| .unwrap(), | ||||||
| "true" | ||||||
| ); | ||||||
| assert_eq!( | ||||||
| builder | ||||||
| .get_config_value(&"aws_sse_customer_key_base64".parse().unwrap()) | ||||||
| .unwrap(), | ||||||
| "some_customer_key" | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| #[test] | ||||||
|
|
||||||

Uh oh!
There was an error while loading. Please reload this page.