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,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())
}