mirror of
https://github.com/matter-labs/teepot.git
synced 2025-07-22 07:24:48 +02:00
chore: split-out vault code from teepot
in teepot-vault
Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
This commit is contained in:
parent
63c16b1177
commit
f8bd9e6a08
61 changed files with 450 additions and 308 deletions
25
crates/teepot-vault/bin/tee-vault-admin/Cargo.toml
Normal file
25
crates/teepot-vault/bin/tee-vault-admin/Cargo.toml
Normal 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
|
89
crates/teepot-vault/bin/tee-vault-admin/src/command.rs
Normal file
89
crates/teepot-vault/bin/tee-vault-admin/src/command.rs
Normal 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))
|
||||
}
|
34
crates/teepot-vault/bin/tee-vault-admin/src/digest.rs
Normal file
34
crates/teepot-vault/bin/tee-vault-admin/src/digest.rs
Normal 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 })))
|
||||
}
|
147
crates/teepot-vault/bin/tee-vault-admin/src/main.rs
Normal file
147
crates/teepot-vault/bin/tee-vault-admin/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
149
crates/teepot-vault/bin/tee-vault-admin/src/sign.rs
Normal file
149
crates/teepot-vault/bin/tee-vault-admin/src/sign.rs
Normal 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())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue