diff --git a/Cargo.lock b/Cargo.lock index 26d6eadba..21d7d5b75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5690,12 +5690,14 @@ name = "storage" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "base64 0.21.7", "log", "rstest", "secret", "serde", "serde_json", + "strum", "tempfile", "thiserror", "tokio", diff --git a/confidential-data-hub/docs/SECURE_STORAGE.md b/confidential-data-hub/docs/SECURE_STORAGE.md index a7c5a0891..87bf49384 100644 --- a/confidential-data-hub/docs/SECURE_STORAGE.md +++ b/confidential-data-hub/docs/SECURE_STORAGE.md @@ -17,7 +17,7 @@ We reuse [direct block device assigned volume feature](https://github.com/kata-c [Aliyun OSS](https://www.alibabacloud.com/product/object-storage-service) is an object storage service provided by Alibaba Cloud (Aliyun). -The [plugin](../storage/src/volume_type/alibaba_cloud_oss/) provides two different modes for secure mount. +The [plugin](../storage/src/volume_type/aliyun) provides two different modes for secure mount. Confidential Data Hub's `secure_mount()` [API](../hub/protos/api.proto) will help to instrument this. diff --git a/confidential-data-hub/docs/use-cases/secure-mount-with-aliyun-oss.md b/confidential-data-hub/docs/use-cases/secure-mount-with-aliyun-oss.md index 19e984b52..739f8fb08 100644 --- a/confidential-data-hub/docs/use-cases/secure-mount-with-aliyun-oss.md +++ b/confidential-data-hub/docs/use-cases/secure-mount-with-aliyun-oss.md @@ -66,39 +66,39 @@ Run CDH confidential-data-hub ``` -Prepare a request JSON `storage.json` +Prepare a request JSON `storage.json`. This example is for aliyun OSS. ```json { - "driver": "", - "driver_options": [ - "alibaba-cloud-oss={\"akId\":\"XXX\",\"akSecret\":\"XXX\",\"annotations\":\"\",\"bucket\":\"\",\"encrypted\":\"gocryptfs\",\"encPasswd\":\"\",\"kmsKeyId\":\"\",\"otherOpts\":\"-o max_stat_cache_size=0 -o allow_other\",\"path\":\"\",\"readonly\":\"\",\"targetPath\":\"/mnt/aliyun-oss\",\"url\":\"https://oss-cn-beijing.aliyuncs.com\",\"volumeId\":\"\"}" - ], - "source": "", - "fstype": "", - "options": [], + "volume_type": "alibaba-cloud-oss", + "options": { + "akId": "XXX", + "akSecret": "XXX", + "annotations": "", + "bucket": "", + "encrypted": "gocryptfs", + "encPasswd": "", + "kmsKeyId": "", + "otherOpts": "-o max_stat_cache_size=0 -o allow_other", + "path": "", + "readonly": "", + "targetPath": "/mnt/aliyun-oss", + "url": "https://oss-cn-beijing.aliyuncs.com", + "volumeId": "" + }, + "flags": [], "mount_point": "/mnt/target-path" } ``` -- `mount_point`: the target path to mount the decrypted storage. - -The only string member of `driver_options` looks like `alibaba-cloud-oss=XXX`. `XXX` here is an escaped JSON object. -```json -{ - "akId": "XXX", - "akSecret": "XXX", - "annotations": "", - "bucket": "", - "encrypted": "gocryptfs", - "encPasswd": "", - "kmsKeyId": "", - "otherOpts": "-o max_stat_cache_size=0 -o allow_other", - "path": "/", - "readonly": "", - "targetPath": "/mnt/aliyun-oss", - "url": "https://oss-cn-beijing.aliyuncs.com", - "volumeId": "" -} -``` +Fields: +- `volume_type`: the secure mount plugin type name. It will determine how the rest of the fields are used. +- `options`: a key-value map that specifies the settings for the mount operation. Different plugin can define +different keys of the `option`. In this example all keys are for Aliyun OSS. +- `flags`: a string list that specifies the settings for the mount operation. Different plugin can define different +usage of this field. +- `mount_point`: The target mount path of the operation. + +Let's dive into the example's `options` field. Note that this example is for Aliyun OSS and different mount plugin +can define its own `options`! The fields here - `akId`: is Id of AK to access the OSS bucket. This will be provided when creating the OSS bucket. This can also be a [sealed secret](../SEALED_SECRET.md). diff --git a/confidential-data-hub/hub/protos/api.proto b/confidential-data-hub/hub/protos/api.proto index 97a98660b..f20bf5c1b 100644 --- a/confidential-data-hub/hub/protos/api.proto +++ b/confidential-data-hub/hub/protos/api.proto @@ -19,12 +19,10 @@ message GetResourceResponse { } message SecureMountRequest { - string driver = 1; - repeated string driver_options = 2; - string source = 3; - string fstype = 4; - repeated string options = 5; - string mount_point = 6; + string volume_type = 1; + map options = 2; + repeated string flags = 3; + string mount_point = 4; } message SecureMountResponse { diff --git a/confidential-data-hub/hub/src/bin/cdh-tool.rs b/confidential-data-hub/hub/src/bin/cdh-tool.rs index ce8ec95de..f136b58e0 100644 --- a/confidential-data-hub/hub/src/bin/cdh-tool.rs +++ b/confidential-data-hub/hub/src/bin/cdh-tool.rs @@ -142,10 +142,8 @@ async fn main() { let storage: Storage = serde_json::from_slice(&storage_manifest).expect("deserialize Storage"); let req = SecureMountRequest { - driver: storage.driver, - driver_options: storage.driver_options, - source: storage.source, - fstype: storage.fstype, + volume_type: storage.volume_type, + flags: storage.flags, options: storage.options, mount_point: storage.mount_point, ..Default::default() diff --git a/confidential-data-hub/hub/src/bin/protos/api.rs b/confidential-data-hub/hub/src/bin/protos/api.rs index 2ff08b405..65e6e1cd7 100644 --- a/confidential-data-hub/hub/src/bin/protos/api.rs +++ b/confidential-data-hub/hub/src/bin/protos/api.rs @@ -517,16 +517,12 @@ impl ::protobuf::reflect::ProtobufValue for GetResourceResponse { #[derive(PartialEq,Clone,Default,Debug)] pub struct SecureMountRequest { // message fields - // @@protoc_insertion_point(field:api.SecureMountRequest.driver) - pub driver: ::std::string::String, - // @@protoc_insertion_point(field:api.SecureMountRequest.driver_options) - pub driver_options: ::std::vec::Vec<::std::string::String>, - // @@protoc_insertion_point(field:api.SecureMountRequest.source) - pub source: ::std::string::String, - // @@protoc_insertion_point(field:api.SecureMountRequest.fstype) - pub fstype: ::std::string::String, + // @@protoc_insertion_point(field:api.SecureMountRequest.volume_type) + pub volume_type: ::std::string::String, // @@protoc_insertion_point(field:api.SecureMountRequest.options) - pub options: ::std::vec::Vec<::std::string::String>, + pub options: ::std::collections::HashMap<::std::string::String, ::std::string::String>, + // @@protoc_insertion_point(field:api.SecureMountRequest.flags) + pub flags: ::std::vec::Vec<::std::string::String>, // @@protoc_insertion_point(field:api.SecureMountRequest.mount_point) pub mount_point: ::std::string::String, // special fields @@ -546,33 +542,23 @@ impl SecureMountRequest { } fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(6); + let mut fields = ::std::vec::Vec::with_capacity(4); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "driver", - |m: &SecureMountRequest| { &m.driver }, - |m: &mut SecureMountRequest| { &mut m.driver }, + "volume_type", + |m: &SecureMountRequest| { &m.volume_type }, + |m: &mut SecureMountRequest| { &mut m.volume_type }, )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "driver_options", - |m: &SecureMountRequest| { &m.driver_options }, - |m: &mut SecureMountRequest| { &mut m.driver_options }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "source", - |m: &SecureMountRequest| { &m.source }, - |m: &mut SecureMountRequest| { &mut m.source }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "fstype", - |m: &SecureMountRequest| { &m.fstype }, - |m: &mut SecureMountRequest| { &mut m.fstype }, - )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( + fields.push(::protobuf::reflect::rt::v2::make_map_simpler_accessor::<_, _, _>( "options", |m: &SecureMountRequest| { &m.options }, |m: &mut SecureMountRequest| { &mut m.options }, )); + fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( + "flags", + |m: &SecureMountRequest| { &m.flags }, + |m: &mut SecureMountRequest| { &mut m.flags }, + )); fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( "mount_point", |m: &SecureMountRequest| { &m.mount_point }, @@ -597,21 +583,27 @@ impl ::protobuf::Message for SecureMountRequest { while let Some(tag) = is.read_raw_tag_or_eof()? { match tag { 10 => { - self.driver = is.read_string()?; + self.volume_type = is.read_string()?; }, 18 => { - self.driver_options.push(is.read_string()?); + let len = is.read_raw_varint32()?; + let old_limit = is.push_limit(len as u64)?; + let mut key = ::std::default::Default::default(); + let mut value = ::std::default::Default::default(); + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => key = is.read_string()?, + 18 => value = is.read_string()?, + _ => ::protobuf::rt::skip_field_for_tag(tag, is)?, + }; + } + is.pop_limit(old_limit); + self.options.insert(key, value); }, 26 => { - self.source = is.read_string()?; + self.flags.push(is.read_string()?); }, 34 => { - self.fstype = is.read_string()?; - }, - 42 => { - self.options.push(is.read_string()?); - }, - 50 => { self.mount_point = is.read_string()?; }, tag => { @@ -626,23 +618,20 @@ impl ::protobuf::Message for SecureMountRequest { #[allow(unused_variables)] fn compute_size(&self) -> u64 { let mut my_size = 0; - if !self.driver.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.driver); + if !self.volume_type.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.volume_type); } - for value in &self.driver_options { - my_size += ::protobuf::rt::string_size(2, &value); + for (k, v) in &self.options { + let mut entry_size = 0; + entry_size += ::protobuf::rt::string_size(1, &k); + entry_size += ::protobuf::rt::string_size(2, &v); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(entry_size) + entry_size }; - if !self.source.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.source); - } - if !self.fstype.is_empty() { - my_size += ::protobuf::rt::string_size(4, &self.fstype); - } - for value in &self.options { - my_size += ::protobuf::rt::string_size(5, &value); + for value in &self.flags { + my_size += ::protobuf::rt::string_size(3, &value); }; if !self.mount_point.is_empty() { - my_size += ::protobuf::rt::string_size(6, &self.mount_point); + my_size += ::protobuf::rt::string_size(4, &self.mount_point); } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); @@ -650,23 +639,23 @@ impl ::protobuf::Message for SecureMountRequest { } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.driver.is_empty() { - os.write_string(1, &self.driver)?; + if !self.volume_type.is_empty() { + os.write_string(1, &self.volume_type)?; } - for v in &self.driver_options { + for (k, v) in &self.options { + let mut entry_size = 0; + entry_size += ::protobuf::rt::string_size(1, &k); + entry_size += ::protobuf::rt::string_size(2, &v); + os.write_raw_varint32(18)?; // Tag. + os.write_raw_varint32(entry_size as u32)?; + os.write_string(1, &k)?; os.write_string(2, &v)?; }; - if !self.source.is_empty() { - os.write_string(3, &self.source)?; - } - if !self.fstype.is_empty() { - os.write_string(4, &self.fstype)?; - } - for v in &self.options { - os.write_string(5, &v)?; + for v in &self.flags { + os.write_string(3, &v)?; }; if !self.mount_point.is_empty() { - os.write_string(6, &self.mount_point)?; + os.write_string(4, &self.mount_point)?; } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) @@ -685,26 +674,16 @@ impl ::protobuf::Message for SecureMountRequest { } fn clear(&mut self) { - self.driver.clear(); - self.driver_options.clear(); - self.source.clear(); - self.fstype.clear(); + self.volume_type.clear(); self.options.clear(); + self.flags.clear(); self.mount_point.clear(); self.special_fields.clear(); } fn default_instance() -> &'static SecureMountRequest { - static instance: SecureMountRequest = SecureMountRequest { - driver: ::std::string::String::new(), - driver_options: ::std::vec::Vec::new(), - source: ::std::string::String::new(), - fstype: ::std::string::String::new(), - options: ::std::vec::Vec::new(), - mount_point: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance + static instance: ::protobuf::rt::Lazy = ::protobuf::rt::Lazy::new(); + instance.get(SecureMountRequest::new) } } @@ -853,17 +832,18 @@ static file_descriptor_proto_data: &'static [u8] = b"\ laintext\x18\x01\x20\x01(\x0cR\tplaintext\"8\n\x12GetResourceRequest\x12\ \"\n\x0cResourcePath\x18\x01\x20\x01(\tR\x0cResourcePath\"1\n\x13GetReso\ urceResponse\x12\x1a\n\x08Resource\x18\x01\x20\x01(\x0cR\x08Resource\"\ - \xbe\x01\n\x12SecureMountRequest\x12\x16\n\x06driver\x18\x01\x20\x01(\tR\ - \x06driver\x12%\n\x0edriver_options\x18\x02\x20\x03(\tR\rdriverOptions\ - \x12\x16\n\x06source\x18\x03\x20\x01(\tR\x06source\x12\x16\n\x06fstype\ - \x18\x04\x20\x01(\tR\x06fstype\x12\x18\n\x07options\x18\x05\x20\x03(\tR\ - \x07options\x12\x1f\n\x0bmount_point\x18\x06\x20\x01(\tR\nmountPoint\"4\ - \n\x13SecureMountResponse\x12\x1d\n\nmount_path\x18\x01\x20\x01(\tR\tmou\ - ntPath2V\n\x13SealedSecretService\x12?\n\x0cUnsealSecret\x12\x16.api.Uns\ - ealSecretInput\x1a\x17.api.UnsealSecretOutput2V\n\x12GetResourceService\ - \x12@\n\x0bGetResource\x12\x17.api.GetResourceRequest\x1a\x18.api.GetRes\ - ourceResponse2V\n\x12SecureMountService\x12@\n\x0bSecureMount\x12\x17.ap\ - i.SecureMountRequest\x1a\x18.api.SecureMountResponseb\x06proto3\ + \xe8\x01\n\x12SecureMountRequest\x12\x1f\n\x0bvolume_type\x18\x01\x20\ + \x01(\tR\nvolumeType\x12>\n\x07options\x18\x02\x20\x03(\x0b2$.api.Secure\ + MountRequest.OptionsEntryR\x07options\x12\x14\n\x05flags\x18\x03\x20\x03\ + (\tR\x05flags\x12\x1f\n\x0bmount_point\x18\x04\x20\x01(\tR\nmountPoint\ + \x1a:\n\x0cOptionsEntry\x12\x10\n\x03key\x18\x01\x20\x01(\tR\x03key\x12\ + \x14\n\x05value\x18\x02\x20\x01(\tR\x05value:\x028\x01\"4\n\x13SecureMou\ + ntResponse\x12\x1d\n\nmount_path\x18\x01\x20\x01(\tR\tmountPath2V\n\x13S\ + ealedSecretService\x12?\n\x0cUnsealSecret\x12\x16.api.UnsealSecretInput\ + \x1a\x17.api.UnsealSecretOutput2V\n\x12GetResourceService\x12@\n\x0bGetR\ + esource\x12\x17.api.GetResourceRequest\x1a\x18.api.GetResourceResponse2V\ + \n\x12SecureMountService\x12@\n\x0bSecureMount\x12\x17.api.SecureMountRe\ + quest\x1a\x18.api.SecureMountResponseb\x06proto3\ "; /// `FileDescriptorProto` object which was a source for this generated file diff --git a/confidential-data-hub/hub/src/bin/server/mod.rs b/confidential-data-hub/hub/src/bin/server/mod.rs index 1e6de9c01..d5846162c 100644 --- a/confidential-data-hub/hub/src/bin/server/mod.rs +++ b/confidential-data-hub/hub/src/bin/server/mod.rs @@ -166,11 +166,9 @@ impl SecureMountService for Server { let reader = HUB.read().await; let reader = reader.as_ref().expect("must be initialized"); let storage = Storage { - driver: req.driver, - driver_options: req.driver_options, - source: req.source, - fstype: req.fstype, + volume_type: req.volume_type, options: req.options, + flags: req.flags, mount_point: req.mount_point, }; let resource = reader.secure_mount(storage).await.map_err(|e| { diff --git a/confidential-data-hub/storage/Cargo.toml b/confidential-data-hub/storage/Cargo.toml index 136bad83f..df85b13f9 100644 --- a/confidential-data-hub/storage/Cargo.toml +++ b/confidential-data-hub/storage/Cargo.toml @@ -7,11 +7,13 @@ edition = "2021" [dependencies] anyhow.workspace = true +async-trait.workspace = true base64.workspace = true log.workspace = true secret = { path = "../secret" } serde.workspace = true serde_json.workspace = true +strum = { workspace = true, features = ["derive"] } tempfile = { workspace = true, optional = true } thiserror.workspace = true tokio = { workspace = true, optional = true } diff --git a/confidential-data-hub/storage/src/error.rs b/confidential-data-hub/storage/src/error.rs index ed29ab401..113ed610e 100644 --- a/confidential-data-hub/storage/src/error.rs +++ b/confidential-data-hub/storage/src/error.rs @@ -5,16 +5,16 @@ use thiserror::Error; +use crate::volume_type; + pub type Result = std::result::Result; #[derive(Error, Debug)] pub enum Error { - #[error("secure mount failed: {0}")] - SecureMountFailed(String), - - #[error("file error: {0}")] - FileError(String), + #[cfg(feature = "aliyun")] + #[error("Error when mounting Aliyun OSS")] + AliyunOssError(#[from] volume_type::aliyun::error::AliyunError), - #[error("unseal secret failed: {0}")] - UnsealSecretFailed(String), + #[error("Failed to recognize the storage type")] + StorageTypeNotRecognized(#[from] strum::ParseError), } diff --git a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/mod.rs b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/mod.rs deleted file mode 100644 index aa50157f8..000000000 --- a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2023 Intel -// -// SPDX-License-Identifier: Apache-2.0 -// - -pub mod oss; diff --git a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs deleted file mode 100644 index 1af993f2c..000000000 --- a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2023 Intel -// -// SPDX-License-Identifier: Apache-2.0 -// - -use std::os::unix::fs::PermissionsExt; - -use base64::{engine::general_purpose::STANDARD, Engine}; -use log::debug; -use secret::secret::Secret; -use serde::{Deserialize, Serialize}; -use tokio::{fs, io::AsyncWriteExt, process::Command}; - -use crate::{Error, Result}; - -/// Name of the file that contains ossfs password -const OSSFS_PASSWD_FILE: &str = "ossfs_passwd"; - -/// Name of the file that contains gocryptfs password -const GOCRYPTFS_PASSWD_FILE: &str = "gocryptfs_passwd"; - -/// Aliyun OSS filesystem client binary -const OSSFS_BIN: &str = "/usr/local/bin/ossfs"; - -/// Gocryptofs binary -const GOCRYPTFS_BIN: &str = "/usr/local/bin/gocryptfs"; - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct Oss { - #[serde(rename = "akId")] - pub ak_id: String, - #[serde(rename = "akSecret")] - pub ak_secret: String, - #[serde(default)] - pub annotations: String, - pub bucket: String, - #[serde(default)] - pub encrypted: String, - #[serde(rename = "encPasswd", default)] - pub enc_passwd: String, - #[serde(rename = "kmsKeyId", default)] - pub kms_key_id: String, - #[serde(rename = "otherOpts")] - pub other_opts: String, - pub path: String, - pub readonly: String, - #[serde(rename = "targetPath")] - pub target_path: String, - pub url: String, - #[serde(rename = "volumeId")] - pub volume_id: String, -} - -async fn unseal_secret(secret: Vec) -> Result> { - // TODO: verify the jws signature using the key specified by `kid` - // in header. Here we directly get the JWS payload - let payload = secret.split(|c| *c == b'.').nth(1).ok_or_else(|| { - Error::SecureMountFailed("illegal input sealed secret (not a JWS)".into()) - })?; - - let secret_json = STANDARD.decode(payload).map_err(|e| { - Error::SecureMountFailed(format!( - "illegal input sealed secret (JWS body is not standard base64 encoded): {e}" - )) - })?; - let secret: Secret = serde_json::from_slice(&secret_json).map_err(|e| { - Error::SecureMountFailed(format!( - "illegal input sealed secret format (json deseralization failed): {e}" - )) - })?; - - let res = secret - .unseal() - .await - .map_err(|e| Error::UnsealSecretFailed(format!("unseal failed: {e}")))?; - Ok(res) -} - -async fn get_plaintext_secret(secret: &str) -> Result { - if secret.starts_with("sealed.") { - debug!("detected sealed secret"); - let tmp = secret - .strip_prefix("sealed.") - .ok_or(Error::SecureMountFailed( - "strip_prefix \"sealed.\" failed".to_string(), - ))?; - let unsealed = unseal_secret(tmp.into()).await?; - - String::from_utf8(unsealed) - .map_err(|e| Error::SecureMountFailed(format!("convert to String failed: {e}"))) - } else { - Ok(secret.into()) - } -} - -impl Oss { - /// Mount the Aliyun OSS storage to the given `mount_point``. - /// - /// The OSS parameters of the mount source are stored inside the `Oss` struct. - /// - /// If `oss.encrypted` is set to `gocryptfs`, the OSS storage is a gocryptofs FUSE. - /// This function will create a temp directory, which is used to mount OSS. Then - /// use gocryptfs to mount the `mount_point` as plaintext and the temp directory - /// as ciphertext. - pub(crate) async fn mount(&self, _source: String, mount_point: String) -> Result { - // unseal secret - let plain_ak_id = get_plaintext_secret(&self.ak_id).await?; - let plain_ak_secret = get_plaintext_secret(&self.ak_secret).await?; - - // create temp directory to storage metadata for this mount operation - let tempdir = tempfile::tempdir().map_err(|e| { - Error::FileError(format!( - "create ossfs metadata temp directory failed: {e:?}" - )) - })?; - - // create ossfs passwd file - let mut ossfs_passwd_path = tempdir.path().to_owned(); - ossfs_passwd_path.push(OSSFS_PASSWD_FILE); - let ossfs_passwd_path = ossfs_passwd_path.to_string_lossy().to_string(); - let mut ossfs_passwd = fs::File::create(&ossfs_passwd_path) - .await - .map_err(|e| Error::FileError(format!("create ossfs password file failed: {e:?}")))?; - let mut permissions = ossfs_passwd - .metadata() - .await - .map_err(|e| Error::FileError(format!("create metadata failed: {e}")))? - .permissions(); - permissions.set_mode(0o600); - ossfs_passwd - .set_permissions(permissions) - .await - .map_err(|e| Error::FileError(format!("set permissions failed: {e}")))?; - ossfs_passwd - .write_all(format!("{}:{}:{}", self.bucket, plain_ak_id, plain_ak_secret).as_bytes()) - .await - .map_err(|e| Error::FileError(format!("write file failed: {e}")))?; - - // generate parameters for ossfs command - let mut opts = self - .other_opts - .split_whitespace() - .map(str::to_string) - .collect(); - - if self.encrypted == "gocryptfs" { - let gocryptfs_dir = tempfile::tempdir().map_err(|e| { - Error::FileError(format!("create gocryptfs mount dir failed: {e:?}")) - })?; - - let gocryptfs_dir_path = gocryptfs_dir.path().to_string_lossy().to_string(); - let mut parameters = vec![ - format!("{}:{}", self.bucket, self.path), - gocryptfs_dir_path.clone(), - format!("-ourl={}", self.url), - format!("-opasswd_file={ossfs_passwd_path}"), - ]; - - parameters.append(&mut opts); - Command::new(OSSFS_BIN) - .args(parameters) - .spawn() - .map_err(|e| Error::SecureMountFailed(format!("failed to mount oss: {e:?}")))?; - - // get the gocryptfs password - let plain_passwd = get_plaintext_secret(&self.enc_passwd).await?; - - // create gocryptfs passwd file - let mut gocryptfs_passwd_path = tempdir.path().to_owned(); - gocryptfs_passwd_path.push(GOCRYPTFS_PASSWD_FILE); - let gocryptfs_passwd_path = gocryptfs_passwd_path.to_string_lossy().to_string(); - let mut gocryptfs_passwd = - fs::File::create(&gocryptfs_passwd_path) - .await - .map_err(|e| { - Error::FileError(format!("create gocryptfs password file failed: {e:?}")) - })?; - - gocryptfs_passwd - .write_all(plain_passwd.as_bytes()) - .await - .map_err(|e| Error::FileError(format!("write file failed: {e}")))?; - - // generate parameters for gocryptfs, and execute - let parameters = vec![ - gocryptfs_dir_path, - mount_point.clone(), - "-passfile".to_string(), - gocryptfs_passwd_path, - "-nosyslog".to_string(), - ]; - Command::new(GOCRYPTFS_BIN) - .args(parameters) - .spawn() - .map_err(|e| Error::SecureMountFailed(format!("failed to decrypt oss: {e:?}")))?; - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - } else { - let mut parameters = vec![ - format!("{}:{}", self.bucket, self.path), - mount_point.clone(), - format!("-ourl={}", self.url), - format!("-opasswd_file={ossfs_passwd_path}"), - ]; - - parameters.append(&mut opts); - Command::new(OSSFS_BIN) - .args(parameters) - .spawn() - .map_err(|e| Error::SecureMountFailed(format!("failed to mount oss: {e:?}")))?; - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - }; - - Ok(mount_point) - } -} diff --git a/confidential-data-hub/storage/src/volume_type/aliyun/error.rs b/confidential-data-hub/storage/src/volume_type/aliyun/error.rs new file mode 100644 index 000000000..672305afb --- /dev/null +++ b/confidential-data-hub/storage/src/volume_type/aliyun/error.rs @@ -0,0 +1,29 @@ +// Copyright (c) 2024 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum AliyunError { + #[error("Error when getting plaintext of OSS parameters")] + GetPlaintextParameter(#[from] anyhow::Error), + + #[error("Gocryptfs decryption mount failed")] + GocryptfsMountFailed, + + #[error("I/O error")] + IOError(#[from] std::io::Error), + + #[error("Failed to mount oss")] + OssfsMountFailed, + + #[error("Serialize/Deserialize failed")] + SerdeError(#[from] serde_json::Error), + + #[error("Failed to recognize the storage type")] + StorageTypeNotRecognized(#[from] strum::ParseError), +} diff --git a/confidential-data-hub/storage/src/volume_type/aliyun/mod.rs b/confidential-data-hub/storage/src/volume_type/aliyun/mod.rs new file mode 100644 index 000000000..3a0f0eba2 --- /dev/null +++ b/confidential-data-hub/storage/src/volume_type/aliyun/mod.rs @@ -0,0 +1,241 @@ +// Copyright (c) 2023 Intel +// Copyright (c) 2024 Alibaba +// +// SPDX-License-Identifier: Apache-2.0 +// + +pub mod error; + +use std::{collections::HashMap, os::unix::fs::PermissionsExt}; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD, Engine}; +use log::debug; +use secret::secret::Secret; +use serde::{Deserialize, Serialize}; +use tokio::{fs, io::AsyncWriteExt, process::Command}; + +use error::{AliyunError, Result}; + +use super::SecureMount; + +/// Name of the file that contains ossfs password +const OSSFS_PASSWD_FILE: &str = "ossfs_passwd"; + +/// Name of the file that contains gocryptfs password +const GOCRYPTFS_PASSWD_FILE: &str = "gocryptfs_passwd"; + +/// Aliyun OSS filesystem client binary +const OSSFS_BIN: &str = "/usr/local/bin/ossfs"; + +/// Gocryptofs binary +const GOCRYPTFS_BIN: &str = "/usr/local/bin/gocryptfs"; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct OssParameters { + #[serde(rename = "akId")] + pub ak_id: String, + #[serde(rename = "akSecret")] + pub ak_secret: String, + #[serde(default)] + pub annotations: String, + pub bucket: String, + #[serde(default)] + pub encrypted: String, + #[serde(rename = "encPasswd", default)] + pub enc_passwd: String, + #[serde(rename = "kmsKeyId", default)] + pub kms_key_id: String, + #[serde(rename = "otherOpts")] + pub other_opts: String, + pub path: String, + pub readonly: String, + #[serde(rename = "targetPath")] + pub target_path: String, + pub url: String, + #[serde(rename = "volumeId")] + pub volume_id: String, +} + +pub(crate) struct Oss; + +async fn unseal_secret(secret: Vec) -> anyhow::Result> { + // TODO: verify the jws signature using the key specified by `kid` + // in header. Here we directly get the JWS payload + let payload = secret + .split(|c| *c == b'.') + .nth(1) + .ok_or(anyhow!("illegal input sealed secret (not a JWS)"))?; + + let secret_json = STANDARD + .decode(payload) + .context("illegal input sealed secret (JWS body is not standard base64 encoded)")?; + + let secret: Secret = serde_json::from_slice(&secret_json) + .context("illegal input sealed secret format (json deseralization failed)")?; + + let res = secret.unseal().await?; + + Ok(res) +} + +async fn get_plaintext_secret(secret: &str) -> anyhow::Result { + if secret.starts_with("sealed.") { + debug!("detected sealed secret"); + let tmp = secret + .strip_prefix("sealed.") + .ok_or(anyhow!("strip_prefix \"sealed.\" failed"))?; + let unsealed = unseal_secret(tmp.into()).await?; + + String::from_utf8(unsealed).context("convert to String failed") + } else { + Ok(secret.into()) + } +} + +impl Oss { + async fn real_mount( + &self, + options: &HashMap, + _flags: &[String], + mount_point: &str, + ) -> Result<()> { + // construct OssParameters + let parameters = serde_json::to_string(options)?; + + let oss_parameter: OssParameters = serde_json::from_str(¶meters)?; + + // unseal secret + let plain_ak_id = get_plaintext_secret(&oss_parameter.ak_id).await?; + let plain_ak_secret = get_plaintext_secret(&oss_parameter.ak_secret).await?; + + // create temp directory to storage metadata for this mount operation + let tempdir = tempfile::tempdir()?; + + // create ossfs passwd file + let mut ossfs_passwd_path = tempdir.path().to_owned(); + ossfs_passwd_path.push(OSSFS_PASSWD_FILE); + let ossfs_passwd_path = ossfs_passwd_path.to_string_lossy().to_string(); + let mut ossfs_passwd = fs::File::create(&ossfs_passwd_path).await?; + let mut permissions = ossfs_passwd.metadata().await?.permissions(); + permissions.set_mode(0o600); + ossfs_passwd.set_permissions(permissions).await?; + ossfs_passwd + .write_all( + format!( + "{}:{}:{}", + oss_parameter.bucket, plain_ak_id, plain_ak_secret + ) + .as_bytes(), + ) + .await?; + ossfs_passwd.flush().await?; + + // generate parameters for ossfs command + let mut opts = oss_parameter + .other_opts + .split_whitespace() + .map(str::to_string) + .collect(); + + if oss_parameter.encrypted == "gocryptfs" { + let gocryptfs_dir = tempfile::tempdir()?; + + let gocryptfs_dir_path = gocryptfs_dir.path().to_string_lossy().to_string(); + let mut parameters = vec![ + format!("{}:{}", oss_parameter.bucket, oss_parameter.path), + gocryptfs_dir_path.clone(), + format!("-ourl={}", oss_parameter.url), + format!("-opasswd_file={ossfs_passwd_path}"), + ]; + + parameters.append(&mut opts); + let mut oss = Command::new(OSSFS_BIN) + .args(parameters) + .spawn() + .map_err(|_| AliyunError::OssfsMountFailed)?; + let oss_res = oss.wait().await?; + if !oss_res.success() { + { + return Err(AliyunError::OssfsMountFailed); + } + } + + // get the gocryptfs password + let plain_passwd = get_plaintext_secret(&oss_parameter.enc_passwd).await?; + + // create gocryptfs passwd file + let mut gocryptfs_passwd_path = tempdir.path().to_owned(); + gocryptfs_passwd_path.push(GOCRYPTFS_PASSWD_FILE); + let gocryptfs_passwd_path = gocryptfs_passwd_path.to_string_lossy().to_string(); + let mut gocryptfs_passwd = fs::File::create(&gocryptfs_passwd_path).await?; + + gocryptfs_passwd.write_all(plain_passwd.as_bytes()).await?; + gocryptfs_passwd.flush().await?; + + // generate parameters for gocryptfs, and execute + let parameters = vec![ + gocryptfs_dir_path, + mount_point.to_string(), + "-passfile".to_string(), + gocryptfs_passwd_path, + "-nosyslog".to_string(), + ]; + let mut gocryptfs = Command::new(GOCRYPTFS_BIN) + .args(parameters) + .spawn() + .map_err(|_| AliyunError::GocryptfsMountFailed)?; + + let gocryptfs_res = gocryptfs.wait().await?; + if !gocryptfs_res.success() { + { + return Err(AliyunError::GocryptfsMountFailed); + } + } + } else { + let mut parameters = vec![ + format!("{}:{}", oss_parameter.bucket, oss_parameter.path), + mount_point.to_string(), + format!("-ourl={}", oss_parameter.url), + format!("-opasswd_file={ossfs_passwd_path}"), + ]; + + parameters.append(&mut opts); + let mut oss = Command::new(OSSFS_BIN) + .args(parameters) + .spawn() + .map_err(|_| AliyunError::OssfsMountFailed)?; + let oss_res = oss.wait().await?; + if !oss_res.success() { + { + return Err(AliyunError::OssfsMountFailed); + } + } + }; + + Ok(()) + } +} + +#[async_trait] +impl SecureMount for Oss { + /// Mount the Aliyun OSS storage to the given `mount_point``. + /// + /// If `oss.encrypted` is set to `gocryptfs`, the OSS storage is a gocryptofs FUSE. + /// This function will create a temp directory, which is used to mount OSS. Then + /// use gocryptfs to mount the `mount_point` as plaintext and the temp directory + /// as ciphertext. + /// + /// This is a wrapper for inner function to convert error type. + async fn mount( + &self, + options: &HashMap, + flags: &[String], + mount_point: &str, + ) -> super::Result<()> { + self.real_mount(options, flags, mount_point) + .await + .map_err(|e| e.into()) + } +} diff --git a/confidential-data-hub/storage/src/volume_type/mod.rs b/confidential-data-hub/storage/src/volume_type/mod.rs index a910fca44..7a89d06b4 100644 --- a/confidential-data-hub/storage/src/volume_type/mod.rs +++ b/confidential-data-hub/storage/src/volume_type/mod.rs @@ -4,53 +4,63 @@ // #[cfg(feature = "aliyun")] -pub mod alibaba_cloud_oss; +pub mod aliyun; + +use std::{collections::HashMap, str::FromStr}; + +use crate::Result; + +use async_trait::async_trait; -#[cfg(feature = "aliyun")] -use self::alibaba_cloud_oss::oss::Oss; -use crate::{Error, Result}; -use log::warn; use serde::Deserialize; +use strum::EnumString; + +#[derive(EnumString, PartialEq, Debug)] +pub enum Volume { + #[cfg(feature = "aliyun")] + #[strum(serialize = "alibaba-cloud-oss")] + AliOss, +} +/// Indicating a mount point and its parameters. #[derive(PartialEq, Clone, Debug, Deserialize)] pub struct Storage { - pub driver: String, - pub driver_options: Vec, - pub source: String, - pub fstype: String, - pub options: Vec, + /// Driver nameof the mount plugin. + pub volume_type: String, + + /// A key-value map to provide extra mount settings. + pub options: HashMap, + + /// A flag set to provide extra mount settings. This vector can also + /// contain string type parameters. + pub flags: Vec, + + /// The target mount point. pub mount_point: String, } +#[async_trait] +pub trait SecureMount { + /// Mount the volume to `mount_point` due to the given options. + async fn mount( + &self, + options: &HashMap, + flags: &[String], + mount_point: &str, + ) -> Result<()>; +} + impl Storage { pub async fn mount(&self) -> Result { - for driver_option in &self.driver_options { - let (volume_type, metadata) = - driver_option - .split_once('=') - .ok_or(Error::SecureMountFailed( - "split by \"=\" failed".to_string(), - ))?; - - match volume_type { - #[cfg(feature = "aliyun")] - "alibaba-cloud-oss" => { - let oss: Oss = serde_json::from_str(metadata).map_err(|e| { - Error::SecureMountFailed(format!( - "illegal mount info format (json deseralization failed): {e}" - )) - })?; - return oss - .mount(self.source.clone(), self.mount_point.clone()) - .await; - } - other => { - warn!("skip mount info with unsupported volume_type: {other}"); - } - }; + let volume_type = Volume::from_str(&self.volume_type)?; + match volume_type { + #[cfg(feature = "aliyun")] + Volume::AliOss => { + let oss = aliyun::Oss {}; + oss.mount(&self.options, &self.flags, &self.mount_point) + .await?; + Ok(self.mount_point.clone()) + } } - Err(Error::SecureMountFailed( - "illegal mount info as no expected driver_options".to_string(), - )) } }