Skip to content
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

cdh: support to encrypt block device #617

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions confidential-data-hub/docs/SECURE_STORAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,28 @@ flowchart LR
```

For more details, please refer to [the guide](use-cases/secure-mount-with-aliyun-oss.md).

### Block Device

The [plugin](../storage/src/volume_type/blockdevice) provides ways to encrypt a block device and mount it to a specific mount point. Currently only support LUKS in [cryptsetup](https://gitlab.com/cryptsetup/cryptsetup/) for block device encryption.

#### LUKS Encryption

In this mode, the device would be encrypted as LUKS device first, and then mount it to a target path to store the data to protect the confidentiality and integrity of the data.

The architecture diagram is

```mermaid
flowchart LR
A[Local/Network] -- mount --> B[Block Device]
subgraph TEE Guest
B -- Check if encrypted --> F{Is Encrypted?}
F -- No --> G[Encrypt by cryptsetup]
G -- encrypt --> C[LUKS Encrypted Block Device]
F -- Yes --> C[LUKS Encrypted Block Device]
C -- open and mapping --> D[Mapped Device]
D -- mount --> E[Target Path]
end
```

For more details, please refer to [the guide](use-cases/secure-mount-with-block-device.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Secure mount with Block Device

This guide helps an user to use confidential data hub to secure mount inside TEE environment.

## Preliminaries

- Ensure that [cryptsetup](https://gitlab.com/cryptsetup/cryptsetup/) is installed.

## Example

### Create a loop device

```shell
$ loop_file="/tmp/test.img"
$ sudo dd if=/dev/zero of=$loop_file bs=1M count=1000
$ sudo losetup -fP $loop_file
$ device=$(sudo losetup -j $loop_file | awk -F'[: ]' '{print $1}')
$ echo $device
# Output should be something like /dev/loop0
$ device_num=$(sudo lsblk -no MAJ:MIN $device)
$ echo $device_num
# Output should be something like 7:0
```

### Secure mount inside a TEE environment

1. Build the CDH and its client tool

Follow the instructions in the [CDH README](../../README.md#confidential-data-hub) and [Client Tool README](../../README.md#client-tool) to build the CDH and its client tool.

2. Install `luks-encrypt-storage`

Install [luks-encrypt-storage](../../storage/scripts/luks-encrypt-storage) into `/usr/local/bin`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a separate service, CDH could require install extra things. I am thinking about whether we can integrate the installations of luks-encrypt-storage, cachefs and ossfs binaries/scripts in CDH's Makefile. In CCv0, we explicit install them in kata scripts. This would bring extra maintaining cost for kata. cc @fitzthum @fidencio

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we can move the tasks of installing binaries or scripts to CDH, as they are only used in the CDH. CDH is responsible for installing them, and we can simplify the code in kata.


3. Run CDH
```shell
$ confidential-data-hub
```

4. Prepare a request JSON `storage.json`
```json
{
{
"volume_type": "BlockDevice",
"options": {
"deviceId": "7:0",
"encryptType": "LUKS",
"dataIntegrity": "true"
},
"flags": [],
"mount_point": "/mnt/test-path"
}
}

```
- Fields:
- `volume_type`: The secure mount plugin type name. It determines how the rest of the fields are used.
- `options`: A key-value map specifying the settings for the mount operation. Different plugins can define different keys in the options. In this example, all keys are for block devices.
- `flags`: A string list specifying settings for the mount operation. Different plugins can define different uses for this field.
- `mount_point`: The target mount path for the operation.

- Options Fields:
- `deviceId`: The device number, formatted as "MAJ:MIN".
- `encryptType`: The encryption type. Currently, only LUKS is supported.
- `encryptKey`: Encryption key. It can be a sealed secret or a resource uri. If not set, it means that the device is unencrypted and a random 4096-byte key will be generated to encrypt the device.
- `dataIntegrity`: Enables dm-integrity to protect data integrity. Note that enabling data integrity will reduce IO performance by more than 30%.

5. Make a request to CDH
```shell
$ client-tool secure-mount --storage-path storage.json

# Check the target path to see if the mount succeeded
$ lsblk |grep "encrypted_disk"
# Expected output:
└─encrypted_disk_OEyEj_dif 253:1 0 968.6M 0 crypt
└─encrypted_disk_OEyEj 253:2 0 968.6M 0 crypt /mnt/test-path
```
1 change: 1 addition & 0 deletions confidential-data-hub/storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
log.workspace = true
kms = { path = "../kms", features = ["kbs"] }
rand = { workspace = true, optional = true }
secret = { path = "../secret" }
serde.workspace = true
Expand Down
149 changes: 149 additions & 0 deletions confidential-data-hub/storage/scripts/luks-encrypt-storage
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/bin/bash
#
# Copyright (c) 2022 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace

[ -n "${DEBUG:-}" ] && set -o xtrace

handle_error() {
local exit_code="${?}"
local line_number="${1:-}"
echo "error:"
echo "Failed at $line_number: ${BASH_COMMAND}"
exit "${exit_code}"
}
trap 'handle_error $LINENO' ERR

die()
{
local msg="$*"
echo >&2 "ERROR: $msg"
exit 1
}

setup()
{
local cmds=()

cmds+=("cryptsetup" "mkfs.ext4" "mount")

local cmd
for cmd in "${cmds[@]}"
do
command -v "$cmd" &>/dev/null || die "need command: '$cmd'"
done
}

setup

device_num=${1:-}
if [ -z "$device_num" ]; then
die "invalid arguments, at least one param for device num"
fi

is_encrypted="false"
if [ -n "${2-}" ]; then
is_encrypted="$2"
fi

mount_point="/tmp/target_path"
if [ -n "${3-}" ]; then
mount_point="$3"
fi

storage_key_path="/run/encrypt_storage.key"
if [ -n "${4-}" ]; then
storage_key_path="$4"
fi

data_integrity="true"
if [ -n "${5-}" ]; then
data_integrity="$5"
fi

device_name=$(sed -e 's/DEVNAME=//g;t;d' "/sys/dev/block/${device_num}/uevent")
device_path="/dev/$device_name"

opened_device_name=$(mktemp "encrypted_disk_XXXXX")

if [[ -n "$device_name" && -b "$device_path" ]]; then

if [ "$is_encrypted" == "false" ]; then

if [ "$data_integrity" == "false" ]; then
echo "YES" | cryptsetup luksFormat --type luks2 "$device_path" --sector-size 4096 \
--cipher aes-xts-plain64 "$storage_key_path"
else
# Wiping a device is a time consuming operation. To avoid a full wipe, integritysetup
# and crypt setup provide a --no-wipe option.
# However, an integrity device that is not wiped will have invalid checksums. Normally
# this should not be a problem since a page must first be written to before it can be read
# (otherwise the data would be arbitrary). The act of writing would populate the checksum
# for the page.
# However, tools like mkfs.ext4 read pages before they are written; sometimes the read
# of an unwritten page happens due to kernel buffering.
# See https://gitlab.com/cryptsetup/cryptsetup/-/issues/525 for explanation and fix.
# The way to propery format the non-wiped dm-integrity device is to figure out which pages
# mkfs.ext4 will write to and then to write to those pages before hand so that they will
# have valid integrity tags.
echo "YES" | cryptsetup luksFormat --type luks2 "$device_path" --sector-size 4096 \
--cipher aes-xts-plain64 --integrity hmac-sha256 "$storage_key_path" \
--integrity-no-wipe
fi
fi

cryptsetup luksOpen -d "$storage_key_path" "$device_path" $opened_device_name
rm "$storage_key_path"

if [ "$data_integrity" == "false" ]; then
mkfs.ext4 /dev/mapper/$opened_device_name -E lazy_journal_init
else
# mkfs.ext4 doesn't perform whole sector writes and this will cause checksum failures
# with an unwiped integrity device. Therefore, first perform a dry run.
output=$(mkfs.ext4 /dev/mapper/$opened_device_name -F -n)

# The above command will produce output like
# mke2fs 1.46.5 (30-Dec-2021)
# Creating filesystem with 268435456 4k blocks and 67108864 inodes
# Filesystem UUID: 4a5ff012-91c0-47d9-b4bb-8f83e830825f
# Superblock backups stored on blocks:
# 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
# 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
# 102400000, 214990848
delimiter="Superblock backups stored on blocks:"
blocks_list=$([[ $output =~ $delimiter(.*) ]] && echo "${BASH_REMATCH[1]}")

# Find list of blocks
block_nums=$(echo "$blocks_list" | grep -Eo '[0-9]{4,}' | sort -n)

# Add zero to list of blocks
block_nums="0 $block_nums"

# Iterate through each block and write to it to ensure that it has valid checksum
for block_num in $block_nums
do
echo "Clearing page at $block_num"
# Zero out the page
dd if=/dev/zero bs=4k count=1 oflag=direct \
of=/dev/mapper/$opened_device_name seek="$block_num"
done

# Now perform the actual ext4 format. Use lazy_journal_init so that the journal is
# initialized on demand. This is safe for ephemeral storage since we don't expect
# ephemeral storage to survice a power cycle.
mkfs.ext4 /dev/mapper/$opened_device_name -E lazy_journal_init
fi

[ ! -d "$mount_point" ] && mkdir -p $mount_point

mount /dev/mapper/$opened_device_name $mount_point
else
die "Invalid device: '$device_path'"
fi
3 changes: 3 additions & 0 deletions confidential-data-hub/storage/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub enum Error {
#[error("Error when mounting Aliyun OSS")]
AliyunOssError(#[from] volume_type::aliyun::error::AliyunError),

#[error("Error when mounting Block device")]
BlockDeviceError(#[from] volume_type::blockdevice::error::BlockDeviceError),

#[error("Failed to recognize the storage type")]
StorageTypeNotRecognized(#[from] strum::ParseError),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2024 Intel
//
// SPDX-License-Identifier: Apache-2.0
//

use thiserror::Error;

pub type Result<T> = std::result::Result<T, BlockDeviceError>;

#[derive(Error, Debug)]
pub enum BlockDeviceError {
#[error("Error when getting encrypt/decrypt keys")]
GetKeysFailure(#[from] anyhow::Error),

#[error("LUKS decryption mount failed")]
LUKSfsMountFailed,

#[error("I/O error")]
IOError(#[from] std::io::Error),

#[error("Failed to mount block device")]
BlockDeviceMountFailed,

#[error("Serialize/Deserialize failed")]
SerdeError(#[from] serde_json::Error),

#[error("Failed to recognize the storage type")]
StorageTypeNotRecognized(#[from] strum::ParseError),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2024 Intel
//
// SPDX-License-Identifier: Apache-2.0
//
use super::Interpreter;
use super::{get_plaintext_key, BlockDeviceError, BlockDeviceParameters, Result};
use async_trait::async_trait;
use log::error;
use rand::{distributions::Alphanumeric, Rng};
use tokio::{
fs,
io::{AsyncReadExt, AsyncWriteExt},
process::Command,
};

/// LUKS encrypt storage binary
const LUKS_ENCRYPT_STORAGE_BIN: &str = "/usr/local/bin/luks-encrypt-storage";

async fn random_encrypt_key() -> Vec<u8> {
let mut buffer = vec![0u8; 4096];
rand::thread_rng().fill(&mut buffer[..]);
buffer
}

async fn create_storage_key_file(
storage_key_path: &str,
encrypt_key: Option<String>,
) -> anyhow::Result<()> {
let mut storage_key_file = fs::File::create(storage_key_path).await?;

let plain_key = match encrypt_key {
Some(encrypt_key) => get_plaintext_key(&encrypt_key).await?,
None => random_encrypt_key().await,
};

storage_key_file.write_all(&plain_key).await?;
storage_key_file.flush().await?;
Ok(())
}

pub(crate) struct LuksInterpreter;

#[async_trait]
impl Interpreter for LuksInterpreter {
async fn secure_device_mount(
&self,
parameters: BlockDeviceParameters,
mount_point: &str,
) -> Result<()> {
let random_string: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(5)
.map(char::from)
.collect();
let storage_key_path = format!("/tmp/encrypted_storage_key_{}", random_string);
create_storage_key_file(&storage_key_path, parameters.encryption_key.clone()).await?;

let parameters = vec![
parameters.device_id,
match parameters.encryption_key {
None => "false".to_string(),
Some(_) => "true".to_string(),
},
mount_point.to_string(),
storage_key_path,
parameters.data_integrity.clone(),
];

let mut encrypt_device = Command::new(LUKS_ENCRYPT_STORAGE_BIN)
.args(parameters)
.spawn()
.map_err(|e| {
error!("luks-encrypt-storage cmd fork failed: {:?}", e);
BlockDeviceError::BlockDeviceMountFailed
})?;

let bd_res = encrypt_device.wait().await?;
if !bd_res.success() {
let mut stderr = String::new();
if let Some(mut err) = encrypt_device.stderr.take() {
err.read_to_string(&mut stderr).await?;
error!("BlockDevice mount failed with stderr: {:?}", stderr);
} else {
error!("BlockDevice mount failed");
}

return Err(BlockDeviceError::BlockDeviceMountFailed);
}
Ok(())
}
}
Loading
Loading