chore: split-out vault code from teepot in teepot-vault

Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
This commit is contained in:
Harald Hoyer 2025-02-18 13:37:34 +01:00
parent 63c16b1177
commit f8bd9e6a08
Signed by: harald
GPG key ID: F519A1143B3FBE32
61 changed files with 450 additions and 308 deletions

View file

@ -0,0 +1,35 @@
[package]
name = "teepot-vault"
description = "TEE secret manager"
license.workspace = true
version.workspace = true
edition.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
actix-http = "3"
actix-web.workspace = true
anyhow.workspace = true
awc.workspace = true
bytes.workspace = true
clap.workspace = true
const-oid.workspace = true
futures-core = { version = "0.3.30", features = ["alloc"], default-features = false }
hex.workspace = true
intel-tee-quote-verification-rs.workspace = true
pgp.workspace = true
rustls.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_with.workspace = true
sha2.workspace = true
tdx-attest-rs.workspace = true
teepot.workspace = true
thiserror.workspace = true
tracing.workspace = true
webpki-roots = "0.26.1"
x509-cert.workspace = true
[dev-dependencies]
base64.workspace = true

View file

@ -0,0 +1,19 @@
[package]
name = "tee-stress-client"
publish = false
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
actix-web.workspace = true
anyhow.workspace = true
clap.workspace = true
serde.workspace = true
teepot.workspace = true
teepot-vault.workspace = true
tracing.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1,105 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2025 Matter Labs
//! Server to handle requests to the Vault TEE
#![deny(missing_docs)]
#![deny(clippy::all)]
use actix_web::rt::time::sleep;
use anyhow::{Context, Result};
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
use teepot_vault::{
client::vault::VaultConnection,
server::{
attestation::{get_quote_and_collateral, VaultAttestationArgs},
pki::make_self_signed_cert,
},
};
use tracing::{error, trace};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Arguments {
/// allowed TCB levels, comma separated
#[arg(long, value_parser = parse_tcb_levels, env = "ALLOWED_TCB_LEVELS", default_value = "Ok")]
my_sgx_allowed_tcb_levels: EnumSet<TcbLevel>,
#[clap(flatten)]
pub attestation: VaultAttestationArgs,
}
#[derive(Debug, Serialize, Deserialize)]
struct MySecret {
val: usize,
}
#[actix_web::main]
async fn main() -> Result<()> {
LogTracer::init().context("Failed to set logger")?;
let subscriber = Registry::default()
.with(EnvFilter::from_default_env())
.with(fmt::layer().with_writer(std::io::stderr));
tracing::subscriber::set_global_default(subscriber).unwrap();
let args = Arguments::parse();
let (report_data, _cert_chain, _priv_key) = make_self_signed_cert("CN=localhost", None)?;
if let Err(e) = get_quote_and_collateral(Some(args.my_sgx_allowed_tcb_levels), &report_data) {
error!("failed to get quote and collateral: {e:?}");
// don't return for now, we can still serve requests but we won't be able to attest
}
let mut vault_1 = args.attestation.clone();
let mut vault_2 = args.attestation.clone();
let mut vault_3 = args.attestation.clone();
vault_1.vault_addr = "https://vault-1:8210".to_string();
vault_2.vault_addr = "https://vault-2:8210".to_string();
vault_3.vault_addr = "https://vault-3:8210".to_string();
let servers = vec![vault_1.clone(), vault_2.clone(), vault_3.clone()];
let mut val: usize = 1;
loop {
let mut conns = Vec::new();
for server in &servers {
match VaultConnection::new(&server.into(), "stress".to_string()).await {
Ok(conn) => conns.push(conn),
Err(e) => {
error!("connecting to {}: {}", server.vault_addr, e);
continue;
}
}
}
if conns.is_empty() {
error!("no connections");
sleep(Duration::from_secs(1)).await;
continue;
}
let i = val % conns.len();
trace!("storing secret");
conns[i]
.store_secret(MySecret { val }, "val")
.await
.context("storing secret")?;
for conn in conns {
let got: MySecret = conn
.load_secret("val")
.await
.context("loading secret")?
.context("loading secret")?;
assert_eq!(got.val, val,);
}
val += 1;
sleep(Duration::from_secs(1)).await;
}
}

View file

@ -0,0 +1,66 @@
libos.entrypoint = "/app/tee-stress-client"
[loader]
argv = [ "/app/tee-stress-client" ]
entrypoint = "file:{{ gramine.libos }}"
env.LD_LIBRARY_PATH = "/lib:{{ arch_libdir }}:/usr{{ arch_libdir }}:/lib"
env.HOME = "/app"
env.MALLOC_ARENA_MAX = "1"
env.AZDCAP_DEBUG_LOG_LEVEL = "ignore"
env.AZDCAP_COLLATERAL_VERSION = "v4"
### Admin Config ###
env.PORT = { passthrough = true }
### VAULT attestation ###
env.VAULT_ADDR = { passthrough = true }
env.VAULT_SGX_MRENCLAVE = { passthrough = true }
env.VAULT_SGX_MRSIGNER = { passthrough = true }
env.VAULT_SGX_ALLOWED_TCB_LEVELS = { passthrough = true }
### DEBUG ###
env.RUST_BACKTRACE = "1"
env.RUST_LOG="info"
[fs]
root.uri = "file:/"
start_dir = "/app"
mounts = [
{ path = "{{ execdir }}", uri = "file:{{ execdir }}" },
{ path = "/lib", uri = "file:{{ gramine.runtimedir() }}" },
{ path = "{{ arch_libdir }}", uri = "file:{{ arch_libdir }}" },
{ path = "/etc", uri = "file:/etc" },
{ type = "tmpfs", path = "/var/tmp" },
{ type = "tmpfs", path = "/tmp" },
{ type = "tmpfs", path = "/app/.dcap-qcnl" },
{ type = "tmpfs", path = "/app/.az-dcap-client" },
{ path = "/lib/libdcap_quoteprov.so", uri = "file:/lib/libdcap_quoteprov.so" },
]
[sgx]
trusted_files = [
"file:/etc/ld.so.cache",
"file:/app/",
"file:{{ execdir }}/",
"file:{{ arch_libdir }}/",
"file:/usr/{{ arch_libdir }}/",
"file:{{ gramine.libos }}",
"file:{{ gramine.runtimedir() }}/",
"file:/usr/lib/ssl/openssl.cnf",
"file:/etc/ssl/",
"file:/etc/sgx_default_qcnl.conf",
"file:/lib/libdcap_quoteprov.so",
]
remote_attestation = "dcap"
max_threads = 64
edmm_enable = false
## max enclave size
enclave_size = "8G"
[sys]
enable_extra_runtime_domain_names_conf = true
enable_sigterm_injection = true
# possible tweak option, if problems with mio
# currently mio is compiled with `mio_unsupported_force_waker_pipe`
# insecure__allow_eventfd = true

View file

@ -0,0 +1,25 @@
[package]
name = "tee-vault-admin"
publish = false
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
actix-web.workspace = true
anyhow.workspace = true
awc.workspace = true
bytemuck.workspace = true
clap.workspace = true
hex.workspace = true
rustls.workspace = true
serde_json.workspace = true
sha2.workspace = true
teepot.workspace = true
teepot-vault.workspace = true
tracing.workspace = true
tracing-actix-web.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1,89 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! post commands
use crate::ServerState;
use actix_web::web;
use anyhow::{anyhow, Context, Result};
use awc::http::StatusCode;
use sha2::{Digest, Sha256};
use std::sync::Arc;
use teepot_vault::client::vault::VaultConnection;
use teepot_vault::json::http::{
VaultCommandRequest, VaultCommandResponse, VaultCommands, VaultCommandsResponse,
};
use teepot_vault::json::secrets::{AdminConfig, AdminState};
use teepot_vault::server::{signatures::VerifySig, HttpResponseError, Status};
use tracing::instrument;
/// Post command
#[instrument(level = "info", name = "/v1/command", skip_all)]
pub async fn post_command(
state: web::Data<Arc<ServerState>>,
item: web::Json<VaultCommandRequest>,
) -> Result<web::Json<VaultCommandsResponse>, HttpResponseError> {
let conn = VaultConnection::new(&state.vault_attestation.clone().into(), "admin".to_string())
.await
.context("connecting to vault")
.status(StatusCode::BAD_GATEWAY)?;
let mut admin_state: AdminState = conn
.load_secret("state")
.await?
.context("empty admin state")
.status(StatusCode::BAD_GATEWAY)?;
let commands: VaultCommands = serde_json::from_str(&item.commands)
.context("parsing commands")
.status(StatusCode::BAD_REQUEST)?;
if admin_state.last_digest.to_ascii_lowercase() != commands.last_digest {
return Err(anyhow!(
"last digest does not match {} != {}",
admin_state.last_digest.to_ascii_lowercase(),
commands.last_digest
))
.status(StatusCode::BAD_REQUEST);
}
let admin_config: AdminConfig = conn
.load_secret("config")
.await?
.context("empty admin config")
.status(StatusCode::BAD_GATEWAY)?;
admin_config.check_sigs(&item.signatures, item.commands.as_bytes())?;
let mut hasher = Sha256::new();
hasher.update(item.commands.as_bytes());
let hash = hasher.finalize();
let digest = hex::encode(hash);
admin_state.last_digest.clone_from(&digest);
conn.store_secret(admin_state, "state").await?;
let mut responds = VaultCommandsResponse {
digest,
results: vec![],
};
for (pos, command) in commands.commands.iter().enumerate() {
let resp = conn
.vault_put(
&format!("Executing command {pos}"),
&command.url,
&command.data,
)
.await?;
let vcr = VaultCommandResponse {
status_code: resp.0.as_u16(),
value: resp.1,
};
responds.results.push(vcr);
}
let _ = conn.revoke_token().await;
Ok(web::Json(responds))
}

View file

@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! digest
use crate::ServerState;
use actix_web::{web, HttpResponse};
use anyhow::{Context, Result};
use awc::http::StatusCode;
use serde_json::json;
use std::sync::Arc;
use teepot_vault::client::vault::VaultConnection;
use teepot_vault::json::secrets::AdminState;
use teepot_vault::server::{HttpResponseError, Status};
use tracing::instrument;
/// Get last digest
#[instrument(level = "info", name = "/v1/digest", skip_all)]
pub async fn get_digest(
state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, HttpResponseError> {
let conn = VaultConnection::new(&state.vault_attestation.clone().into(), "admin".to_string())
.await
.context("connecting to vault")
.status(StatusCode::BAD_GATEWAY)?;
let admin_state: AdminState = conn
.load_secret("state")
.await?
.context("empty admin state")
.status(StatusCode::BAD_GATEWAY)?;
Ok(HttpResponse::Ok().json(json!({"last_digest": admin_state.last_digest })))
}

View file

@ -0,0 +1,147 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
//! Server to handle requests to the Vault TEE
#![deny(missing_docs)]
#![deny(clippy::all)]
mod command;
mod digest;
mod sign;
use actix_web::{web, web::Data, App, HttpServer};
use anyhow::{Context, Result};
use clap::Parser;
use command::post_command;
use digest::get_digest;
use rustls::ServerConfig;
use sign::post_sign;
use std::{net::Ipv6Addr, sync::Arc};
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
use teepot_vault::{
json::http::{SignRequest, VaultCommandRequest, DIGEST_URL},
server::{
attestation::{get_quote_and_collateral, VaultAttestationArgs},
new_json_cfg,
pki::make_self_signed_cert,
},
};
use tracing::{error, info};
use tracing_actix_web::TracingLogger;
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
/// Server state
pub struct ServerState {
/// Server TLS public key hash
pub report_data: [u8; 64],
/// Vault attestation args
pub vault_attestation: VaultAttestationArgs,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Arguments {
/// allowed TCB levels, comma separated
#[arg(long, value_parser = parse_tcb_levels, env = "ALLOWED_TCB_LEVELS", default_value = "Ok")]
server_sgx_allowed_tcb_levels: EnumSet<TcbLevel>,
/// port to listen on
#[arg(long, env = "PORT", default_value = "8444")]
port: u16,
#[clap(flatten)]
pub attestation: VaultAttestationArgs,
}
#[actix_web::main]
async fn main() -> Result<()> {
LogTracer::init().context("Failed to set logger")?;
let subscriber = Registry::default()
.with(EnvFilter::from_default_env())
.with(fmt::layer().with_writer(std::io::stderr));
tracing::subscriber::set_global_default(subscriber).unwrap();
let args = Arguments::parse();
let (report_data, cert_chain, priv_key) = make_self_signed_cert("CN=localhost", None)?;
if let Err(e) = get_quote_and_collateral(Some(args.server_sgx_allowed_tcb_levels), &report_data)
{
error!("failed to get quote and collateral: {e:?}");
// don't return for now, we can still serve requests but we won't be able to attest
}
let _ = rustls::crypto::ring::default_provider().install_default();
// init server config builder with safe defaults
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert([cert_chain].into(), priv_key)
.context("Failed to load TLS key/cert files")?;
info!("Starting HTTPS server at port {}", args.port);
info!("Quote verified! Connection secure!");
let server_state = Arc::new(ServerState {
report_data,
vault_attestation: args.attestation,
});
let server = match HttpServer::new(move || {
App::new()
// enable logger
.wrap(TracingLogger::default())
.app_data(new_json_cfg())
.app_data(Data::new(server_state.clone()))
.service(web::resource(VaultCommandRequest::URL).route(web::post().to(post_command)))
.service(web::resource(SignRequest::URL).route(web::post().to(post_sign)))
.service(web::resource(DIGEST_URL).route(web::get().to(get_digest)))
})
.bind_rustls_0_23((Ipv6Addr::UNSPECIFIED, args.port), config)
{
Ok(c) => c,
Err(e) => {
error!("Failed to bind to port {}: {e:?}", args.port);
return Err(e).context(format!("Failed to bind to port {}", args.port));
}
};
if let Err(e) = server.worker_max_blocking_threads(2).workers(8).run().await {
error!("failed to start HTTPS server: {e:?}");
return Err(e).context("Failed to start HTTPS server");
}
Ok(())
}
#[cfg(test)]
mod tests {
use serde_json::json;
use teepot_vault::json::http::{VaultCommand, VaultCommands};
const TEST_DATA: &str = include_str!("../../../tests/data/test.json");
#[test]
fn test_vault_commands() {
let cmd = VaultCommand {
url: "/v1/auth/tee/tees/test".to_string(),
data: json!({
"lease": "1000",
"name": "test",
"types": "sgx",
"sgx_allowed_tcb_levels": "Ok,SwHardeningNeeded",
"sgx_mrsigner": "c5591a72b8b86e0d8814d6e8750e3efe66aea2d102b8ba2405365559b858697d",
"token_policies": "test"
}),
};
let cmds = VaultCommands {
commands: vec![cmd],
last_digest: "".into(),
};
let test_data_cmds: VaultCommands = serde_json::from_str(TEST_DATA).unwrap();
assert_eq!(cmds, test_data_cmds);
}
}

View file

@ -0,0 +1,149 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! post signing request
use crate::ServerState;
use actix_web::http::StatusCode;
use actix_web::web;
use anyhow::{anyhow, Context, Result};
use sha2::{Digest, Sha256};
use std::sync::Arc;
use teepot::sgx::sign::PrivateKey as _;
use teepot::sgx::sign::{Author, Signature};
use teepot::sgx::sign::{Body, RS256PrivateKey};
use teepot_vault::client::vault::VaultConnection;
use teepot_vault::json::http::{SignRequest, SignRequestData, SignResponse};
use teepot_vault::json::secrets::{AdminConfig, AdminState, SGXSigningKey};
use teepot_vault::server::signatures::VerifySig as _;
use teepot_vault::server::{HttpResponseError, Status};
use tracing::instrument;
/// Sign command
#[instrument(level = "info", name = "/v1/sign", skip_all)]
pub async fn post_sign(
state: web::Data<Arc<ServerState>>,
item: web::Json<SignRequest>,
) -> Result<web::Json<SignResponse>, HttpResponseError> {
let conn = VaultConnection::new(&state.vault_attestation.clone().into(), "admin".to_string())
.await
.context("connecting to vault")
.status(StatusCode::BAD_GATEWAY)?;
let mut admin_state: AdminState = conn
.load_secret("state")
.await?
.context("empty admin state")
.status(StatusCode::BAD_GATEWAY)?;
let sign_request: SignRequestData = serde_json::from_str(&item.sign_request_data)
.context("parsing sign request data")
.status(StatusCode::BAD_REQUEST)?;
// Sanity checks
if sign_request.tee_type != "sgx" {
return Err(anyhow!("tee_type not supported")).status(StatusCode::BAD_REQUEST);
}
let tee_name = sign_request.tee_name;
if !tee_name.is_ascii() {
return Err(anyhow!("tee_name is not ascii")).status(StatusCode::BAD_REQUEST);
}
// check if tee_name is alphanumeric
if !tee_name.chars().all(|c| c.is_alphanumeric()) {
return Err(anyhow!("tee_name is not alphanumeric")).status(StatusCode::BAD_REQUEST);
}
// check if tee_name starts with an alphabetic char
if !tee_name.chars().next().unwrap().is_alphabetic() {
return Err(anyhow!("tee_name does not start with an alphabetic char"))
.status(StatusCode::BAD_REQUEST);
}
if admin_state.last_digest != sign_request.last_digest {
return Err(anyhow!(
"last digest does not match {} != {}",
admin_state.last_digest.to_ascii_lowercase(),
sign_request.last_digest
))
.status(StatusCode::BAD_REQUEST);
}
let admin_config: AdminConfig = conn
.load_secret("config")
.await?
.context("empty admin config")
.status(StatusCode::BAD_GATEWAY)?;
admin_config.check_sigs(&item.signatures, item.sign_request_data.as_bytes())?;
let mut hasher = Sha256::new();
hasher.update(item.sign_request_data.as_bytes());
let hash = hasher.finalize();
let digest = hex::encode(hash);
admin_state.last_digest.clone_from(&digest);
conn.store_secret(admin_state, "state").await?;
// Sign SGX enclave
let key_path = format!("signing_keys/{}", tee_name);
let sgx_key = match conn
.load_secret::<SGXSigningKey>(&key_path)
.await
.context("Error loading signing key")
.status(StatusCode::INTERNAL_SERVER_ERROR)?
{
Some(key) => RS256PrivateKey::from_pem(&key.pem_pk)
.context("Failed to parse private key")
.status(StatusCode::INTERNAL_SERVER_ERROR)?,
None => {
let private_key = RS256PrivateKey::generate(3)
.context("Failed to generate private key")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
let pem_pk = private_key
.to_pem()
.context("Failed to convert private key to pem")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
let key = SGXSigningKey { pem_pk };
conn.store_secret(key.clone(), &key_path)
.await
.context("Error storing generated private key")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
private_key
}
};
let signed_data = sign_sgx(&sign_request.data, &sgx_key)?;
let respond = SignResponse {
digest,
signed_data,
};
let _ = conn.revoke_token().await;
Ok(web::Json(respond))
}
fn sign_sgx(body_bytes: &[u8], sgx_key: &RS256PrivateKey) -> Result<Vec<u8>, HttpResponseError> {
let body: Body = bytemuck::try_pod_read_unaligned(body_bytes)
.context("Invalid SGX input data")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
if body.can_set_debug() {
return Err(anyhow!("Not signing SGX enclave with debug flag"))
.status(StatusCode::BAD_REQUEST);
}
// FIXME: do we need the date and sw defined value?
let author = Author::new(0, 0);
let sig = Signature::new(sgx_key, author, body)
.context("Failed to create RSA signature")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(bytemuck::bytes_of(&sig).to_vec())
}

View file

@ -0,0 +1,21 @@
[package]
name = "tee-vault-unseal"
publish = false
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
actix-web.workspace = true
anyhow.workspace = true
awc.workspace = true
clap.workspace = true
rustls.workspace = true
serde_json.workspace = true
teepot.workspace = true
teepot-vault.workspace = true
tracing.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1,80 @@
# Read system health check
path "sys/health"
{
capabilities = ["read", "sudo"]
}
# Create and manage ACL policies broadly across Vault
# List existing policies
path "sys/policies/acl"
{
capabilities = ["list"]
}
# Create and manage ACL policies
path "sys/policies/acl/*"
{
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# Enable and manage authentication methods broadly across Vault
# Manage auth methods broadly across Vault
path "auth/*"
{
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# Create, update, and delete auth methods
path "sys/auth/*"
{
capabilities = ["create", "update", "delete", "sudo"]
}
# List auth methods
path "sys/auth"
{
capabilities = ["read"]
}
# Enable and manage the key/value secrets engine at `secret/` path
# List, create, update, and delete key/value secrets
path "secret/*"
{
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# Manage secrets engines
path "sys/mounts/*"
{
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# List existing secrets engines.
path "sys/mounts"
{
capabilities = ["read"]
}
# Manage plugins
# https://developer.hashicorp.com/vault/api-docs/system/plugins-catalog
path "sys/plugins/catalog/*"
{
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# List existing plugins
# https://developer.hashicorp.com/vault/api-docs/system/plugins-catalog
path "sys/plugins/catalog"
{
capabilities = ["list"]
}
# Reload plugins
# https://developer.hashicorp.com/vault/api-docs/system/plugins-reload-backend
path "sys/plugins/reload/backend"
{
capabilities = ["create", "update", "sudo"]
}

View file

@ -0,0 +1,137 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use crate::{get_vault_status, UnsealServerState, Worker};
use actix_web::error::ErrorBadRequest;
use actix_web::{web, HttpResponse};
use anyhow::{anyhow, Context, Result};
use awc::http::StatusCode;
use serde_json::json;
use teepot_vault::client::TeeConnection;
use teepot_vault::json::http::{Init, InitResponse, VaultInitRequest};
use teepot_vault::json::secrets::AdminConfig;
use teepot_vault::server::{HttpResponseError, Status};
use tracing::{debug, error, info, instrument, trace};
#[instrument(level = "info", name = "/v1/sys/init", skip_all)]
pub async fn post_init(
worker: web::Data<Worker>,
init: web::Json<Init>,
) -> Result<HttpResponse, HttpResponseError> {
let Init {
pgp_keys,
secret_shares,
secret_threshold,
admin_pgp_keys,
admin_threshold,
admin_tee_mrenclave,
} = init.into_inner();
let conn = TeeConnection::new(&worker.vault_attestation);
let client = conn.client();
let vault_url = &worker.config.vault_url;
let vault_init = VaultInitRequest {
pgp_keys,
secret_shares,
secret_threshold,
};
if admin_threshold < 1 {
return Ok(HttpResponse::from_error(ErrorBadRequest(
json!({"error": "admin_threshold must be at least 1"}),
)));
}
if admin_threshold > admin_pgp_keys.len() {
return Ok(HttpResponse::from_error(ErrorBadRequest(
json!({"error": "admin_threshold must be less than or equal to the number of admin_pgp_keys"}),
)));
}
loop {
let current_state = worker.state.read().unwrap().clone();
match current_state {
UnsealServerState::VaultUninitialized => {
break;
}
UnsealServerState::VaultUnsealed => {
return Err(anyhow!("Vault already unsealed")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::VaultInitialized { .. } => {
return Err(anyhow!("Vault already initialized")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::VaultInitializedAndConfigured => {
return Err(anyhow!("Vault already initialized")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::Undefined => {
let state = get_vault_status(vault_url, client).await;
*worker.state.write().unwrap() = state;
continue;
}
}
}
trace!(
"Sending init request to Vault {}",
serde_json::to_string(&vault_init).unwrap()
);
let mut response = client
.post(format!("{}/v1/sys/init", vault_url))
.send_json(&vault_init)
.await?;
let status_code = response.status();
if !status_code.is_success() {
error!("Vault returned server error: {}", status_code);
return Err(HttpResponseError::from_proxy(response).await);
}
let response = response
.json::<serde_json::Value>()
.await
.context("Failed to convert to json")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
info!("Vault initialized");
trace!("response {}", response);
let root_token = response["root_token"]
.as_str()
.ok_or(anyhow!("No `root_token` field"))
.status(StatusCode::BAD_GATEWAY)?
.to_string();
debug!("Root token: {root_token}");
let unseal_keys = response["keys_base64"]
.as_array()
.ok_or(anyhow!("No `keys_base64` field"))
.status(StatusCode::BAD_GATEWAY)?
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect::<Vec<_>>();
debug!("Unseal keys: {}", unseal_keys.join(", "));
/*
FIXME: use unseal keys to create new token
let mut output = File::create("/opt/vault/data/root_token")
.context("Failed to create `/opt/vault/data/root_token`")?;
output
.write_all(root_token.as_bytes())
.context("Failed to write root_token")?;
*/
*worker.state.write().unwrap() = UnsealServerState::VaultInitialized {
admin_config: AdminConfig {
admin_pgp_keys,
admin_threshold,
},
admin_tee_mrenclave,
root_token,
};
let response = InitResponse { unseal_keys };
Ok(HttpResponse::Ok().json(response)) // <- send response
}

View file

@ -0,0 +1,236 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
//! Server to initialize and unseal the Vault TEE.
#![deny(missing_docs)]
#![deny(clippy::all)]
mod init;
mod unseal;
use actix_web::{rt::time::sleep, web, web::Data, App, HttpServer};
use anyhow::{bail, Context, Result};
use awc::Client;
use clap::Parser;
use init::post_init;
use rustls::ServerConfig;
use std::fmt::Debug;
use std::io::Read;
use std::net::Ipv6Addr;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use teepot::pki::make_self_signed_cert;
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
use teepot_vault::client::{AttestationArgs, TeeConnection};
use teepot_vault::json::http::{Init, Unseal};
use teepot_vault::json::secrets::AdminConfig;
use teepot_vault::server::attestation::{get_quote_and_collateral, VaultAttestationArgs};
use teepot_vault::server::new_json_cfg;
use tracing::{error, info};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
use unseal::post_unseal;
const VAULT_TOKEN_HEADER: &str = "X-Vault-Token";
/// Worker thread state and data
#[derive(Debug, Clone)]
pub struct Worker {
/// TLS config for the HTTPS client
pub vault_attestation: Arc<AttestationArgs>,
/// Server config
pub config: Arc<UnsealServerConfig>,
/// Server state
pub state: Arc<RwLock<UnsealServerState>>,
}
/// Global Server config
#[derive(Debug, Default)]
pub struct UnsealServerConfig {
/// Vault URL
pub vault_url: String,
/// The expected report_data for the Vault TEE
pub report_data: Box<[u8]>,
/// allowed TCB levels
pub allowed_tcb_levels: Option<EnumSet<TcbLevel>>,
/// SHA256 of the vault_auth_tee plugin binary
pub vault_auth_tee_sha: String,
/// version string of the vault_auth_tee plugin
pub vault_auth_tee_version: String,
/// the common cacert file for the vault cluster
pub ca_cert_file: PathBuf,
}
/// Server state
#[derive(Debug, Clone)]
pub enum UnsealServerState {
/// Undefined
Undefined,
/// Vault is not yet initialized
VaultUninitialized,
/// Vault is initialized but not unsealed
VaultInitialized {
/// config for the admin TEE
admin_config: AdminConfig,
/// initial admin TEE mrenclave
admin_tee_mrenclave: String,
/// Vault root token
root_token: String,
},
/// Vault is already initialized but not unsealed
/// and should already be configured
VaultInitializedAndConfigured,
/// Vault is unsealed
VaultUnsealed,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// allowed TCB levels, comma separated
#[arg(long, value_parser = parse_tcb_levels, env = "ALLOWED_TCB_LEVELS", default_value = "Ok")]
allowed_tcb_levels: EnumSet<TcbLevel>,
/// port to listen on
#[arg(long, env = "PORT", default_value = "8443")]
port: u16,
/// the sha256 of the `vault_auth_tee` plugin, with precedence over the file
#[arg(long, env = "VAULT_AUTH_TEE_SHA256")]
vault_auth_tee_sha: Option<String>,
/// the file containing the sha256 of the `vault_auth_tee` plugin
#[arg(long, env = "VAULT_AUTH_TEE_SHA256_FILE")]
vault_auth_tee_sha_file: Option<PathBuf>,
#[arg(long, env = "VAULT_AUTH_TEE_VERSION")]
vault_auth_tee_version: String,
/// ca cert file
#[arg(long, env = "CA_CERT_FILE", default_value = "/opt/vault/cacert.pem")]
ca_cert_file: PathBuf,
#[clap(flatten)]
pub attestation: VaultAttestationArgs,
}
#[actix_web::main]
async fn main() -> Result<()> {
LogTracer::init().context("Failed to set logger")?;
let subscriber = Registry::default()
.with(EnvFilter::from_default_env())
.with(
fmt::layer()
.with_span_events(fmt::format::FmtSpan::NEW)
.with_writer(std::io::stderr),
);
tracing::subscriber::set_global_default(subscriber).unwrap();
let args = Args::parse();
info!("Starting up");
if let Err(e) = get_quote_and_collateral(Some(args.allowed_tcb_levels), &[0u8; 64]) {
error!("failed to get quote and collateral: {e:?}");
// don't return for now, we can still serve requests but we won't be able to attest
}
let (report_data, cert_chain, priv_key) = make_self_signed_cert("CN=localhost", None)?;
let _ = rustls::crypto::ring::default_provider().install_default();
// init server config builder with safe defaults
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert([cert_chain].into(), priv_key)
.context("Failed to load TLS key/cert files")?;
let attestation_args: AttestationArgs = args.attestation.clone().into();
let conn = TeeConnection::new(&attestation_args);
let server_state = get_vault_status(&args.attestation.vault_addr, conn.client()).await;
let vault_auth_tee_sha = if let Some(vault_auth_tee_sha) = args.vault_auth_tee_sha {
vault_auth_tee_sha
} else if let Some(sha_file) = args.vault_auth_tee_sha_file {
let mut file = std::fs::File::open(sha_file)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
contents.trim_end().into()
} else {
bail!("Neither `VAULT_AUTH_TEE_SHA256_FILE` nor `VAULT_AUTH_TEE_SHA256` set!");
};
info!("Starting HTTPS server at port {}", args.port);
let server_config = Arc::new(UnsealServerConfig {
vault_url: args.attestation.vault_addr,
report_data: Box::from(report_data),
allowed_tcb_levels: Some(args.allowed_tcb_levels),
vault_auth_tee_sha,
vault_auth_tee_version: args.vault_auth_tee_version,
ca_cert_file: args.ca_cert_file,
});
let server_state = Arc::new(RwLock::new(server_state));
let worker = Worker {
vault_attestation: Arc::new(attestation_args),
config: server_config,
state: server_state,
};
let server = match HttpServer::new(move || {
App::new()
// enable logger
//.wrap(TracingLogger::default())
.app_data(new_json_cfg())
.app_data(Data::new(worker.clone()))
.service(web::resource(Init::URL).route(web::post().to(post_init)))
.service(web::resource(Unseal::URL).route(web::post().to(post_unseal)))
})
.bind_rustls_0_23((Ipv6Addr::UNSPECIFIED, args.port), config)
{
Ok(c) => c,
Err(e) => {
error!("Failed to bind to port {}: {e:?}", args.port);
return Err(e).context(format!("Failed to bind to port {}", args.port));
}
};
if let Err(e) = server.worker_max_blocking_threads(2).workers(8).run().await {
error!("failed to start HTTPS server: {e:?}");
return Err(e).context("Failed to start HTTPS server");
}
Ok(())
}
async fn get_vault_status(vault_url: &str, client: &Client) -> UnsealServerState {
loop {
let r = client
.get(format!("{}/v1/sys/health", vault_url))
.send()
.await;
if let Ok(r) = r {
// https://developer.hashicorp.com/vault/api-docs/system/health
match r.status().as_u16() {
200 | 429 | 472 | 473 => {
info!("Vault is initialized and unsealed");
break UnsealServerState::VaultUnsealed;
}
501 => {
info!("Vault is not initialized");
break UnsealServerState::VaultUninitialized;
}
503 => {
info!("Vault is initialized but not unsealed");
break UnsealServerState::VaultInitializedAndConfigured;
}
s => {
error!("Vault is not ready: status code {s}");
}
}
}
info!("Waiting for vault to be ready");
sleep(Duration::from_secs(1)).await;
}
}

View file

@ -0,0 +1,409 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use crate::{get_vault_status, UnsealServerConfig, UnsealServerState, Worker, VAULT_TOKEN_HEADER};
use actix_web::http::StatusCode;
use actix_web::rt::time::sleep;
use actix_web::{web, HttpResponse};
use anyhow::{anyhow, Context, Result};
use awc::{Client, ClientRequest, SendClientRequest};
use serde_json::{json, Value};
use std::fs::File;
use std::future::Future;
use std::io::Read;
use std::time::Duration;
use teepot_vault::client::vault::VaultConnection;
use teepot_vault::client::TeeConnection;
use teepot_vault::json::http::Unseal;
use teepot_vault::json::secrets::{AdminConfig, AdminState};
use teepot_vault::server::{HttpResponseError, Status};
use tracing::{debug, error, info, instrument, trace};
#[instrument(level = "info", name = "/v1/sys/unseal", skip_all)]
pub async fn post_unseal(
worker: web::Data<Worker>,
item: web::Json<Unseal>,
) -> Result<HttpResponse, HttpResponseError> {
let conn = TeeConnection::new(&worker.vault_attestation);
let client = conn.client();
let app = &worker.config;
let vault_url = &app.vault_url;
loop {
let current_state = worker.state.read().unwrap().clone();
match current_state {
UnsealServerState::VaultUninitialized => {
return Err(anyhow!("Vault not yet initialized")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::VaultUnsealed => {
return Err(anyhow!("Vault already unsealed")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::VaultInitialized { .. } => {
break;
}
UnsealServerState::VaultInitializedAndConfigured => {
break;
}
UnsealServerState::Undefined => {
let state = get_vault_status(vault_url, client).await;
*worker.state.write().unwrap() = state;
continue;
}
}
}
let mut response = client
.post(format!("{}/v1/sys/unseal", vault_url))
.send_json(&item.0)
.await?;
let status_code = response.status();
if !status_code.is_success() {
error!("Vault returned server error: {}", status_code);
let mut client_resp = HttpResponse::build(status_code);
for (header_name, header_value) in response.headers().iter() {
client_resp.insert_header((header_name.clone(), header_value.clone()));
}
return Ok(client_resp.streaming(response));
}
let response: Value = response
.json()
.await
.context("parsing unseal response")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
debug!("unseal: {:?}", response);
if response.get("errors").is_some() {
return Ok(HttpResponse::Ok().json(response));
}
let sealed = response
.get("sealed")
.map(|v| v.as_bool().unwrap_or(true))
.unwrap_or(true);
debug!(sealed);
// if unsealed
if !sealed {
let mut state = UnsealServerState::VaultUnsealed;
std::mem::swap(&mut *worker.state.write().unwrap(), &mut state);
match state {
UnsealServerState::VaultUninitialized => {
return Err(anyhow!("Invalid internal state")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::VaultUnsealed => {
return Err(anyhow!("Invalid internal state")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::VaultInitialized {
admin_config,
admin_tee_mrenclave,
root_token,
} => {
debug!(root_token);
info!("Vault is unsealed");
vault_configure_unsealed(
app,
&admin_config,
&root_token,
&admin_tee_mrenclave,
client,
)
.await
.context("Failed to configure unsealed vault")
.status(StatusCode::BAD_GATEWAY)?;
// destroy root token
let _response = client
.post(format!("{}/v1/auth/token/revoke-self", app.vault_url))
.insert_header((VAULT_TOKEN_HEADER, root_token.to_string()))
.send()
.await;
info!("Vault unsealed and configured!");
}
UnsealServerState::VaultInitializedAndConfigured => {
info!("Vault is unsealed and hopefully configured!");
info!("Initiating raft join");
// load TLS cert chain
let mut cert_file = File::open(&app.ca_cert_file)
.context("Failed to open TLS cert chain")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
let mut cert_buf = Vec::new();
cert_file
.read_to_end(&mut cert_buf)
.context("Failed to read TLS cert chain")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
let cert_chain = std::str::from_utf8(&cert_buf)
.context("Failed to parse TLS cert chain as UTF-8")
.status(StatusCode::INTERNAL_SERVER_ERROR)?
.to_string();
let payload = json!({"leader_ca_cert": cert_chain, "retry": true });
let mut response = client
.post(format!("{}/v1/sys/storage/raft/join", vault_url))
.send_json(&payload)
.await?;
let status_code = response.status();
if !status_code.is_success() {
error!("Vault returned server error: {}", status_code);
let mut client_resp = HttpResponse::build(status_code);
for (header_name, header_value) in response.headers().iter() {
client_resp.insert_header((header_name.clone(), header_value.clone()));
}
return Ok(client_resp.streaming(response));
}
let response: Value = response
.json()
.await
.context("parsing raft join response")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
debug!("raft join: {:?}", response);
if response.get("errors").is_some() {
return Ok(HttpResponse::Ok().json(response));
}
}
UnsealServerState::Undefined => {
unreachable!("Invalid internal state");
}
}
}
Ok(HttpResponse::Accepted().json(response)) // <- send response
}
pub async fn vault_configure_unsealed(
app: &UnsealServerConfig,
admin_config: &AdminConfig,
root_token: &str,
admin_tee_mrenclave: &str,
c: &Client,
) -> Result<(), HttpResponseError> {
wait_for_plugins_catalog(app, root_token, c).await;
if !plugin_is_already_running(app, root_token, c).await? {
let r = vault(
"Installing vault-auth-tee plugin",
c.put(format!(
"{}/v1/sys/plugins/catalog/auth/vault-auth-tee",
app.vault_url
)),
root_token,
json!({
"sha256": app.vault_auth_tee_sha,
"command": "vault-auth-tee",
"version": app.vault_auth_tee_version
}),
)
.await
.map_err(|e| anyhow!("{:?}", e))
.status(StatusCode::BAD_GATEWAY)?;
if !r.status().is_success() {
let err = HttpResponseError::from_proxy(r).await;
return Err(err);
}
} else {
info!("vault-auth-tee plugin already installed");
}
if !plugin_is_already_running(app, root_token, c).await? {
let r = vault(
"Activating vault-auth-tee plugin",
c.post(format!("{}/v1/sys/auth/tee", app.vault_url)),
root_token,
json!({"type": "vault-auth-tee"}),
)
.await
.map_err(|e| anyhow!("{:?}", e))
.status(StatusCode::BAD_GATEWAY)?;
if !r.status().is_success() {
let err = HttpResponseError::from_proxy(r).await;
return Err(err);
}
} else {
info!("vault-auth-tee plugin already activated");
}
if let Ok(mut r) = c
.get(format!("{}/v1/auth/tee/tees?list=true", app.vault_url))
.insert_header((VAULT_TOKEN_HEADER, root_token))
.send()
.await
{
let r: Value = r
.json()
.await
.map_err(|e| anyhow!("{:?}", e))
.status(StatusCode::BAD_GATEWAY)?;
trace!("{:?}", r);
if let Some(tees) = r.get("data").and_then(|v| v.get("keys")) {
if let Some(tees) = tees.as_array() {
if tees.contains(&json!("root")) {
info!("root TEE already installed");
return Ok(());
}
}
}
}
vault(
"Installing root TEE",
c.put(format!("{}/v1/auth/tee/tees/admin", app.vault_url)),
root_token,
json!({
"lease": "1000",
"name": "admin",
"types": "sgx",
"sgx_allowed_tcb_levels": "Ok,SwHardeningNeeded",
"sgx_mrenclave": &admin_tee_mrenclave,
"token_policies": "admin"
}),
)
.await
.map_err(|e| anyhow!("{:?}", e))
.status(StatusCode::BAD_GATEWAY)?;
// Install admin policies
let admin_policy = include_str!("admin-policy.hcl");
vault(
"Installing admin policy",
c.put(format!("{}/v1/sys/policies/acl/admin", app.vault_url)),
root_token,
json!({ "policy": admin_policy }),
)
.await
.map_err(|e| anyhow!("{:?}", e))
.status(StatusCode::BAD_GATEWAY)?;
vault(
"Enable the key/value secrets engine v1 at secret/.",
c.put(format!("{}/v1/sys/mounts/secret", app.vault_url)),
root_token,
json!({ "type": "kv", "description": "K/V v1" } ),
)
.await
.map_err(|e| anyhow!("{:?}", e))
.status(StatusCode::BAD_GATEWAY)?;
// Create a `VaultConnection` for the `admin` tee to initialize the secrets for it.
// Safety: the connection was already attested
let admin_vcon = unsafe {
VaultConnection::new_from_client_without_attestation(
app.vault_url.clone(),
c.clone(),
"admin".into(),
root_token.to_string(),
)
};
// initialize the admin config
admin_vcon.store_secret(admin_config, "config").await?;
admin_vcon
.store_secret(AdminState::default(), "state")
.await?;
Ok(())
}
async fn wait_for_plugins_catalog(app: &UnsealServerConfig, root_token: &str, c: &Client) {
info!("Waiting for plugins to be loaded");
loop {
let r = c
.get(format!("{}/v1/sys/plugins/catalog", app.vault_url))
.insert_header((VAULT_TOKEN_HEADER, root_token))
.send()
.await;
match r {
Ok(r) => {
if r.status().is_success() {
break;
} else {
debug!("/v1/sys/plugins/catalog status: {:#?}", r)
}
}
Err(e) => {
debug!("/v1/sys/plugins/catalog error: {}", e)
}
}
info!("Waiting for plugins to be loaded");
sleep(Duration::from_secs(1)).await;
}
}
async fn plugin_is_already_running(
app: &UnsealServerConfig,
root_token: &str,
c: &Client,
) -> std::result::Result<bool, HttpResponseError> {
if let Ok(mut r) = c
.get(format!("{}/v1/sys/auth", app.vault_url))
.insert_header((VAULT_TOKEN_HEADER, root_token))
.send()
.await
{
if !r.status().is_success() {
return Ok(false);
}
let r: Value = r
.json()
.await
.map_err(|e| anyhow!("{:?}", e))
.status(StatusCode::BAD_GATEWAY)?;
trace!("{}", r.to_string());
let is_running = r
.get("data")
.and_then(|v| v.get("tee/"))
.and_then(|v| v.get("running_sha256"))
.and_then(|v| v.as_str())
.and_then(|v| if v.is_empty() { None } else { Some(v) })
.and_then(|v| {
if v == app.vault_auth_tee_sha {
Some(v)
} else {
None
}
})
.is_some();
Ok(is_running)
} else {
Ok(false)
}
}
async fn vault(
action: &str,
req: ClientRequest,
token: &str,
json: Value,
) -> <SendClientRequest as Future>::Output {
info!("{}", action);
debug!("json: {:?}", json);
match req
.insert_header((VAULT_TOKEN_HEADER, token))
.send_json(&json)
.await
{
Ok(r) => {
debug!("response {:?}", r);
Ok(r)
}
Err(e) => {
error!("{}: {}", action, e);
Err(e)
}
}
}

View file

@ -0,0 +1,21 @@
[package]
name = "teepot-read"
publish = false
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web.workspace = true
anyhow.workspace = true
awc.workspace = true
clap.workspace = true
serde_json.workspace = true
teepot-vault.workspace = true
tracing.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1,111 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! Get the secrets from a Vault TEE and pass them as environment variables to a command
#![deny(missing_docs)]
#![deny(clippy::all)]
use anyhow::{Context, Result};
use clap::Parser;
use serde_json::Value;
use std::collections::HashMap;
use std::os::unix::process::CommandExt;
use std::process::Command;
use teepot_vault::client::vault::VaultConnection;
use teepot_vault::server::attestation::VaultAttestationArgs;
use tracing::{debug, info, warn};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Arguments {
/// turn on test mode
#[arg(long, hide = true)]
pub test: bool,
/// vault token
#[arg(long, env = "VAULT_TOKEN", hide = true)]
pub vault_token: String,
#[clap(flatten)]
pub attestation: VaultAttestationArgs,
/// name of this TEE to login to vault
#[arg(long, required = true)]
pub name: String,
/// secrets to get from vault and pass as environment variables
#[arg(long, required = true)]
pub secrets: Vec<String>,
/// command to run
pub command: Vec<String>,
}
#[actix_web::main]
async fn main() -> Result<()> {
LogTracer::init().context("Failed to set logger")?;
let subscriber = Registry::default()
.with(EnvFilter::from_default_env())
.with(fmt::layer().with_writer(std::io::stderr));
tracing::subscriber::set_global_default(subscriber).unwrap();
let args = Arguments::parse();
// Split every string with a ',' into a vector of strings, flatten them and collect them.
let secrets = args
.secrets
.iter()
.flat_map(|s| s.split(','))
.collect::<Vec<_>>();
info!("args: {:?}", args);
let conn = if args.test {
warn!("TEST MODE");
let client = awc::Client::builder()
.add_default_header((actix_web::http::header::USER_AGENT, "teepot/1.0"))
.finish();
// SAFETY: TEST MODE
unsafe {
VaultConnection::new_from_client_without_attestation(
args.attestation.vault_addr.clone(),
client,
args.name.clone(),
args.vault_token.clone(),
)
}
} else {
VaultConnection::new(&args.attestation.clone().into(), args.name.clone())
.await
.expect("connecting to vault")
};
let mut env: HashMap<String, String> = HashMap::new();
for secret_name in secrets {
debug!("getting secret {secret_name}");
let secret_val: serde_json::Value = match conn.load_secret(secret_name).await? {
Some(val) => val,
None => {
debug!("secret {secret_name} not found");
continue;
}
};
debug!("got secret {secret_name}: {secret_val}");
// Plain strings can be converted to strings.
let env_val = match secret_val {
Value::String(s) => s,
_ => secret_val.to_string(),
};
env.insert(secret_name.to_string(), env_val);
}
let err = Command::new(&args.command[0])
.args(&args.command[1..])
.envs(env)
.exec();
Err(err).context("exec failed")
}

View file

@ -0,0 +1,21 @@
[package]
name = "teepot-write"
publish = false
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web.workspace = true
anyhow.workspace = true
awc.workspace = true
clap.workspace = true
serde_json.workspace = true
teepot-vault.workspace = true
tracing.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1,96 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! Write secrets to a Vault TEE from environment variables
#![deny(missing_docs)]
#![deny(clippy::all)]
use anyhow::{Context, Result};
use clap::Parser;
use serde_json::Value;
use std::{collections::HashMap, env};
use teepot_vault::{client::vault::VaultConnection, server::attestation::VaultAttestationArgs};
use tracing::{debug, info, warn};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Arguments {
/// turn on test mode
#[arg(long, hide = true)]
pub test: bool,
/// vault token
#[arg(long, env = "VAULT_TOKEN", hide = true)]
pub vault_token: String,
#[clap(flatten)]
pub attestation: VaultAttestationArgs,
/// name of this TEE to login to vault
#[arg(long, required = true)]
pub name: String,
/// name of this TEE to login to vault
#[arg(long)]
pub store_name: Option<String>,
/// secrets to write to vault with the value of the environment variables
#[arg(long, required = true)]
pub secrets: Vec<String>,
}
#[actix_web::main]
async fn main() -> Result<()> {
LogTracer::init().context("Failed to set logger")?;
let subscriber = Registry::default()
.with(EnvFilter::from_default_env())
.with(fmt::layer().with_writer(std::io::stderr));
tracing::subscriber::set_global_default(subscriber).unwrap();
let args = Arguments::parse();
// Split every string with a ',' into a vector of strings, flatten them and collect them.
let secrets = args
.secrets
.iter()
.flat_map(|s| s.split(','))
.collect::<Vec<_>>();
info!("args: {:?}", args);
let conn = if args.test {
warn!("TEST MODE");
let client = awc::Client::builder()
.add_default_header((actix_web::http::header::USER_AGENT, "teepot/1.0"))
.finish();
// SAFETY: TEST MODE
unsafe {
VaultConnection::new_from_client_without_attestation(
args.attestation.vault_addr.clone(),
client,
args.name.clone(),
args.vault_token.clone(),
)
}
} else {
VaultConnection::new(&args.attestation.clone().into(), args.name.clone())
.await
.expect("connecting to vault")
};
let tee_name = args.store_name.unwrap_or(args.name.clone());
let env = env::vars()
.filter(|(k, _)| secrets.contains(&k.as_str()))
.collect::<HashMap<_, _>>();
for (secret_name, secret_val) in env {
debug!("storing secret {secret_name}: {secret_val}");
let secret_val = Value::String(secret_val);
conn.store_secret_for_tee(&tee_name, &secret_val, &secret_name)
.await
.expect("storing secret");
info!("stored secret {secret_name}");
}
Ok(())
}

View file

@ -0,0 +1,20 @@
[package]
name = "vault-admin"
publish = false
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
actix-web.workspace = true
anyhow.workspace = true
bytemuck.workspace = true
clap.workspace = true
hex.workspace = true
pgp.workspace = true
serde_json.workspace = true
teepot.workspace = true
teepot-vault.workspace = true
tracing.workspace = true

View file

@ -0,0 +1,46 @@
```bash
idents=( tests/data/pub*.asc )
cargo run --bin vault-admin -p vault-admin -- \
verify \
${idents[@]/#/-i } \
tests/data/test.json \
tests/data/test.json.asc
Verified signature for `81A312C59D679D930FA9E8B06D728F29A2DBABF8`
RUST_LOG=info cargo run -p vault-admin -- \
command \
--sgx-mrsigner c5591a72b8b86e0d8814d6e8750e3efe66aea2d102b8ba2405365559b858697d \
--sgx-allowed-tcb-levels SwHardeningNeeded \
--server https://127.0.0.1:8444 \
crates/teepot-vault/tests/data/test.json \
crates/teepot-vault/tests/data/test.json.asc
2023-08-04T10:51:14.919941Z INFO vault_admin: Quote verified! Connection secure!
2023-08-04T10:51:14.920430Z INFO tee_client: Getting attestation report
2023-08-04T10:51:15.020459Z INFO tee_client: Checked or set server certificate public key hash `f6dc06b9f2a14fa16a94c076a85eab8513f99ec0091801cc62c8761e42908fc1`
2023-08-04T10:51:15.024310Z INFO tee_client: Verifying attestation report
2023-08-04T10:51:15.052712Z INFO tee_client: TcbLevel is allowed: SwHardeningNeeded: Software hardening is needed
2023-08-04T10:51:15.054508Z WARN tee_client: Info: Advisory ID: INTEL-SA-00615
2023-08-04T10:51:15.054572Z INFO tee_client: Report data matches `f6dc06b9f2a14fa16a94c076a85eab8513f99ec0091801cc62c8761e42908fc1`
2023-08-04T10:51:15.054602Z INFO tee_client: mrsigner `c5591a72b8b86e0d8814d6e8750e3efe66aea2d102b8ba2405365559b858697d` matches
[
{
"request": {
"data": {
"lease": "1000",
"name": "test",
"sgx_allowed_tcb_levels": "Ok,SwHardeningNeeded",
"sgx_mrsigner": "c5591a72b8b86e0d8814d6e8750e3efe66aea2d102b8ba2405365559b858697d",
"token_policies": "test",
"types": "sgx"
},
"url": "/v1/auth/tee/tees/test"
},
"response": {
"status_code": 204,
"value": null
}
}
]
```

View file

@ -0,0 +1,371 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use anyhow::{anyhow, bail, Context, Result};
use clap::{Args, Parser, Subcommand};
use pgp::{types::PublicKeyTrait, Deserializable, SignedPublicKey};
use serde_json::Value;
use std::{
default::Default,
fs::{File, OpenOptions},
io::{Read, Write},
path::{Path, PathBuf},
};
use teepot::{
log::{setup_logging, LogLevelParser},
sgx::sign::Signature,
};
use teepot_vault::{
client::{AttestationArgs, TeeConnection},
json::http::{
SignRequest, SignRequestData, SignResponse, VaultCommandRequest, VaultCommands,
VaultCommandsResponse, DIGEST_URL,
},
server::signatures::verify_sig,
};
use tracing::{error, level_filters::LevelFilter};
#[derive(Args, Debug)]
struct SendArgs {
#[clap(flatten)]
pub attestation: AttestationArgs,
/// Vault command file
#[arg(required = true)]
pub command_file: PathBuf,
/// GPG signature files
#[arg(required = true)]
pub sigs: Vec<PathBuf>,
}
#[derive(Args, Debug)]
struct SignTeeArgs {
#[clap(flatten)]
pub attestation: AttestationArgs,
/// output file
#[arg(short, long, required = true)]
pub out: PathBuf,
/// signature request file
#[arg(required = true)]
pub sig_request_file: PathBuf,
/// GPG signature files
#[arg(required = true)]
pub sigs: Vec<PathBuf>,
}
#[derive(Args, Debug)]
struct DigestArgs {
#[clap(flatten)]
pub attestation: AttestationArgs,
}
#[derive(Args, Debug)]
struct VerifyArgs {
/// GPG identity files
#[arg(short, long, required = true)]
pub idents: Vec<PathBuf>,
/// Vault command file
#[arg(required = true)]
pub command_file: PathBuf,
/// GPG signature files
#[arg(required = true)]
pub sigs: Vec<PathBuf>,
}
#[derive(Args, Debug)]
struct CreateSignRequestArgs {
/// Last digest
#[arg(long)]
pub last_digest: Option<String>,
/// TEE name
#[arg(long)]
pub tee_name: Option<String>,
/// Vault command file
#[arg(required = true)]
pub sig_file: PathBuf,
}
#[derive(Subcommand, Debug)]
enum SubCommands {
/// Send the signed commands to execute to the vault
Command(SendArgs),
/// Verify the signature(s) for the commands to send
Verify(VerifyArgs),
/// Get the digest of the last executed commands
Digest(DigestArgs),
/// Send the signed commands to execute to the vault
SignTee(SignTeeArgs),
/// Create a sign request
CreateSignRequest(CreateSignRequestArgs),
}
/// Admin tool for the vault
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Arguments {
#[clap(subcommand)]
cmd: SubCommands,
/// Log level for the log output.
/// Valid values are: `off`, `error`, `warn`, `info`, `debug`, `trace`
#[clap(long, default_value_t = LevelFilter::WARN, value_parser = LogLevelParser)]
pub log_level: LevelFilter,
}
#[actix_web::main]
async fn main() -> Result<()> {
let args = Arguments::parse();
tracing::subscriber::set_global_default(setup_logging(
env!("CARGO_CRATE_NAME"),
&args.log_level,
)?)?;
match args.cmd {
SubCommands::Command(args) => send_commands(args).await?,
SubCommands::SignTee(args) => send_sig_request(args).await?,
SubCommands::Verify(args) => {
verify(args.command_file, args.idents.iter(), args.sigs.iter())?
}
SubCommands::Digest(args) => digest(args).await?,
SubCommands::CreateSignRequest(args) => create_sign_request(args)?,
}
Ok(())
}
fn create_sign_request(args: CreateSignRequestArgs) -> Result<()> {
let mut sigstruct_file = File::open(&args.sig_file)?;
let mut sigstruct_bytes = Vec::new();
sigstruct_file.read_to_end(&mut sigstruct_bytes)?;
let sigstruct = bytemuck::try_from_bytes::<Signature>(&sigstruct_bytes)
.context(format!("parsing signature file {:?}", &args.sig_file))?;
let body = sigstruct.body();
let data = bytemuck::bytes_of(&body).to_vec();
let sign_request_data = SignRequestData {
data,
last_digest: args.last_digest.unwrap_or_default(),
tee_name: args.tee_name.unwrap_or_default(),
tee_type: "sgx".to_string(),
..Default::default()
};
println!("{}", serde_json::to_string_pretty(&sign_request_data)?);
Ok(())
}
fn verify(
msg: impl AsRef<Path>,
idents_file_paths: impl Iterator<Item = impl AsRef<Path>>,
sig_paths: impl Iterator<Item = impl AsRef<Path>>,
) -> Result<()> {
let mut cmd_file = File::open(msg.as_ref())?;
let mut cmd_buf = Vec::new();
cmd_file
.read_to_end(&mut cmd_buf)
.context(format!("reading command file {:?}", &cmd_file))?;
let mut idents = Vec::new();
for ident_file_path in idents_file_paths {
let ident_file = File::open(ident_file_path.as_ref()).context(format!(
"reading identity file {:?}",
ident_file_path.as_ref()
))?;
idents.push(
SignedPublicKey::from_armor_single(ident_file)
.context(format!(
"reading identity file {:?}",
ident_file_path.as_ref()
))?
.0,
);
}
for sig_path in sig_paths {
let mut sig_file = File::open(&sig_path)
.context(format!("reading signature file {:?}", sig_path.as_ref()))?;
let mut sig = String::new();
sig_file
.read_to_string(&mut sig)
.context(format!("reading signature file {:?}", sig_path.as_ref()))?;
let ident_pos = verify_sig(&sig, &cmd_buf, &idents)?;
println!(
"Verified signature for `{}`",
hex::encode_upper(idents.get(ident_pos).unwrap().fingerprint().as_bytes())
);
// Remove the identity from the list of identities to verify
idents.remove(ident_pos);
}
Ok(())
}
async fn send_commands(args: SendArgs) -> Result<()> {
// Read the command file into a string
let mut cmd_file = File::open(&args.command_file)?;
let mut commands = String::new();
cmd_file.read_to_string(&mut commands)?;
// Check that the command file is valid JSON
let vault_commands: VaultCommands = serde_json::from_str(&commands)
.context(format!("parsing command file {:?}", &args.command_file))?;
let mut signatures = Vec::new();
for sig in args.sigs {
let mut sig_file = File::open(sig)?;
let mut sig = String::new();
sig_file.read_to_string(&mut sig)?;
signatures.push(sig);
}
let send_req = VaultCommandRequest {
commands,
signatures,
};
let conn = TeeConnection::new(&args.attestation);
let mut response = conn
.client()
.post(&format!(
"{server}{url}",
server = conn.server(),
url = VaultCommandRequest::URL
))
.send_json(&send_req)
.await
.map_err(|e| anyhow!("sending command request: {}", e))?;
let status_code = response.status();
if !status_code.is_success() {
error!("sending command request: {}", status_code);
if let Ok(r) = response.json::<Value>().await {
eprintln!(
"Error sending command request: {}",
serde_json::to_string(&r).unwrap_or_default()
);
}
bail!("sending command request: {}", status_code);
}
let cmd_responses: VaultCommandsResponse = response
.json()
.await
.context("failed parsing command response")?;
println!("digest: {}", &cmd_responses.digest);
let pairs = cmd_responses
.results
.iter()
.zip(vault_commands.commands.iter())
.map(|(resp, cmd)| {
let mut pair = serde_json::Map::new();
pair.insert("request".to_string(), serde_json::to_value(cmd).unwrap());
pair.insert("response".to_string(), serde_json::to_value(resp).unwrap());
pair
})
.collect::<Vec<_>>();
println!("{}", serde_json::to_string_pretty(&pairs)?);
Ok(())
}
async fn send_sig_request(args: SignTeeArgs) -> Result<()> {
// Read the command file into a string
let mut cmd_file = File::open(&args.sig_request_file)?;
let mut sign_request_data_str = String::new();
cmd_file.read_to_string(&mut sign_request_data_str)?;
// Check that the command file is valid JSON
let _sign_request_data: SignRequestData = serde_json::from_str(&sign_request_data_str)
.context(format!("parsing command file {:?}", &args.sig_request_file))?;
let mut signatures = Vec::new();
for sig in args.sigs {
let mut sig_file = File::open(sig)?;
let mut sig = String::new();
sig_file.read_to_string(&mut sig)?;
signatures.push(sig);
}
// open out_file early to fail fast if it is not writable
let mut out_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&args.out)?;
let send_req = SignRequest {
sign_request_data: sign_request_data_str,
signatures,
};
let conn = TeeConnection::new(&args.attestation);
let mut response = conn
.client()
.post(&format!(
"{server}{url}",
server = conn.server(),
url = SignRequest::URL
))
.send_json(&send_req)
.await
.map_err(|e| anyhow!("sending sign request: {}", e))?;
let status_code = response.status();
if !status_code.is_success() {
error!("sending sign request: {}", status_code);
if let Ok(r) = response.json::<Value>().await {
eprintln!(
"Error sending sign request: {}",
serde_json::to_string(&r).unwrap_or_default()
);
}
bail!("sending sign request: {}", status_code);
}
let sign_response: SignResponse = response
.json()
.await
.context("failed parsing sign response")?;
println!("digest: {}", &sign_response.digest);
out_file.write_all(&sign_response.signed_data)?;
println!("{{ \"digest\": \"{}\" }}", sign_response.digest);
Ok(())
}
async fn digest(args: DigestArgs) -> Result<()> {
let conn = TeeConnection::new(&args.attestation);
let mut response = conn
.client()
.get(&format!("{server}{DIGEST_URL}", server = conn.server()))
.send()
.await
.map_err(|e| anyhow!("sending digest request: {}", e))?;
let status_code = response.status();
if !status_code.is_success() {
error!("sending digest request: {}", status_code);
if let Ok(r) = response.json::<Value>().await {
eprintln!("Error sending digest request: {}", r);
}
bail!("sending digest request: {}", status_code);
}
let digest_response: Value = response
.json()
.await
.context("failed parsing digest response")?;
println!("{}", serde_json::to_string_pretty(&digest_response)?);
Ok(())
}

View file

@ -0,0 +1,19 @@
[package]
name = "vault-unseal"
publish = false
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
actix-web.workspace = true
anyhow.workspace = true
base64.workspace = true
clap.workspace = true
serde_json.workspace = true
teepot-vault.workspace = true
tracing.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1,220 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use anyhow::{anyhow, bail, Context, Result};
use base64::{engine::general_purpose, Engine as _};
use clap::{Args, Parser, Subcommand};
use serde_json::Value;
use std::{fs::File, io::Read};
use teepot_vault::{
client::{AttestationArgs, TeeConnection},
json::http::{Init, InitResponse, Unseal},
};
use tracing::{error, info, trace, warn};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
#[derive(Args, Debug)]
pub struct InitArgs {
/// admin threshold
#[arg(long)]
admin_threshold: usize,
/// PGP keys to sign commands for the admin tee
#[arg(short, long)]
admin_pgp_key_file: Vec<String>,
/// admin TEE mrenclave
#[arg(long)]
admin_tee_mrenclave: String,
/// secret threshold
#[arg(long)]
unseal_threshold: usize,
/// PGP keys to encrypt the unseal keys with
#[arg(short, long)]
unseal_pgp_key_file: Vec<String>,
}
/// subcommands and their options/arguments.
#[derive(Subcommand, Debug)]
enum SubCommands {
Init(InitArgs),
Unseal,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Arguments {
#[clap(flatten)]
pub attestation: AttestationArgs,
/// Subcommands (with their own options)
#[clap(subcommand)]
cmd: SubCommands,
}
#[actix_web::main]
async fn main() -> Result<()> {
LogTracer::init().context("Failed to set logger")?;
let subscriber = Registry::default()
.with(EnvFilter::from_default_env())
.with(fmt::layer().with_writer(std::io::stderr));
tracing::subscriber::set_global_default(subscriber).unwrap();
let args = Arguments::parse();
match args.cmd {
SubCommands::Init(_) => init(args).await?,
SubCommands::Unseal => unseal(args).await?,
}
Ok(())
}
async fn init(args: Arguments) -> Result<()> {
let conn = TeeConnection::new(&args.attestation);
info!("Quote verified! Connection secure!");
let SubCommands::Init(init_args) = args.cmd else {
unreachable!()
};
if init_args.admin_threshold == 0 {
bail!("admin threshold must be greater than 0");
}
if init_args.unseal_threshold == 0 {
bail!("unseal threshold must be greater than 0");
}
if init_args.admin_threshold > init_args.admin_pgp_key_file.len() {
bail!("admin threshold must be less than or equal to the number of admin pgp keys");
}
if init_args.unseal_threshold > init_args.unseal_pgp_key_file.len() {
bail!("unseal threshold must be less than or equal to the number of unseal pgp keys");
}
let mut pgp_keys = Vec::new();
for filename in init_args.unseal_pgp_key_file {
let mut file =
File::open(&filename).context(format!("Failed to open pgp key file {}", &filename))?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
let key = std::str::from_utf8(&buf)?.trim().to_string();
pgp_keys.push(key);
}
let mut admin_pgp_keys = Vec::new();
for filename in init_args.admin_pgp_key_file {
let mut file =
File::open(&filename).context(format!("Failed to open pgp key file {}", &filename))?;
// read all lines from file and concatenate them
let mut key = String::new();
file.read_to_string(&mut key)
.context(format!("Failed to read pgp key file {}", &filename))?;
key.retain(|c| !c.is_ascii_whitespace());
let bytes = general_purpose::STANDARD.decode(key).context(format!(
"Failed to base64 decode pgp key file {}",
&filename
))?;
admin_pgp_keys.push(bytes.into_boxed_slice());
}
let init = Init {
secret_shares: pgp_keys.len() as _,
secret_threshold: init_args.unseal_threshold,
admin_threshold: init_args.admin_threshold,
admin_tee_mrenclave: init_args.admin_tee_mrenclave,
admin_pgp_keys: admin_pgp_keys.into_boxed_slice(),
pgp_keys,
};
info!("Initializing vault");
let mut response = conn
.client()
.post(&format!(
"{server}{url}",
server = conn.server(),
url = Init::URL
))
.send_json(&init)
.await
.map_err(|e| anyhow!("Error sending init request: {}", e))?;
let status_code = response.status();
if !status_code.is_success() {
error!("Failed to init vault: {}", status_code);
if let Ok(r) = response.json::<Value>().await {
eprintln!("Failed to init vault: {}", r);
}
bail!("failed to init vault: {}", status_code);
}
let init_response: Value = response.json().await.context("failed to init vault")?;
info!("Got Response: {}", init_response.to_string());
let resp: InitResponse =
serde_json::from_value(init_response).context("Failed to parse init response")?;
println!("{}", serde_json::to_string(&resp).unwrap());
Ok(())
}
async fn unseal(args: Arguments) -> Result<()> {
info!("Reading unencrypted key from stdin");
// read all bytes from stdin
let mut stdin = std::io::stdin();
let mut buf = Vec::new();
stdin.read_to_end(&mut buf)?;
let key = std::str::from_utf8(&buf)?.trim().to_string();
if key.is_empty() {
bail!("Error reading key from stdin");
}
let conn = TeeConnection::new(&args.attestation);
info!("Quote verified! Connection secure!");
info!("Unsealing vault");
let unseal_data = Unseal { key };
let mut response = conn
.client()
.post(&format!(
"{server}{url}",
server = conn.server(),
url = Unseal::URL
))
.send_json(&unseal_data)
.await
.map_err(|e| anyhow!("Error sending unseal request: {}", e))?;
let status_code = response.status();
if !status_code.is_success() {
error!("Failed to unseal vault: {}", status_code);
if let Ok(r) = response.json::<Value>().await {
eprintln!("Failed to unseal vault: {}", r);
}
bail!("failed to unseal vault: {}", status_code);
}
let unseal_response: Value = response.json().await.context("failed to unseal vault")?;
trace!("Got Response: {}", unseal_response.to_string());
if matches!(unseal_response["sealed"].as_bool(), Some(true)) {
warn!("Vault is still sealed!");
println!("Vault is still sealed!");
} else {
info!("Vault is unsealed!");
println!("Vault is unsealed!");
}
Ok(())
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
//! Helper functions for CLI clients to verify Intel SGX enclaves and other TEEs.
@ -8,15 +8,7 @@
pub mod vault;
use crate::{
quote::Report,
server::pki::{RaTlsCollateralExtension, RaTlsQuoteExtension},
sgx::Quote,
};
pub use crate::{
quote::{verify_quote_with_collateral, QuoteVerificationResult},
sgx::{parse_tcb_levels, sgx_ql_qv_result_t, EnumSet, TcbLevel},
};
use crate::server::pki::{RaTlsCollateralExtension, RaTlsQuoteExtension};
use actix_web::http::header;
use anyhow::Result;
use awc::{Client, Connector};
@ -33,6 +25,11 @@ use rustls::{
};
use sha2::{Digest, Sha256};
use std::{sync::Arc, time, time::Duration};
use teepot::{quote::Report, sgx::Quote};
pub use teepot::{
quote::{verify_quote_with_collateral, QuoteVerificationResult},
sgx::{parse_tcb_levels, sgx_ql_qv_result_t, EnumSet, TcbLevel},
};
use tracing::{debug, error, info, trace, warn};
use x509_cert::{
der::{Decode as _, Encode as _},

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
// Copyright (c) 2023-2025 Matter Labs
//! Helper functions for CLI clients to verify Intel SGX enclaves and other TEEs.
@ -9,15 +9,8 @@
use super::{AttestationArgs, TeeConnection};
use crate::{
json::http::{AuthRequest, AuthResponse},
quote::error::QuoteContext,
server::{pki::make_self_signed_cert, AnyHowResponseError, HttpResponseError, Status},
};
pub use crate::{
quote::{verify_quote_with_collateral, QuoteVerificationResult},
sgx::{
parse_tcb_levels, sgx_gramine_get_quote, sgx_ql_qv_result_t, Collateral, EnumSet, TcbLevel,
},
};
use actix_http::error::PayloadError;
use actix_web::{http::header, ResponseError};
use anyhow::{anyhow, bail, Context, Result};
@ -35,6 +28,13 @@ use std::{
sync::Arc,
time,
};
use teepot::quote::error::QuoteContext;
pub use teepot::{
quote::{verify_quote_with_collateral, QuoteVerificationResult},
sgx::{
parse_tcb_levels, sgx_gramine_get_quote, sgx_ql_qv_result_t, Collateral, EnumSet, TcbLevel,
},
};
use tracing::{debug, error, info, trace};
const VAULT_TOKEN_HEADER: &str = "X-Vault-Token";

View file

@ -1,15 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
// Copyright (c) 2023-2025 Matter Labs
//! Common types for the teepot http JSON API
use crate::sgx::Collateral;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::base64::Base64;
use serde_with::serde_as;
use serde_with::{base64::Base64, serde_as};
use std::fmt::Display;
use std::sync::Arc;
/// The unseal request data
#[derive(Debug, Serialize, Deserialize)]
@ -23,15 +20,6 @@ impl Unseal {
pub const URL: &'static str = "/v1/sys/unseal";
}
/// The attestation response
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationResponse {
/// The quote
pub quote: Arc<[u8]>,
/// The collateral
pub collateral: Arc<Collateral>,
}
/// The init request data
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]

View file

@ -1,12 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2025 Matter Labs
//! Common types for the teepot secrets JSON API
use crate::sgx::sign::Zeroizing;
use serde::{Deserialize, Serialize};
use serde_with::base64::Base64;
use serde_with::serde_as;
use teepot::sgx::sign::Zeroizing;
/// Configuration for the admin tee
#[serde_as]

View file

@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! Helper functions to verify Intel SGX enclaves and other TEEs.
#![deny(missing_docs)]
#![deny(clippy::all)]
pub mod client;
pub mod json;
pub mod server;
pub mod tdx;
/// pad a byte slice to a fixed sized array
pub fn pad<const T: usize>(input: &[u8]) -> [u8; T] {
let mut output = [0; T];
let len = input.len();
if len > T {
output.copy_from_slice(&input[..T]);
} else {
output[..len].copy_from_slice(input);
}
output
}

View file

@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! Common attestation API for all TEEs
use crate::client::AttestationArgs;
use clap::Args;
use serde::{Deserialize, Serialize};
pub use teepot::{
quote::{
attestation::get_quote_and_collateral, error::QuoteContext, get_quote,
verify_quote_with_collateral, QuoteVerificationResult,
},
sgx::{parse_tcb_levels, Collateral, EnumSet, TcbLevel},
};
/// Options and arguments needed to attest a TEE
#[derive(Args, Debug, Clone, Serialize, Deserialize, Default)]
pub struct VaultAttestationArgs {
/// hex encoded SGX mrsigner of the enclave to attest
#[arg(long, env = "VAULT_SGX_MRSIGNER")]
pub vault_sgx_mrsigner: Option<String>,
/// hex encoded SGX mrenclave of the enclave to attest
#[arg(long, env = "VAULT_SGX_MRENCLAVE")]
pub vault_sgx_mrenclave: Option<String>,
/// URL of the server
#[arg(long, required = true, env = "VAULT_ADDR")]
pub vault_addr: String,
/// allowed TCB levels, comma separated:
/// Ok, ConfigNeeded, ConfigAndSwHardeningNeeded, SwHardeningNeeded, OutOfDate, OutOfDateConfigNeeded
#[arg(long, value_parser = parse_tcb_levels, env = "VAULT_SGX_ALLOWED_TCB_LEVELS")]
pub vault_sgx_allowed_tcb_levels: Option<EnumSet<TcbLevel>>,
}
impl From<VaultAttestationArgs> for AttestationArgs {
fn from(value: VaultAttestationArgs) -> Self {
AttestationArgs {
sgx_mrsigner: value.vault_sgx_mrsigner,
sgx_mrenclave: value.vault_sgx_mrenclave,
server: value.vault_addr,
sgx_allowed_tcb_levels: value.vault_sgx_allowed_tcb_levels,
}
}
}
impl From<&VaultAttestationArgs> for AttestationArgs {
fn from(value: &VaultAttestationArgs) -> Self {
AttestationArgs {
sgx_mrsigner: value.vault_sgx_mrsigner.clone(),
sgx_mrenclave: value.vault_sgx_mrenclave.clone(),
server: value.vault_addr.clone(),
sgx_allowed_tcb_levels: value.vault_sgx_allowed_tcb_levels,
}
}
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
// Copyright (c) 2023-2025 Matter Labs
//! # tee-server
@ -7,18 +7,19 @@
#![deny(clippy::all)]
pub mod attestation;
pub mod pki;
pub mod signatures;
use actix_web::http::StatusCode;
use actix_web::web::Bytes;
use actix_web::{error, HttpRequest, HttpResponse};
use actix_web::{HttpMessage, ResponseError};
use actix_web::{
error, http::StatusCode, web::Bytes, HttpMessage, HttpRequest, HttpResponse, ResponseError,
};
use anyhow::anyhow;
use awc::error::{PayloadError, SendRequestError};
use awc::ClientResponse;
use awc::{
error::{PayloadError, SendRequestError},
ClientResponse,
};
use futures_core::Stream;
use std::fmt::{Debug, Display, Formatter};
pub use teepot::pki;
use tracing::error;
/// Anyhow error with an HTTP status code

View file

@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! Intel TDX helper functions.
pub mod rtmr;
pub use intel_tee_quote_verification_rs::Collateral;
use tdx_attest_rs::{tdx_att_get_quote, tdx_attest_error_t, tdx_report_data_t};
pub use teepot::sgx::tcblevel::{parse_tcb_levels, EnumSet, TcbLevel};
use teepot::sgx::QuoteError;
/// Get a TDX quote
pub fn tgx_get_quote(report_data_bytes: &[u8; 64]) -> Result<Box<[u8]>, QuoteError> {
let mut tdx_report_data = tdx_report_data_t { d: [0; 64usize] };
tdx_report_data.d.copy_from_slice(report_data_bytes);
let (error, quote) = tdx_att_get_quote(Some(&tdx_report_data), None, None, 0);
if error == tdx_attest_error_t::TDX_ATTEST_SUCCESS {
if let Some(quote) = quote {
Ok(quote.into())
} else {
Err(QuoteError::TdxAttGetQuote {
msg: "tdx_att_get_quote: No quote returned".into(),
inner: error,
})
}
} else {
Err(error.into())
}
}

View file

@ -0,0 +1,90 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2025 Matter Labs
//! rtmr event data
use teepot::sgx::QuoteError;
/// The actual rtmr event data handled in DCAP
#[repr(C, packed)]
pub struct TdxRtmrEvent {
/// Always 1
version: u32,
/// The RTMR that will be extended. As defined in
/// https://github.com/confidential-containers/td-shim/blob/main/doc/tdshim_spec.md#td-measurement
/// we will use RTMR 3 for guest application code and configuration.
rtmr_index: u64,
/// Data that will be used to extend RTMR
extend_data: [u8; 48usize],
/// Not used in DCAP
event_type: u32,
/// Always 0
event_data_size: u32,
/// Not used in DCAP
event_data: Vec<u8>,
}
impl Default for TdxRtmrEvent {
fn default() -> Self {
Self {
extend_data: [0; 48],
version: 1,
rtmr_index: 3,
event_type: 0,
event_data_size: 0,
event_data: Vec::new(),
}
}
}
impl TdxRtmrEvent {
/// use the extend data
pub fn with_extend_data(mut self, extend_data: [u8; 48]) -> Self {
self.extend_data = extend_data;
self
}
/// extend the rtmr index
pub fn with_rtmr_index(mut self, rtmr_index: u64) -> Self {
self.rtmr_index = rtmr_index;
self
}
/// extending the index, consuming self
pub fn extend(self) -> Result<(), QuoteError> {
let event: Vec<u8> = self.into();
match tdx_attest_rs::tdx_att_extend(&event) {
tdx_attest_rs::tdx_attest_error_t::TDX_ATTEST_SUCCESS => Ok(()),
error_code => Err(error_code.into()),
}
}
}
impl From<TdxRtmrEvent> for Vec<u8> {
fn from(val: TdxRtmrEvent) -> Self {
let event_ptr = &val as *const TdxRtmrEvent as *const u8;
let event_data_size = std::mem::size_of::<u8>() * val.event_data_size as usize;
let res_size = std::mem::size_of::<u32>() * 3
+ std::mem::size_of::<u64>()
+ std::mem::size_of::<[u8; 48]>()
+ event_data_size;
let mut res = vec![0; res_size];
unsafe {
for (i, chunk) in res.iter_mut().enumerate().take(res_size - event_data_size) {
*chunk = *event_ptr.add(i);
}
}
let event_data = val.event_data;
for i in 0..event_data_size {
res[i + res_size - event_data_size] = event_data[i];
}
res
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "teepot"
description = "TEE secret manager"
description = "TEE utilities"
# no MIT license, because of copied code from:
# * https://github.com/enarx/enarx
# * https://github.com/enarx/sgx
@ -11,18 +11,13 @@ authors.workspace = true
repository.workspace = true
[dependencies]
actix-http.workspace = true
actix-web.workspace = true
anyhow.workspace = true
async-trait.workspace = true
awc.workspace = true
bytemuck.workspace = true
bytes.workspace = true
clap.workspace = true
config.workspace = true
const-oid.workspace = true
enumset.workspace = true
futures-core.workspace = true
getrandom.workspace = true
hex.workspace = true
intel-tee-quote-verification-rs.workspace = true
@ -34,7 +29,6 @@ opentelemetry-otlp.workspace = true
opentelemetry-semantic-conventions.workspace = true
opentelemetry_sdk.workspace = true
p256.workspace = true
pgp.workspace = true
pkcs8.workspace = true
reqwest.workspace = true
rsa.workspace = true
@ -42,7 +36,6 @@ rustls.workspace = true
secp256k1 = { workspace = true, features = ["recovery"] }
serde.workspace = true
serde_json.workspace = true
serde_with.workspace = true
sha2.workspace = true
sha3.workspace = true
signature.workspace = true
@ -52,7 +45,6 @@ tracing.workspace = true
tracing-futures.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true
webpki-roots.workspace = true
x509-cert.workspace = true
zeroize.workspace = true
@ -61,4 +53,3 @@ base64.workspace = true
testaso.workspace = true
tokio.workspace = true
tracing-test.workspace = true
zksync_basic_types.workspace = true

View file

@ -37,7 +37,6 @@ pub fn public_key_to_ethereum_address(public: &PublicKey) -> [u8; 20] {
#[cfg(test)]
mod tests {
use secp256k1::{Secp256k1, SecretKey};
use zksync_basic_types::H256;
use super::*;
@ -71,10 +70,10 @@ mod tests {
assert_eq!(address, expected_address.as_slice());
// Generate a random root hash, create a message from the hash, and sign the message using
// Take a root hash, create a message from the hash, and sign the message using
// the secret key
let root_hash = H256::random();
let root_hash_bytes = root_hash.as_bytes();
let root_hash = b"12345678901234567890123456789012";
let root_hash_bytes = root_hash.as_slice();
let msg_to_sign = Message::from_digest(root_hash_bytes.try_into().unwrap());
let signature = sign_message(&secret_key, msg_to_sign).unwrap();

View file

@ -6,14 +6,12 @@
#![deny(missing_docs)]
#![deny(clippy::all)]
pub mod client;
pub mod config;
pub mod ethereum;
pub mod json;
pub mod log;
pub mod pki;
pub mod prover;
pub mod quote;
pub mod server;
pub mod sgx;
pub mod tdx;

View file

@ -1,18 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
// Copyright (c) 2023-2025 Matter Labs
//! Common attestation API for all TEEs
use crate::{
client::AttestationArgs,
json::http::AttestationResponse,
quote::{
error::QuoteContext, get_quote, verify_quote_with_collateral, QuoteVerificationResult,
},
sgx::{parse_tcb_levels, Collateral, EnumSet, TcbLevel},
sgx::{Collateral, EnumSet, TcbLevel},
};
use anyhow::{bail, Context, Result};
use clap::Args;
use intel_tee_quote_verification_rs::tee_qv_get_collateral;
use serde::{Deserialize, Serialize};
use std::{
@ -28,6 +25,15 @@ struct Attestation {
earliest_expiration_date: i64,
}
/// The attestation response
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationResponse {
/// The quote
pub quote: Arc<[u8]>,
/// The collateral
pub collateral: Arc<Collateral>,
}
/// Returns the quote and collateral for the current TEE.
///
/// if `allowed_tcb_levels` is `None`, then any TCB level is accepted.
@ -110,43 +116,3 @@ pub fn get_quote_and_collateral(
Ok(AttestationResponse { quote, collateral })
}
/// Options and arguments needed to attest a TEE
#[derive(Args, Debug, Clone, Serialize, Deserialize, Default)]
pub struct VaultAttestationArgs {
/// hex encoded SGX mrsigner of the enclave to attest
#[arg(long, env = "VAULT_SGX_MRSIGNER")]
pub vault_sgx_mrsigner: Option<String>,
/// hex encoded SGX mrenclave of the enclave to attest
#[arg(long, env = "VAULT_SGX_MRENCLAVE")]
pub vault_sgx_mrenclave: Option<String>,
/// URL of the server
#[arg(long, required = true, env = "VAULT_ADDR")]
pub vault_addr: String,
/// allowed TCB levels, comma separated:
/// Ok, ConfigNeeded, ConfigAndSwHardeningNeeded, SwHardeningNeeded, OutOfDate, OutOfDateConfigNeeded
#[arg(long, value_parser = parse_tcb_levels, env = "VAULT_SGX_ALLOWED_TCB_LEVELS")]
pub vault_sgx_allowed_tcb_levels: Option<EnumSet<TcbLevel>>,
}
impl From<VaultAttestationArgs> for AttestationArgs {
fn from(value: VaultAttestationArgs) -> Self {
AttestationArgs {
sgx_mrsigner: value.vault_sgx_mrsigner,
sgx_mrenclave: value.vault_sgx_mrenclave,
server: value.vault_addr,
sgx_allowed_tcb_levels: value.vault_sgx_allowed_tcb_levels,
}
}
}
impl From<&VaultAttestationArgs> for AttestationArgs {
fn from(value: &VaultAttestationArgs) -> Self {
AttestationArgs {
sgx_mrsigner: value.vault_sgx_mrsigner.clone(),
sgx_mrenclave: value.vault_sgx_mrenclave.clone(),
server: value.vault_addr.clone(),
sgx_allowed_tcb_levels: value.vault_sgx_allowed_tcb_levels,
}
}
}

View file

@ -1,10 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
// Copyright (c) 2023-2025 Matter Labs
// Parts of it are Copyright (c) 2024 Phala Network
// and copied from https://github.com/Phala-Network/dcap-qvl
//! Get a quote from a TEE
pub mod attestation;
pub mod error;
use crate::{

View file

@ -1,83 +0,0 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
NcPkByuqaJvurs3VRpU4rOyFaxaTKXfr
=Bpm9
-----END PGP PRIVATE KEY BLOCK-----

View file

@ -1,31 +0,0 @@
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
NcPkByuqaJvurs3VRpU4rOyFaxaTKXfr