Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

21 changed files with 231 additions and 3525 deletions

3
.gitignore vendored

@ -1,6 +1,3 @@
# SPDX-License-Identifier: Unlicense
dtrfs.tar
build
tmp
target

24
LICENSE

@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>

@ -1,41 +1,17 @@
<!--
SPDX-License-Identifier: Unlicense
-->
## OS template
You will need a working OS template to work with this project.
Easy solution create an OS template:
- start any archlinux machine (the arch installer also works)
- install `arch-install-scripts`
- run `pacstrap /mnt base openssh` to install base packages to /mnt
- mount the archlinux installation .iso in a VM
- run `pacstrap /mnt base linux openssh`
- run `ln -s /usr/lib/systemd/system/sshd.service /mnt/etc/systemd/system/multi-user.target.wants/sshd.service`
- run `fsarchiver savedir /tmp/os_template.fsa /mnt` to save your OS template
- download `/tmp/os_template.fsa` to your machine
- run `fsarchiver savedir /tmp/os_template.fsa /mnt`
- download `/tmp/os_template.fsa`
- upload the `os_template.fsa` anywhere so that it can be downloaded with wget
Some notes on the above:
- base is the only package required to run a dtrfs VM; the kernel is not needed cause we are using SNP
- base and linux are the only packages to run a VM
- you will need sshd to operate the VM, so create the symlink to make it start with the OS
- fsarchiver is very good at preserving OS data
- fsarchiver saves the absolute path (which means you must use `/mnt` as this is hardcoded)
- the initrd will dump that template to the encrypted disk
- the same procedure can be used with any distribution, but we didn't test that yet
## initrd and linux
You will need an initrd and a kernel to run SNP VMs.
- start any archlinux machine
- clone this repo
- inspect your kernel version by running `file -sL /boot/vmlinuz-linux`.
- (optional) update the kernel version in `./creator_exports.sh`
- create the initrd by running `./create.sh`; this will save the initrd in the build folder
- grab your kernel from `/boot/vmlinuz-linux` and...
- ... upload kernel and initrd to your hypervizor
## module scanner
Optionally, you can use `./remote_create.sh` to upload this repo to remote node and build your initrd.
This will automatically scan the kernel modules running on the remote host, and package all modules in the initrd. This is ideal if your VM has a setup that is not cover by the modules hardcoded in this repo.

@ -1,16 +1,10 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
source creator_exports.sh
source creator_functions.sh
mkdir -p build
cd build
echo_cyan "Installing build dependencies..."
install_build_deps
echo_cyan "Starting installation at $ROOT."
create_dirs
@ -22,31 +16,31 @@ install_binary $(which openssl) && cp -r /etc/ssl "${ROOT}/etc/"
install_binary $(which cryptsetup)
install_binary $(which blkid)
install_binary $(which fdisk)
install_binary $(which sysctl)
install_binary $(which mkfs.ext4)
install_binary $(which ssh-keygen)
install_binary $(which fsarchiver)
install_kmod
install_busybox
install_dtrfs_api
install_guest_api
echo_cyan "Installing scripts..."
install_init_script
echo_cyan "Installing base modules required to boot"
echo_cyan "Installing kernel modules..."
# # Uncomment this section if you want to grab modules from the guest OS
# scan_modules
install_module virtio_net
install_module ext4
install_module virtio_blk
install_module msr
install_module sev-guest
install_module dm_crypt
install_module hid-generic
install_module dm-integrity
install_guest_mods
[[ "$GRAB_LOCAL_MODS" == "YES" ]] && {
scan_modules
backup_active_modules
}
install_module cbc
install_module hmac
install_module sha256
install_module rng
install_module aes
echo_cyan "Building module dependency tree..."
cp /lib/modules/${KERNEL}/modules.{order,builtin,builtin.modinfo} "${ROOT}/lib/modules/${KERNEL}/"

7
creator_exports.sh Executable file

@ -0,0 +1,7 @@
#!/bin/bash
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# root of the initrd, that will be used to create the cpio archive
export ROOT="${script_dir}/build/initrd_root"
export BUSYBOX_PATH="/usr/lib/initcpio/busybox"
export KERNEL="$(uname -r)"

@ -1,7 +1,5 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
echo_cyan() {
echo -e "\033[0;36m$1\033[0m"
}
@ -18,24 +16,11 @@ echo_red() {
echo -e "\033[0;31m$1\033[0m"
}
# TODO: prepare DTRFS to create initrd for other distros
install_build_deps() {
if grep "Arch Linux" /etc/os-release > /dev/null; then
which wget fsarchiver cpio mkinitcpio || {
echo_red "Please install build deps: wget fsarchiver cpio mkinitcpio"
exit 1
}
qemu_guest_modules="../arch_guest_mods"
else
echo_red "ArchLinux is the only distribution currently supported by DTRFS."
exit 1
fi
}
create_dirs() {
rm -rf "$ROOT" 2>/dev/null
mkdir -p "${ROOT}/usr/bin/"
mkdir -p "${ROOT}/usr/bin"
mkdir -p "${ROOT}/usr/lib/dtrfs"
mkdir -p "${ROOT}/usr/lib"
mkdir -p "${ROOT}/dev"
mkdir -p "${ROOT}/etc"
mkdir -p "${ROOT}/mnt"
@ -117,8 +102,7 @@ install_busybox() {
install_init_script() {
cp ../init.sh "${ROOT}/init"
cp ../init_functions.sh "${ROOT}/usr/lib/dtrfs/"
cp ../install_os.sh "${ROOT}/usr/lib/dtrfs/"
cp ../init_functions.sh "${ROOT}/"
}
install_module() {
@ -151,28 +135,8 @@ _install_module() {
done <<< "$( echo "$depends" )"
}
install_guest_mods() {
local mod=''
echo_cyan "Installing kernel modules needed for QEMU guests..."
while read -r mod; do
[[ -z $mod ]] && continue
_install_module "$mod"
done <<< "$(cat "$qemu_guest_modules")"
}
backup_active_modules() {
local modules='' mod=''
echo_yellow "Installing to the initrd all kernel modules currently loaded..."
modules="$(lsmod | awk '{ print $1 }' | grep -v Module)"
while read -r mod; do
[[ -z $mod ]] && continue
_install_module "$mod"
done <<< "$( echo "$modules" )"
}
scan_modules() {
local drivers='' mod=''
echo_yellow "Installing kernel modules based on current hardware..."
install_module "$(df -T / | awk '{ print $2 }' | tail -1)"
drivers=$(lshw -c disk 2>/dev/null | grep -oE 'driver=[a-z\_\-]+' | cut -d '=' -f2;
@ -184,25 +148,29 @@ scan_modules() {
done <<< "$( echo "$drivers" )"
}
install_dtrfs_api() {
local my_location="$(pwd)"
echo_cyan "Building dtrfs_api..."
cd ../../dtrfs_api && cargo build --release || {
echo_yellow "Could not build dtrfs_api. Looking for binary at $(pwd)/dtrfs_api"
}
cd "$my_location"
cp ../../dtrfs_api/target/release/dtrfs_api ./
install_binary "$(pwd)/dtrfs_api"
install_guest_api() {
my_location="$(pwd)"
echo_blue "Building guest_api with cargo and saving log to ${my_location}/guest_api.log"
git clone git@gitea.detee.cloud:SNP/remote_decryption.git
cd remote_decryption/guest_api
# TODO: stick to master branch after code stabilizes
git checkout dtrfs
git pull
cargo build --release > "${my_location}/guest_api.log" 2>&1 ||
echo_red "Failed to build guest_api"
strip --discard-all target/release/guest_api
install_binary "$(pwd)/target/release/guest_api"
cd $my_location
}
create_archive() {
local archive="detee-$(hostnamectl hostname)-${KERNEL}.cpio.gz"
echo_cyan "Creating archive $(pwd)/$archive"
echo_cyan "Creating archive $archive"
echo $archive > .archive_name
my_location="$(pwd)"
cd ${ROOT}
find . | cpio -o -H newc | gzip \
> "${my_location}/detee-$(hostnamectl hostname)-${KERNEL}.cpio.gz"
cd "$my_location"
cd $my_location
}

2564
dtrfs_api/Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,21 +0,0 @@
# SPDX-License-Identifier: Unlicense
[package]
name = "dtrfs_api"
version = "0.1.0"
edition = "2021"
[dependencies]
bs58 = "0.5.1"
anyhow = "1.0.93"
bincode = "1.3.3"
regex = "1.11.1"
sev = { version = "4.0", default-features = false, features = ['crypto_nossl','snp'] }
ed25519-dalek = { version = "2.1.1" }
lazy_static = "1.5.0"
actix-web = { version = "4.9.0", features = ["rustls-0_23"] }
sha3 = "0.10.8"
rustls = "0.23.18"
rustls-pemfile = "2.2.0"
serde = { version = "1.0.215", features = ["derive"] }
base64 = "0.22.1"

@ -1,5 +0,0 @@
# SPDX-License-Identifier: Unlicense
reorder_impl_items = true
use_small_heuristics = "Max"
merge_imports = true

@ -1,220 +0,0 @@
// SPDX-License-Identifier: Unlicense
mod os;
mod snp;
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use lazy_static::lazy_static;
use regex::Regex;
use rustls::{pki_types::PrivateKeyDer, ServerConfig};
use rustls_pemfile::{certs, pkcs8_private_keys};
use serde::Deserialize;
use sha3::{Digest, Sha3_512};
use std::{
fs::File,
io::{BufReader, Read},
};
const CRT_FILE: &str = "/tmp/certs/dtrfs_api.crt";
const KEY_FILE: &str = "/tmp/certs/dtrfs_api.key";
const CMDLINE_FILE: &str = "/proc/cmdline";
lazy_static! {
static ref SNP_REPORT: String = snp::get_report_as_base64(get_cert_hash()).unwrap();
static ref CRT_CONTENTS: String = {
let mut msg = String::new();
let _ = BufReader::new(File::open(CRT_FILE).unwrap()).read_to_string(&mut msg);
msg
};
static ref CMDLINE: String = {
let mut cmdline = String::new();
let _ = BufReader::new(File::open(CMDLINE_FILE).unwrap()).read_to_string(&mut cmdline);
cmdline
};
}
fn get_cert_hash() -> [u8; 64] {
let mut hasher = Sha3_512::new();
let crt = File::open(CRT_FILE).expect("Could not open crt file.");
let mut buf_reader = BufReader::new(crt);
let mut buffer = Vec::new();
buf_reader.read_to_end(&mut buffer).expect("Could not read certificate.");
hasher.update(buffer);
let crt_hash = hasher.finalize();
crt_hash.into()
}
fn verifying_key() -> Result<VerifyingKey, Box<dyn std::error::Error>> {
let re = Regex::new(r"detee_admin=([A-Za-z0-9]+)").unwrap();
let key_str = re.find(&CMDLINE).map(|m| m.as_str()).unwrap_or("");
let key_str =
key_str.strip_prefix("detee_admin=").ok_or("Could not get admin key from cmdline")?;
Ok(VerifyingKey::from_bytes(
&bs58::decode(key_str)
.into_vec()?
.try_into()
.map_err(|_| bs58::decode::Error::BufferTooSmall)?,
)?)
}
fn verify(req: &HttpRequest) -> Result<(), Box<dyn std::error::Error>> {
let signature = req
.headers()
.get("ed25519-signature")
.ok_or_else(|| "Did not find ed25519-signature header")?;
let signature = bs58::decode(signature).into_vec()?;
let signature = Signature::from_bytes(signature.as_slice().try_into()?);
let verifying_key = verifying_key()?;
Ok(verifying_key.verify(CRT_CONTENTS.as_bytes(), &signature)?)
}
#[get("/")]
async fn homepage() -> HttpResponse {
let mut text: String = "Available commands:\n".to_string();
text += "GET: /report /ssh_key\n";
text += "POST: /install /decrypt /switch_root /ssh_key\n";
text += "\nAll requests require the ed25519-signature header";
HttpResponse::Ok().body(text)
}
#[get("/report")]
async fn get_report() -> HttpResponse {
HttpResponse::Ok().body(SNP_REPORT.clone())
}
#[derive(Deserialize)]
struct InstallForm {
hostname: String,
url: String,
sha: String,
keyfile: String,
}
// TODO: QA this function to make sure we don't accidentally allow empty string keyfile
#[post("/install")]
async fn post_install_form(req: HttpRequest, form: web::Form<InstallForm>) -> HttpResponse {
if let Err(e) = verify(&req) {
return HttpResponse::BadRequest().body(format!("Signature verification failed: {}", e));
};
match os::encrypt_and_install_os(&form.url, &form.sha, &form.keyfile, &form.hostname) {
Ok(s) => HttpResponse::Ok().body(s),
Err(e) => HttpResponse::InternalServerError().body(format!("{e:?}")),
}
}
#[derive(Deserialize)]
struct DecryptForm {
keyfile: String,
}
// TODO: QA this function to make sure we don't accidentally allow empty string keyfile
#[post("/decrypt")]
async fn post_decrypt_form(req: HttpRequest, form: web::Form<DecryptForm>) -> HttpResponse {
if let Err(e) = verify(&req) {
return HttpResponse::BadRequest().body(format!("Signature verification failed: {}", e));
};
let decrypt_result = os::try_backup_keyfile(&form.keyfile);
if let Err(decryption_error) = decrypt_result {
return HttpResponse::BadRequest()
.body(format!("Could not decrypt root: {decryption_error:?}"));
}
let hot_key_result = os::replace_hot_keyfile();
HttpResponse::Ok().body(format!("{:?}\n{:?}", decrypt_result, hot_key_result))
}
#[post("/switch_root")]
async fn post_process_exit(req: HttpRequest) -> HttpResponse {
if let Err(e) = verify(&req) {
return HttpResponse::BadRequest().body(format!("Signature verification failed: {}", e));
};
std::process::exit(0);
}
#[derive(Deserialize)]
struct SSHKeyForm {
ssh_key: String,
}
#[get("/server_ssh_pubkeys")]
async fn get_server_pubkeys() -> HttpResponse {
match os::get_server_ssh_pubkeys() {
Ok(keys) => HttpResponse::Ok().body(keys),
Err(e) => HttpResponse::InternalServerError()
.body(format!("Could not get pubkeys due to error: {e:?}\nDid you install the OS?")),
}
}
#[get("/authorized_keys")]
async fn get_authorized_keys(req: HttpRequest) -> HttpResponse {
if let Err(e) = verify(&req) {
return HttpResponse::BadRequest().body(format!("Signature verification failed: {}", e));
};
match os::list_ssh_keys() {
Ok(keys) => HttpResponse::Ok().body(keys),
Err(e) => HttpResponse::BadRequest().body(format!("{e:?}")),
}
}
#[post("/authorized_keys")]
async fn post_authorized_keys(req: HttpRequest, form: web::Form<SSHKeyForm>) -> HttpResponse {
if let Err(e) = verify(&req) {
return HttpResponse::BadRequest().body(format!("Signature verification failed: {}", e));
};
let ssh_key = &form.ssh_key;
match os::add_authorized_key(ssh_key) {
Ok(()) => HttpResponse::Ok().body("Key added to authorized_keys"),
Err(e) => HttpResponse::BadRequest().body(format!("{e:?}")),
}
}
fn load_rustls_config() -> rustls::ServerConfig {
rustls::crypto::aws_lc_rs::default_provider().install_default().unwrap();
let config = ServerConfig::builder().with_no_client_auth();
let cert_file = &mut BufReader::new(File::open(CRT_FILE).unwrap());
let key_file = &mut BufReader::new(File::open(KEY_FILE).unwrap());
let cert_chain = certs(cert_file).collect::<Result<Vec<_>, _>>().unwrap();
let mut keys = pkcs8_private_keys(key_file)
.map(|key| key.map(PrivateKeyDer::Pkcs8))
.collect::<Result<Vec<_>, _>>()
.unwrap();
if keys.is_empty() {
eprintln!("Could not locate PKCS 8 private keys.");
std::process::exit(1);
}
config.with_single_cert(cert_chain, keys.remove(0)).unwrap()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
match os::try_hot_keyfile() {
Ok(_) => {
println!("Hot decryption successful. Booting OS...");
return Ok(());
}
Err(e) => {
println!("Hot decryption failed: {e:?}");
}
}
let config = load_rustls_config();
HttpServer::new(|| {
App::new()
.service(post_install_form)
.service(post_decrypt_form)
.service(post_process_exit)
.service(get_server_pubkeys)
.service(post_authorized_keys)
.service(get_authorized_keys)
.service(get_report)
.service(homepage)
})
.bind_rustls_0_23("[::]:22", config)?
.run()
.await
}

@ -1,164 +0,0 @@
// SPDX-License-Identifier: Unlicense
use crate::snp::get_derived_key;
use anyhow::{anyhow, Result};
use base64::prelude::{Engine, BASE64_URL_SAFE};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::Path,
process::Command,
};
const SNP_KEYFILE_PATH: &str = "/tmp/detee_snp_keyfile";
const BACKUP_KEYFILE_PATH: &str = "/tmp/detee_backup_keyfile";
pub fn encrypt_and_install_os(
install_url: &str,
install_sha: &str,
keyfile: &str,
vm_hostname: &str,
) -> Result<String> {
let binary_keyfile = BASE64_URL_SAFE.decode(keyfile)?;
std::fs::write(BACKUP_KEYFILE_PATH, binary_keyfile)?;
// this path is hardcoded also in the initrd creation script
let install_result = Command::new("/usr/lib/dtrfs/install_os.sh")
.env("INSTALL_URL", install_url)
.env("INSTALL_SHA", install_sha)
.env("SNP_KEY_FILE", SNP_KEYFILE_PATH)
.env("ROOT_KEYFILE", BACKUP_KEYFILE_PATH)
.env("VM_HOSTNAME", vm_hostname)
.output()?;
if !install_result.status.success() {
return Err(anyhow!(
"OS installation script failed.\nScript stdout:\n{}\nScript stderr:\n{}",
String::from_utf8(install_result.stdout)
.unwrap_or("Could not grab stdout from installation script.".to_string()),
String::from_utf8(install_result.stderr)
.unwrap_or("Could not grab stderr from installation script.".to_string()),
));
}
Ok(format!(
"Successfully installed OS. Script stdout:\n{}\nScript stderr:\n{}",
String::from_utf8(install_result.stdout)
.unwrap_or("Could not grab stdout from installation script.".to_string()),
String::from_utf8(install_result.stderr)
.unwrap_or("Could not grab stderr from installation script.".to_string()),
))
}
pub fn try_hot_keyfile() -> Result<()> {
let hot_key = get_derived_key()?;
std::fs::write(SNP_KEYFILE_PATH, hot_key)?;
decrypt_and_mount(SNP_KEYFILE_PATH)?;
Ok(())
}
pub fn try_backup_keyfile(keyfile: &str) -> Result<String> {
let binary_keyfile = BASE64_URL_SAFE.decode(keyfile)?;
std::fs::write(BACKUP_KEYFILE_PATH, binary_keyfile)?;
decrypt_and_mount(BACKUP_KEYFILE_PATH)?;
Ok("Succesfully mounted /mnt using backup keyfile.".to_string())
}
fn decrypt_and_mount(keyfile_path: &str) -> Result<()> {
let decryption_result = Command::new("cryptsetup")
.arg("open")
.arg("--key-file")
.arg(keyfile_path)
.arg("/dev/vda1")
.arg("root")
.output()?;
if !decryption_result.status.success() {
return Err(anyhow!("Could not decrypt disk."));
}
let mount_result = Command::new("mount").arg("/dev/mapper/root").arg("/mnt").output()?;
if !mount_result.status.success() {
return Err(anyhow!("Could not mount /dev/mapper/root to /mnt"));
}
Ok(())
}
pub fn replace_hot_keyfile() -> Result<String> {
let _delete_old_keyfile = Command::new("cryptsetup")
.arg("luksKillSlot")
.arg("-d")
.arg(BACKUP_KEYFILE_PATH)
.arg("/dev/vda1")
.arg("1")
.output();
let meta = std::fs::metadata(SNP_KEYFILE_PATH)?;
if meta.len() == 0 {
return Err(anyhow!("Could not replace hot keyfile using SNP KDF."));
}
let _add_hot_keyfile = Command::new("cryptsetup")
.arg("luksAddKey")
.arg("--key-file")
.arg(BACKUP_KEYFILE_PATH)
.arg("--new-keyfile")
.arg(SNP_KEYFILE_PATH)
.arg("/dev/vda1")
.output();
Ok("Succesfully replaced hot keyfile using SNP KDF.".to_string())
}
pub fn add_authorized_key(key: &str) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
if !Path::new("/mnt/etc/os-release").try_exists().is_ok_and(|found| found == true) {
return Err(anyhow!(
"Operating system not mounted. Please install OS or decrypt existing OS."
));
}
let encoded_key: Vec<&str> = key.split(" ").collect();
if encoded_key.len() < 2 {
return Err(anyhow!("Supplied key is expected to have at least two words."));
}
if let Ok(keys) = File::open("/mnt/.ssh/authorized_keys") {
let mut buffered = BufReader::new(keys).lines();
while let Some(Ok(k)) = buffered.next() {
if k.contains(encoded_key[1]) {
return Err(anyhow!("authorized_keys already contains {key}"));
}
}
} else {
std::fs::create_dir_all("/mnt/root/.ssh")?;
let permissions = std::fs::Permissions::from_mode(0o700);
std::fs::set_permissions("/mnt/root/.ssh", permissions)?;
std::fs::OpenOptions::new()
.create(true)
.write(true)
.open("/mnt/root/.ssh/authorized_keys")?;
let permissions = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions("/mnt/root/.ssh/authorized_keys", permissions)?;
}
let mut keys_file = std::fs::OpenOptions::new()
.append(true) // Open in append mode
.create(true) // Create the file if it doesn't exist
.open("/mnt/root/.ssh/authorized_keys")?;
writeln!(keys_file, "{key}")?;
Ok(())
}
pub fn get_server_ssh_pubkeys() -> Result<String> {
let files = vec![
"/mnt/etc/ssh/ssh_host_rsa_key.pub",
"/mnt/etc/ssh/ssh_host_ecdsa_key.pub",
"/mnt/etc/ssh/ssh_host_ed25519_key.pub",
];
Ok(files
.iter()
.map(|f| std::fs::read_to_string(f))
.collect::<Result<String, std::io::Error>>()?)
}
pub fn list_ssh_keys() -> Result<String> {
Ok(std::fs::read_to_string("/mnt/root/.ssh/authorized_keys")?)
}

@ -1,23 +0,0 @@
// SPDX-License-Identifier: Unlicense
use anyhow::{Context, Result};
use sev::firmware::guest::{AttestationReport, DerivedKey, Firmware, GuestFieldSelect};
use base64::prelude::{Engine, BASE64_URL_SAFE};
fn request_hardware_report(data: [u8; 64]) -> Result<AttestationReport> {
let mut fw = Firmware::open().context("unable to open /dev/sev-guest")?;
fw.get_report(None, Some(data), Some(0)).context("unable to fetch attestation report")
}
pub fn get_report_as_base64(data: [u8; 64]) -> Result<String> {
let report = request_hardware_report(data)?;
Ok(BASE64_URL_SAFE.encode(bincode::serialize(&report)?))
}
pub fn get_derived_key() -> Result<String> {
let mut fw = Firmware::open()?;
let request =
DerivedKey::new(false, GuestFieldSelect(u64::from_str_radix("11111", 2)?), 1, 0, 0);
let derived_key: [u8; 32] = fw.get_derived_key(None, request)?;
Ok(BASE64_URL_SAFE.encode(derived_key))
}

39
init.sh Executable file

@ -0,0 +1,39 @@
#!/bin/bash
source /init_functions.sh
install_url="/tmp/detee_install_url"
install_sha="/tmp/detee_install_sha"
root_keyfile="/tmp/detee_root_keyfile"
ssh_key_file="/tmp/detee_ssh_key"
snp_key_file="/tmp/detee_luks_hotkey"
create_mounts
load_modules
setup_network
# if you wait a bit, it works. The Kernel works in mysterious ways.
sleep 2
modprobe sev_guest
snp_key="$(GET_DERIVATION_KEY=yes guest_api)"
[[ -n $snp_key ]] && echo $snp_key > $snp_key_file
try_hot_decrypt || {
create_certs
guest_api
if [[ -f "$install_url" ]]; then
install_os
else
cryptsetup luksKillSlot -d $root_keyfile /dev/vda1 1
[[ -f "$snp_key_file" ]] && cryptsetup luksAddKey \
--key-file $root_keyfile \
--new-keyfile $snp_key_file /dev/vda1
cryptsetup open -d $root_keyfile /dev/vda1 root
mount /dev/mapper/root /mnt
fi
}
github_ssh_key
detee_ssh_key
exec switch_root /mnt /sbin/init "$@"

139
init_functions.sh Normal file

@ -0,0 +1,139 @@
#!/bin/bash
echo_blue() {
echo -e "\033[34m$1\033[0m"
}
echo_red() {
echo -e "\033[0;31m$1\033[0m"
}
load_modules() {
cat /load_modules.sh | bash
}
create_mounts() {
mount -t proc proc /proc -o nosuid,noexec,nodev
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mount -t devtmpfs dev /dev -o mode=0755,nosuid
mount -t tmpfs run /run -o nosuid,nodev,mode=0755
mkdir -m755 /run/initramfs
if [ -e /sys/firmware/efi ]; then
mount -t efivarfs efivarfs /sys/firmware/efi/efivars -o nosuid,nodev,noexec
fi
# Setup /dev symlinks
if [ -e /proc/kcore ]; then
ln -sfT /proc/kcore /dev/core
fi
ln -sfT /proc/self/fd /dev/fd
ln -sfT /proc/self/fd/0 /dev/stdin
ln -sfT /proc/self/fd/1 /dev/stdout
ln -sfT /proc/self/fd/2 /dev/stderr
}
try_hot_decrypt() {
[[ -f "$snp_key_file" ]] && {
cryptsetup open --key-file $snp_key_file /dev/vda1 root || return 1
mount /dev/mapper/root /mnt || return 1
return 0
}
return 1
}
create_certs() {
cert_dir="/tmp/certs"
key="$cert_dir/guest_api.key"
cert="$cert_dir/guest_api.crt"
subject="/C=W3/O=DeTEE/OU=COCO/CN=guest-api"
mkdir -p "$cert_dir"
openssl genpkey -algorithm RSA -out "$key" \
-pkeyopt rsa_keygen_bits:4096 2>/dev/null
openssl req -x509 -new \
-key "$key" -out "$cert" \
-days 365 -subj "$subject" 2>/dev/null
}
# expects kernel param in this format: detee_net=192.168.122.140_24_192.168.122.1_1.1.1.1
setup_network() {
local settings='' ip_addr='' mask='' cidr='' gateway='' nameserver=''
settings=$(cat /proc/cmdline | grep -oE 'detee_net=[0-9a-z\_\:\.]+' | cut -d '=' -f2)
# TODO: replace with exit 0 when you are ready to force a kernel panic
[[ -z "$settings" ]] && return 0
settings="${settings#detee_net=}"
ip_addr="$( echo ${settings} | cut -d'_' -f1 )"
mask="$( echo ${settings} | cut -d'_' -f2 )"
cidr="${ip_addr}/${mask}"
gateway="$( echo ${settings} | cut -d'_' -f3 )"
nameserver="$( echo ${settings} | cut -d'_' -f4 )"
ip addr add $cidr dev eth0
ip link set eth0 up
ip route add default via $gateway
echo nameserver $nameserver > /etc/resolv.conf
sleep 2
ping -c 2 $gateway
}
install_os() {
local url="$(cat $install_url)" hostname=''
# mount root if it exists
blkid | grep vda1 | grep LUKS && {
cryptsetup open -d $root_keyfile /dev/vda1 root
mount /dev/mapper/root /mnt
return 0
}
# install OS if disk is empty
(
echo n
echo p
echo
echo
echo
echo w
) | fdisk /dev/vda
cryptsetup luksFormat --batch-mode -d $root_keyfile /dev/vda1
[[ -f "$snp_key_file" ]] && cryptsetup luksAddKey \
--key-file $root_keyfile \
--new-keyfile $snp_key_file /dev/vda1
cryptsetup open -d $root_keyfile /dev/vda1 root
mkfs.ext4 /dev/mapper/root
mount /dev/mapper/root /mnt
wget -O /mnt/template.fsa "$url"
sha256sum /mnt/template.fsa | grep $(cat ${install_sha}) || exit 1
fsarchiver restdir /mnt/template.fsa /
rm /mnt/template.fsa
# TODO: decide for UX if maybe we should allow user to inject fstab
echo "" > /mnt/etc/fstab
hostname=$(cat /proc/cmdline | grep -oE 'detee_name=[0-9a-z\_\.\-]+' | cut -d '=' -f2)
[[ -n "$hostname" ]] && echo $hostname > /mnt/etc/hostname
}
# detee_ghu stands for GitHub user and expects format detee_ghu=ghe0
github_ssh_key() {
local key=''
github_user=$(cat /proc/cmdline | grep -oE 'detee_ghu=[0-9a-z\_\.\-]+' | cut -d '=' -f2)
[[ -z "$github_user" ]] && return 0
mkdir -p /mnt/root/.ssh
cd /mnt/root/.ssh
touch authorized_keys
key="$(wget -O - https://github.com/${github_user}.keys)"
grep -F "$( echo $key | awk '{ print $2 }' )" authorized_keys > /dev/null || {
echo "$key" >> authorized_keys
chmod 600 authorized_keys
}
}
# this can be injected through the guest_api
detee_ssh_key() {
local key=''
mkdir -p /mnt/root/.ssh
cd /mnt/root/.ssh
[[ -f "$ssh_key_file" ]] && while read -r key; do
grep -F "$( echo $key | awk '{ print $2 }' )" authorized_keys > /dev/null || {
echo "$key" >> authorized_keys
}
done < "$ssh_key_file"
chmod 600 authorized_keys
}

@ -1,7 +1,4 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
dir="/tmp/dtrfs"
@ -14,13 +11,9 @@ if [ -z $1 ]; then
fi
server="$1"
scp_server="$1"
echo $1 | grep -F ':' > /dev/null && scp_server="[${1}]"
if ! [ -z $2 ]; then
server="${2}@${server}"
echo $1 | grep -F ':' > /dev/null && scp_server="${2}@${scp_server}"
fi
if [ -z $3 ]; then
@ -31,24 +24,20 @@ else
scp="scp -P $3"
fi
echo
echo Starting installation...
echo
set -e
# TODO: test if this works
mkdir -p tmp build
my_location="$(pwd)"
cd ../dtrfs_api && cargo build --release || {
echo Could not build dtrfs_api
exit 1
}
cd "$my_location"
cp ../dtrfs_api/target/release/dtrfs_api ./build/
tar cf tmp/dtrfs.tar *.sh build/dtrfs_api arch_guest_mods
mkdir -p tmp
tar cf tmp/dtrfs.tar *.sh
$ssh $server rm -rf ${dir}
$ssh $server mkdir -p ${dir}
$scp tmp/dtrfs.tar ${scp_server}:${dir}
$scp tmp/dtrfs.tar ${server}:${dir}
$ssh $server tar -xf ${dir}/dtrfs.tar -C ${dir}
$ssh $server GRAB_LOCAL_MODS=YES ${dir}/create.sh
$ssh $server ${dir}/create.sh
archive=$($ssh $server cat ${dir}/build/.archive_name)
$scp ${scp_server}:${dir}/build/${archive} tmp/
$scp ${server}:${dir}/build/${archive} tmp/
echo
echo initrd downloaded to: $(pwd)/tmp/${archive}

@ -1,131 +0,0 @@
aesni_intel
asn1_encoder
async_tx
async_xor
atkbd
bridge
br_netfilter
cbc
cdrom
crc16
crc32c_generic
crc32c_intel
crc32_pclmul
crct10dif_pclmul
cryptd
crypto_simd
curve25519_x86_64
dm_bufio
dm_crypt
dm-integrity
dm_integrity
dm_mod
efi_secret
encrypted_keys
ext4
failover
gf128mul
ghash_clmulni_intel
i2c_i801
i2c_mux
i2c_smbus
i8042
inet_diag
intel_agp
intel_gtt
intel_pmc_bxt
intel_rapl_common
intel_rapl_msr
ip6table_filter
ip6table_mangle
ip6table_nat
ip6_tables
ip6_udp_tunnel
ip_set
ip_set_hash_net
iptable_filter
iptable_mangle
iptable_nat
iptable_raw
ip_tables
ipt_REJECT
iTCO_vendor_support
iTCO_wdt
jbd2
libaesgcm
libchacha20poly1305
libcrc32c
libcurve25519_generic
libps2
llc
loop
lpc_ich
mac_hid
mbcache
mousedev
net_failover
nf_conntrack
nf_conntrack_netlink
nf_defrag_ipv4
nf_defrag_ipv6
nf_nat
nfnetlink
nfnetlink_acct
nfnetlink_log
nf_reject_ipv4
nf_tables
nft_chain_nat
nft_compat
overlay
parport
parport_pc
pcspkr
polyval_clmulni
polyval_generic
ppdev
psmouse
qemu_fw_cfg
serio
serio_raw
sev-guest
sev_guest
sha1_ssse3
sha256
sha256_ssse3
sha512_ssse3
sr_mod
stp
tcp_diag
tee
trusted
tsm
udp_tunnel
veth
virtio_blk
virtio_net
vivaldi_fmap
vmw_vmci
vmw_vsock_virtio_transport_common
vmw_vsock_vmci_transport
vsock
vsock_loopback
vxlan
wireguard
xfrm_algo
xfrm_user
xor
x_tables
xt_addrtype
xt_comment
xt_conntrack
xt_limit
xt_mark
xt_MASQUERADE
xt_multiport
xt_nat
xt_nfacct
xt_NFLOG
xt_physdev
xt_REDIRECT
xt_set
xt_tcpudp

@ -1,18 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# root of the initrd, that will be used to create the cpio archive
[[ -n "$ROOT" ]] ||
export ROOT="${script_dir}/build/initrd_root"
# you need busybox to be able to instsall
[[ -n "$BUSYBOX_PATH" ]] ||
export BUSYBOX_PATH="/usr/lib/initcpio/busybox"
# choose a kernel if you don't want to use the active one
[[ -n "$KERNEL" ]] ||
export KERNEL="$(uname -r)"
# this will allow you to grab modules from the machine where the installer is running
[[ -n "$GRAB_LOCAL_MODS" ]] ||
export GRAB_LOCAL_MODS="no"

@ -1,30 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
source /usr/lib/dtrfs/init_functions.sh
install_url="/tmp/detee_install_url"
install_sha="/tmp/detee_install_sha"
root_keyfile="/tmp/detee_root_keyfile"
ssh_key_file="/tmp/detee_ssh_key"
snp_key_file="/tmp/detee_luks_hotkey"
create_mounts
load_modules
setup_network
# load this module again cause it fails the first time
modprobe sev_guest
create_certs
dtrfs_api
github_ssh_key
cp /etc/resolv.conf /mnt/etc/resolv.conf
# copy kernel modules in case the user deleted the old modules
mkdir -p /mnt/lib/modules/
cp -rn /lib/modules/* /mnt/lib/modules/
exec switch_root /mnt /sbin/init "$@"

@ -1,100 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
load_modules() {
cat /load_modules.sh | bash
}
create_mounts() {
mount -t proc proc /proc -o nosuid,noexec,nodev
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mount -t devtmpfs dev /dev -o mode=0755,nosuid
mount -t tmpfs run /run -o nosuid,nodev,mode=0755
mkdir -m755 /run/initramfs
if [ -e /sys/firmware/efi ]; then
mount -t efivarfs efivarfs /sys/firmware/efi/efivars -o nosuid,nodev,noexec
fi
# Setup /dev symlinks
if [ -e /proc/kcore ]; then
ln -sfT /proc/kcore /dev/core
fi
ln -sfT /proc/self/fd /dev/fd
ln -sfT /proc/self/fd/0 /dev/stdin
ln -sfT /proc/self/fd/1 /dev/stdout
ln -sfT /proc/self/fd/2 /dev/stderr
}
create_certs() {
cert_dir="/tmp/certs"
key="$cert_dir/dtrfs_api.key"
cert="$cert_dir/dtrfs_api.crt"
subject="/C=W3/O=DeTEE/OU=COCO/CN=dtrfs-api"
mkdir -p "$cert_dir"
openssl genpkey -algorithm RSA -out "$key" \
-pkeyopt rsa_keygen_bits:4096 2>/dev/null
openssl req -x509 -new \
-key "$key" -out "$cert" \
-addext "subjectAltName=DNS:dtrfs-api" \
-days 365 -subj "$subject" 2>/dev/null
}
setup_network_device() {
local device="$1" settings="$2" ip_addr='' mask='' cidr='' gateway=''
[[ -z "$settings" ]] && return 0
ip_addr="$( echo ${settings} | cut -d '_' -f1 )"
mask="$( echo ${settings} | cut -d '_' -f2 )"
cidr="${ip_addr}/${mask}"
gateway="$( echo ${settings} | cut -d '_' -f3 )"
ip addr add $cidr dev $device
ip link set $device up
sysctl -w net.ipv6.conf.$device.accept_ra=0
ip route add default via $gateway
sleep 2
ping -c 2 $gateway
}
# Expects kernel param in this format: detee_net=192.168.122.140_24_192.168.122.1_1.1.1.1
# In case the interface name is not specified, it defaults to eth0
# Supports manual device, for example: detee_net_eth1
# TODO: test if it is required to specify mac instead of device name
setup_network() {
local device_eth0_cfg='' device_cfg='' device_configs=''
sysctl -w net.ipv6.conf.all.accept_ra=0
sysctl -w net.ipv6.conf.default.accept_ra=0
# handle the default: detee_net=...
device_eth0_cfg=$(cat /proc/cmdline | grep -oE "detee_net=[0-9a-f\_\:\.]+" | cut -d '=' -f2)
[[ -z "$device_eth0_cfg" ]] || setup_network_device eth0 $device_eth0_cfg
# handle extra devices: detee_net_eth1=...
device_configs=$(cat /proc/cmdline| grep -oE "detee_net_[a-z0-9]*=[0-9a-f\_\:\.]+")
while read -r device_cfg; do
setup_network_device \
$(echo $device_cfg | cut -d '=' -f1 | cut -d '_' -f3) \
$(echo $device_cfg | cut -d '=' -f2)
done <<< "$( echo "$device_configs" )"
echo nameserver 1.1.1.1 >> /etc/resolv.conf
echo nameserver 1.0.0.1 >> /etc/resolv.conf
echo nameserver 2606:4700:4700::1111 >> /etc/resolv.conf
echo nameserver 2606:4700:4700::1001 >> /etc/resolv.conf
}
# detee_ghu stands for GitHub user and expects format detee_ghu=ghe0
github_ssh_key() {
local key=''
github_user=$(cat /proc/cmdline | grep -oE 'detee_ghu=[0-9a-zA-Z\_\.\-]+' | cut -d '=' -f2)
[[ -z "$github_user" ]] && return 0
mkdir -p /mnt/root/.ssh
cd /mnt/root/.ssh
touch authorized_keys
key="$(wget -O - https://github.com/${github_user}.keys)"
grep -F "$( echo $key | awk '{ print $2 }' )" authorized_keys > /dev/null || {
echo "$key" >> authorized_keys
chmod 600 authorized_keys
}
}

@ -1,76 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
# This script is called by dtrfs_api to install an OS.
[[ -z "$INSTALL_URL" ]] && {
echo "Did not find INSTALL_URL env variable".
exit 1
}
[[ -z "$INSTALL_SHA" ]] && {
echo "Did not find INSTALL_SHA env variable".
exit 2
}
[[ -z "$VM_HOSTNAME" ]] && {
echo "Did not find VM_HOSTNAME env variable".
exit 2
}
[[ -f "$ROOT_KEYFILE" ]] || {
echo "Did not find keyfile at the following location: $ROOT_KEYFILE"
exit 3
}
# mount root if it exists
blkid | grep vda1 | grep LUKS && {
echo "/dev/vda1 already has a LUKS partition"
exit 4
}
echo === Creating partition /dev/vda1
(
echo n
echo p
echo
echo
echo
echo w
) | fdisk /dev/vda
echo "=== Formatting /dev/vda1 using cryptsetup luksFormat and opening as root"
cryptsetup luksFormat --batch-mode -d $ROOT_KEYFILE /dev/vda1 || exit 5
[[ -f "$SNP_KEY_FILE" ]] && {
echo "Adding LUKS slot via SNP KDF key found at $SNP_KEY_FILE"
cryptsetup luksAddKey \
--key-file $ROOT_KEYFILE \
--new-keyfile $SNP_KEY_FILE /dev/vda1
}
cryptsetup open -d $ROOT_KEYFILE /dev/vda1 root || exit 6
echo "=== Formatting /dev/mapper/root as ext4 and mounting at /mnt"
mkfs.ext4 /dev/mapper/root || exit 7
mount /dev/mapper/root /mnt || exit 8
echo "=== Downloading OS template from $INSTALL_URL and verifying hash"
wget -O /mnt/template.fsa "$INSTALL_URL" || {
echo "Failed to download $INSTALL_URL"
exit 9
}
sha256sum /mnt/template.fsa | grep "${INSTALL_SHA}" || exit 1
echo "=== Installing OS template"
fsarchiver restdir /mnt/template.fsa /
rm /mnt/template.fsa
# TODO: decide for UX if maybe we should allow user to inject fstab
echo "" > /mnt/etc/fstab
echo "=== Setting up guest hostname as $VM_HOSTNAME"
echo $VM_HOSTNAME > /mnt/etc/hostname
echo "=== Generating SSH public keys"
echo "root:x:0:0:root:/root:/bin/sh" > /etc/passwd
[[ -f "/mnt/etc/ssh/ssh_host_rsa_key" ]] ||
ssh-keygen -t rsa -f /mnt/etc/ssh/ssh_host_rsa_key -N '' > /dev/null
[[ -f "/mnt/etc/ssh/ssh_host_ecdsa_key" ]] ||
ssh-keygen -t ecdsa -f /mnt/etc/ssh/ssh_host_ecdsa_key -N '' > /dev/null
[[ -f "/mnt/etc/ssh/ssh_host_ed25519_key" ]] ||
ssh-keygen -t ed25519 -f /mnt/etc/ssh/ssh_host_ed25519_key -N '' > /dev/null
echo "=== Done! Download keys from /server_pubkeys"

@ -1,27 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: Unlicense
kernel_path="/boot/vmlinuz-linux"
dtrfs_path="$1"
dtrfs_name=$(basename $dtrfs_path)
dtrfs_sha=$(sha256sum $dtrfs_path | awk '{ print $1 }')
kernel_name="vmlinuz-linux-$(uname -r)"
kernel_sha=$(sha256sum $kernel_path | awk '{ print $1 }')
scp $dtrfs_path registry.detee.ltd:/var/www/html/${dtrfs_name}
ssh registry.detee.ltd ln -s $dtrfs_name /var/www/html/${dtrfs_sha}
scp $kernel_path registry.detee.ltd:/var/www/html/${kernel_name}
ssh registry.detee.ltd ln -s $kernel_name /var/www/html/${kernel_sha}
echo "Also add this to detee-cli/src/snp/mod.rs"
echo "
name: \"dtrfs-$(uname -r)\".to_string(),
vendor: \"ghe0\".to_string(),
dtrfs_url: \"http://registry.detee.ltd/${dtrfs_name}\".to_string(),
dtrfs_sha: \"${dtrfs_sha}\".to_string(),
kernel_url: \"http://registry.detee.ltd/${kernel_name}\".to_string(),
kernel_sha: \"${kernel_sha}\".to_string()
"