Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Rust build artifacts
/target/
Cargo.lock

# IDE files
.vscode/
.idea/
*.swp
*.swo

# Temporary files
*.tmp
*.temp
21 changes: 21 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "clevis-pin-trustee"
version = "0.1.0"
edition = "2024"
description = "Clevis PIN for URL-based encryption/decryption"
repository = "https://github.com/confidential-clusters/clevis-pin-trustee"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json", "blocking"] }
tokio = { version = "1.0", features = ["full"] }
clap = { version = "4.0", features = ["derive"] }
anyhow = "1.0"
josekit = "0.10.3"
base64 = "0.22.1"
rand = "0.9.2"
hex = "0.4.3"

[dev-dependencies]
tempfile = "3.0"
10 changes: 10 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM docker.io/library/rust:trixie as build
Copy link
Member

Choose a reason for hiding this comment

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

Let's use a Fedora image here to do the build. We can do what we did in compure-pcrs.


COPY . /src
WORKDIR /src
RUN cargo build --release

FROM quay.io/fedora/fedora:42
COPY --from=build /src/target/release/clevis-pin-trustee /usr/bin/clevis-pin-trustee
COPY --from=build /src/clevis-encrypt-trustee /usr/bin/clevis-encrypt-trustee
COPY --from=build /src/clevis-decrypt-trustee /usr/bin/clevis-decrypt-trustee
3 changes: 3 additions & 0 deletions clevis-decrypt-trustee
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
Copy link
Member

Choose a reason for hiding this comment

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

Hum, why do we need that? Let's place the binaries in the right place directly?


clevis-pin-trustee decrypt "$@"
3 changes: 3 additions & 0 deletions clevis-encrypt-trustee
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

clevis-pin-trustee encrypt "$@"
9 changes: 9 additions & 0 deletions data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"servers": [
{
"url": "http://localhost:8080",
"cert": ""
}
],
"path": "conf-cluster/12345/root"
}
237 changes: 237 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
use anyhow::{anyhow, Context, Result};
use base64::{engine::general_purpose, Engine as _};
use clap::{Parser, Subcommand};
use josekit::jwe::alg::direct::DirectJweAlgorithm::Dir;
use josekit::jwk::Jwk;
use serde::{Deserialize, Serialize};
use std::io::{self, Read, Write};
use std::process::Command as StdCommand;
use std::thread;
use std::time::Duration;

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Server {
url: String,
cert: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct Config {
servers: Vec<Server>,
path: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct ClevisHeader {
pin: String,
servers: Vec<Server>,
path: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Key {
pub key_type: String,
pub key: String,
}

fn fetch_and_prepare_jwk(
servers: &[Server],
path: &str,
) -> Result<Jwk> {
let key = fetch_luks_key(servers, path)?;
let key = String::from_utf8(
general_purpose::STANDARD
.decode(&key)
.context("Error decoding key in base64")?,
)
.context("Error decoding the key in JSON")?;
eprintln!("Key: {:?}", key);
let key: Key = serde_json::from_str(&key).context("Error in parsing the fetched key")?;

let mut jwk = Jwk::new(&key.key_type);
jwk.set_key_value(&key.key);
jwk.set_key_operations(vec!["encrypt", "decrypt"]);

Ok(jwk)
}

fn encrypt(config: &str) -> Result<()> {
let config: Config =
serde_json::from_str(config).map_err(|e| anyhow!("Failed to parse config JSON: {}", e))?;

let mut input = Vec::new();
io::stdin().read_to_end(&mut input)?;

let jwk = fetch_and_prepare_jwk(
&config.servers,
&config.path,
)?;

eprintln!("{}", jwk.to_string());
let encrypter = Dir
.encrypter_from_jwk(&jwk)
.context("Error creating direct encrypter")?;

let private_hdr = ClevisHeader {
pin: "trustee".to_string(),
servers: config.servers.clone(),
path: config.path,
};

let mut hdr = josekit::jwe::JweHeader::new();
hdr.set_algorithm("ECDH-ES");
hdr.set_content_encryption("A256GCM");
hdr.set_claim(
"clevis",
Some(serde_json::value::to_value(private_hdr).context("Error serializing private header")?),
)
.context("Error adding clevis claim")?;

let jwe_token = josekit::jwe::serialize_compact(&input, &hdr, &encrypter)
.context("Error serializing JWE token")?;

io::stdout()
.write_all(jwe_token.as_bytes())
.context("Error writing the token on stdout")?;
eprintln!("Encryption successful.");

Ok(())
}

fn decrypt() -> Result<()> {
let mut input = Vec::new();
io::stdin().read_to_end(&mut input)?;
let input = std::str::from_utf8(&input).context("Input is not valid UTF-8")?;

let hdr = josekit::jwt::decode_header(&input).context("Error decoding header")?;
let hdr_clevis = hdr.claim("clevis").context("Error getting clevis claim")?;
let hdr_clevis: ClevisHeader =
serde_json::from_value(hdr_clevis.clone()).context("Error deserializing clevis header")?;

eprintln!("Decrypt with header: {:?}", hdr_clevis);

let decrypter_jwk = fetch_and_prepare_jwk(
&hdr_clevis.servers,
&hdr_clevis.path,
)?;

let decrypter = Dir
.decrypter_from_jwk(&decrypter_jwk)
.context("Error creating decrypter")?;

let (payload, _) =
josekit::jwe::deserialize_compact(&input, &decrypter).context("Error decrypting JWE")?;

io::stdout().write_all(&payload)?;

eprintln!("Decryption successful.");
Ok(())
}

fn fetch_luks_key(
servers: &[Server],
path: &str,
) -> Result<String> {
const MAX_ATTEMPTS: u32 = 3;
const DELAY: Duration = Duration::from_secs(5);
Comment on lines +127 to +128
Copy link
Member

Choose a reason for hiding this comment

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

This is fine for now but we'll need to make this either bigger or configurable.


if servers.is_empty() {
return Err(anyhow!("No URLs provided"));
}

(1..=MAX_ATTEMPTS)
.find_map(|attempt| {
eprintln!(
"Attempting to fetch LUKS key (attempt {}/{})",
attempt, MAX_ATTEMPTS
);

for (index, server) in servers.iter().enumerate() {
eprintln!("Trying URL {}/{}: {}", index + 1, servers.len(), server.url);
match try_fetch_luks_key(&server.url, path) {
Ok(key) => {
eprintln!("Successfully fetched LUKS key from URL: {}", server.url);
return Some(Ok(key));
}
Err(e) => {
eprintln!("Error with URL {}: {}", server.url, e);
}
}
}

if attempt < MAX_ATTEMPTS {
eprintln!("All URLs failed for attempt {}. Retrying in {:?} seconds...", attempt, DELAY);
thread::sleep(DELAY);
}
None
})
.unwrap_or_else(|| {
Err(anyhow!(
"Failed to fetch the LUKS key from all URLs after {} attempts",
MAX_ATTEMPTS
))
})
}

fn try_fetch_luks_key(
url: &str,
path: &str,
) -> Result<String> {
let output = StdCommand::new("trustee-attester")
.arg("--url")
.arg(url)
.arg("get-resource")
.arg("--path")
.arg(path)
.output()
.map_err(|e| anyhow!("Failed to execute trustee-attester: {}", e))?;

io::stderr().write_all(&output.stderr)?;
io::stderr().write_all(&output.stdout)?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("trustee-attester failed: {}", stderr));
}

let key = String::from_utf8(output.stdout)
.map_err(|e| anyhow!("Invalid UTF-8 for the LUKS key: {}", e))?
.trim()
.to_string();

if key.is_empty() {
return Err(anyhow!("Received empty LUKS key"));
}

Ok(key)
}

/// Clevis PIN for confidential cluster
#[derive(Parser)]
#[command(name = "clevis-pin-trustee")]
#[command(version = "0.1.0")]
#[command(about = "Clevis PIN for confidential clusters")]
struct Cli {
#[command(subcommand)]
command: Commands,
}

#[derive(Subcommand)]
enum Commands {
/// Encrypt data using the configuration
Encrypt {
/// Input data or arguments
config: String,
},
/// Decrypt the input data
Decrypt,
}

fn main() -> Result<()> {
let cli = Cli::parse();

match cli.command {
Commands::Encrypt { config } => encrypt(&config),
Commands::Decrypt => decrypt(),
}
}
1 change: 1 addition & 0 deletions test-secret-trustee
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "key_type": "oct", "key": "2b442dd5db4478367729ef8bbf2e7480" }
18 changes: 18 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash

set -euo pipefail

device=$(sudo losetup |grep test.img | awk '{print $1}'|head -n1 || true)
if [ -z "$device" ]; then
truncate test.img --size 1GB
device=$(sudo losetup -f --show test.img)
fi
echo "Device $device"
echo "cLevisTest1234" > key
sudo cryptsetup isLuks $device
if [ $? -ne 0 ]; then
yes "YES"| sudo cryptsetup luksFormat -d key --force-password $device
fi
sudo clevis luks bind -f -k key -d $device trustee "$(cat data.json)"
sudo clevis luks list -d $device
sudo clevis luks unlock -d $device -n myroot