Merge pull request #28 from matter-labs/ra_tls

feat: use real RA-TLS for everything
This commit is contained in:
Harald Hoyer 2024-02-27 16:54:37 +01:00 committed by GitHub
commit 109b7f7cf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 837 additions and 834 deletions

402
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,7 @@ hex.workspace = true
intel-tee-quote-verification-rs.workspace = true
num-integer.workspace = true
num-traits.workspace = true
p256.workspace = true
pgp.workspace = true
pkcs8.workspace = true
rand.workspace = true
@ -38,8 +39,10 @@ serde.workspace = true
serde_json.workspace = true
serde_with.workspace = true
sha2.workspace = true
signature.workspace = true
thiserror.workspace = true
tracing.workspace = true
webpki-roots.workspace = true
x509-cert.workspace = true
zeroize.workspace = true
@ -86,10 +89,11 @@ mio = "0.8.10"
num-integer = "0.1.46"
num-traits = "0.2.18"
pgp = "0.11"
p256 = "0.13.2"
pkcs8 = { version = "0.10" }
rand = "0.8"
ring = { version = "0.17.8", features = ["std"], default-features = false }
rsa = { version = "0.9.6", features = ["sha2"] }
rsa = { version = "0.9.6", features = ["sha2", "pem"] }
rustls = { version = "0.22" }
rustls-pemfile = "2"
sec1 = { version = "0.7.3", features = ["der"], default-features = false }
@ -97,6 +101,7 @@ serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
serde_with = { version = "3.6", features = ["base64", "hex"] }
sha2 = "0.10.8"
signature = "2.2.0"
teepot = { path = "." }
testaso = "0.1.0"
thiserror = "1.0.57"
@ -105,6 +110,6 @@ tracing = "0.1"
tracing-actix-web = "0.7"
tracing-log = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
x509 = { version = "0.2", package = "x509-cert", default-features = false }
x509-cert = "0.2.5"
x509-cert = { version = "0.2", features = ["builder", "signature"] }
zeroize = { version = "1.7.0", features = ["serde"] }
webpki-roots = "0.26.1"

View file

@ -0,0 +1,21 @@
[package]
name = "tee-ratls-preexec"
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]
anyhow.workspace = true
clap.workspace = true
rsa.workspace = true
rustls-pemfile.workspace = true
rustls.workspace = true
teepot.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
x509-cert.workspace = true

View file

@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024 Matter Labs
//! Pre-exec for binary running in a TEE needing attestation of a secret signing key
#![deny(missing_docs)]
#![deny(clippy::all)]
use anyhow::{Context, Result};
use clap::Parser;
use rsa::pkcs1v15::SigningKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::sha2::Sha256;
use rsa::RsaPrivateKey;
use std::fs::File;
use std::io::Write;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use teepot::server::pki::make_signed_cert;
use tracing::error;
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
use x509_cert::der::asn1::Ia5String;
use x509_cert::der::DecodePem;
use x509_cert::ext::pkix::name::GeneralName;
use x509_cert::Certificate;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// ca cert file
#[arg(long, env = "CA_CERT_FILE", default_value = "/opt/vault/cacert.pem")]
ca_cert_file: PathBuf,
/// ca key file
#[arg(long, env = "CA_KEY_FILE", default_value = "/opt/vault/cakey.pem")]
ca_key_file: PathBuf,
/// out cert file
#[arg(long, env = "TLS_CERT_FILE", default_value = "/opt/vault/tls/tls.crt")]
tls_cert_file: PathBuf,
/// out key file
#[arg(long, env = "TLS_KEY_FILE", default_value = "/opt/vault/tls/tls.key")]
tls_key_file: PathBuf,
/// DNS names, comma separated
#[arg(long, env = "DNS_NAMES", required = true)]
dns_names: String,
/// program to exec [args...] (required)
#[arg(required = true, allow_hyphen_values = true, last = true)]
cmd_args: Vec<String>,
}
fn main_with_error() -> 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).context("Failed to set logger")?;
let args = Args::parse();
// read `issuer_cert_bytes` from file
let ca_cert = std::fs::read(args.ca_cert_file).context("Failed to read ca_cert")?;
let issuer_cert = Certificate::from_pem(&ca_cert)?;
let issuer_key =
RsaPrivateKey::read_pkcs8_pem_file(args.ca_key_file).context("Failed to read ca_key")?;
let issuer_key_pair = SigningKey::<Sha256>::new(issuer_key);
// TODO: read values from config file or env or args
let dn = "O=system:nodes,CN=system:node";
let mut an = vec![std::net::IpAddr::from(std::net::Ipv4Addr::LOCALHOST).into()];
an.extend(
args.dns_names
.split(',')
.map(|s| GeneralName::DnsName(Ia5String::try_from(s.to_string()).unwrap())),
);
let (_report_data, cert, priv_key) =
make_signed_cert(dn, Some(an), &issuer_cert, &issuer_key_pair)?;
// open args.tls_cert_file and write cert and ca_cert
let mut file = File::create(&args.tls_cert_file).context("Failed to create tls_cert")?;
file.write_all(cert.as_bytes())
.context("Failed to write tls_cert")?;
file.write_all(&ca_cert)
.context("Failed to write tls_cert")?;
std::fs::write(args.tls_key_file, priv_key).context("Failed to write tls_cert")?;
let err = Command::new(&args.cmd_args[0])
.args(&args.cmd_args[1..])
.exec();
Err(err).with_context(|| format!("exec of `{cmd}` failed", cmd = args.cmd_args.join(" ")))
}
fn main() -> Result<()> {
let ret = main_with_error();
if let Err(e) = &ret {
error!("Error: {}", e);
}
ret
}

View file

@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024 Matter Labs
//! Server to handle requests to the Vault TEE
@ -45,7 +46,7 @@ async fn main() -> Result<()> {
let args = Arguments::parse();
let (report_data, _cert_chain, _priv_key) = make_self_signed_cert()?;
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

View file

@ -1,25 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! attestation
use crate::ServerState;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Json};
use anyhow::{Context, Result};
use std::sync::Arc;
use teepot::json::http::AttestationResponse;
use teepot::server::attestation::get_quote_and_collateral;
use teepot::server::{HttpResponseError, Status};
use tracing::instrument;
/// Get attestation
#[instrument(level = "info", name = "/v1/sys/attestation", skip_all)]
pub async fn get_attestation(
worker: Data<Arc<ServerState>>,
) -> Result<Json<AttestationResponse>, HttpResponseError> {
get_quote_and_collateral(None, &worker.report_data)
.context("Error getting attestation")
.map(Json)
.status(StatusCode::INTERNAL_SERVER_ERROR)
}

View file

@ -1,12 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
//! Server to handle requests to the Vault TEE
#![deny(missing_docs)]
#![deny(clippy::all)]
mod attestation;
mod command;
mod digest;
mod sign;
@ -14,7 +12,6 @@ mod sign;
use actix_web::web::Data;
use actix_web::{web, App, HttpServer};
use anyhow::{Context, Result};
use attestation::get_attestation;
use clap::Parser;
use command::post_command;
use digest::get_digest;
@ -22,7 +19,7 @@ use rustls::ServerConfig;
use sign::post_sign;
use std::net::Ipv6Addr;
use std::sync::Arc;
use teepot::json::http::{SignRequest, VaultCommandRequest, ATTESTATION_URL, DIGEST_URL};
use teepot::json::http::{SignRequest, VaultCommandRequest, DIGEST_URL};
use teepot::server::attestation::{get_quote_and_collateral, VaultAttestationArgs};
use teepot::server::new_json_cfg;
use teepot::server::pki::make_self_signed_cert;
@ -65,7 +62,7 @@ async fn main() -> Result<()> {
let args = Arguments::parse();
let (report_data, cert_chain, priv_key) = make_self_signed_cert()?;
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)
{
@ -94,7 +91,6 @@ async fn main() -> Result<()> {
.wrap(TracingLogger::default())
.app_data(new_json_cfg())
.app_data(Data::new(server_state.clone()))
.service(web::resource(ATTESTATION_URL).route(web::get().to(get_attestation)))
.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)))

View file

@ -1,27 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
use crate::Worker;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Json};
use anyhow::{Context, Result};
use teepot::json::http::AttestationResponse;
use teepot::server::attestation::get_quote_and_collateral;
use teepot::server::{HttpResponseError, Status};
use tracing::instrument;
#[instrument(level = "info", name = "/v1/sys/attestation", skip_all)]
pub async fn get_attestation(
worker: Data<Worker>,
) -> Result<Json<AttestationResponse>, HttpResponseError> {
let report_data: [u8; 64] = worker
.config
.report_data
.clone()
.try_into()
.map_err(|_| "Error getting attestation")?;
get_quote_and_collateral(None, &report_data)
.context("Error getting attestation")
.map(Json)
.status(StatusCode::INTERNAL_SERVER_ERROR)
}

View file

@ -1,12 +1,13 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
use crate::{create_https_client, get_vault_status, UnsealServerState, Worker};
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::client::TeeConnection;
use teepot::json::http::{Init, InitResponse, VaultInitRequest};
use teepot::json::secrets::AdminConfig;
use teepot::server::{HttpResponseError, Status};
@ -25,7 +26,8 @@ pub async fn post_init(
admin_threshold,
admin_tee_mrenclave,
} = init.into_inner();
let client = create_https_client(worker.client_tls_config.clone());
let conn = TeeConnection::new(&worker.vault_attestation);
let client = conn.client();
let vault_url = &worker.config.vault_url;
let vault_init = VaultInitRequest {
@ -62,7 +64,7 @@ pub async fn post_init(
return Err(anyhow!("Vault already initialized")).status(StatusCode::BAD_REQUEST);
}
UnsealServerState::Undefined => {
let state = get_vault_status(vault_url, client.clone()).await;
let state = get_vault_status(vault_url, client).await;
*worker.state.write().unwrap() = state;
continue;
}

View file

@ -1,55 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
//! Server to initialize and unseal the Vault TEE.
#![deny(missing_docs)]
#![deny(clippy::all)]
mod attestation;
mod init;
mod unseal;
use actix_web::http::header;
use actix_web::rt::time::sleep;
use actix_web::web::Data;
use actix_web::{web, App, HttpServer};
use anyhow::{bail, Context, Result};
use attestation::get_attestation;
use awc::{Client, Connector};
use anyhow::{Context, Result};
use awc::Client;
use clap::Parser;
use init::post_init;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::client::WebPkiServerVerifier;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, Error, ServerConfig, SignatureScheme};
use rustls_pemfile::{certs, read_one};
use sha2::{Digest, Sha256};
use rustls::ServerConfig;
use std::fmt::Debug;
use std::net::Ipv6Addr;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use std::{fs::File, io::BufReader};
use teepot::json::http::{Init, Unseal, ATTESTATION_URL};
use teepot::client::{AttestationArgs, TeeConnection};
use teepot::json::http::{Init, Unseal};
use teepot::json::secrets::AdminConfig;
use teepot::server::attestation::get_quote_and_collateral;
use teepot::server::attestation::{get_quote_and_collateral, VaultAttestationArgs};
use teepot::server::new_json_cfg;
use teepot::server::pki::make_self_signed_cert;
use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel};
use tracing::{error, info, trace};
use tracing::{error, info};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
use unseal::post_unseal;
use x509_cert::der::Decode as _;
use x509_cert::der::Encode as _;
use x509_cert::Certificate;
const VAULT_AUTH_TEE_SHA256: &str = include_str!("../../../assets/vault-auth-tee.sha256");
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 client_tls_config: Arc<ClientConfig>,
pub vault_attestation: Arc<AttestationArgs>,
/// Server config
pub config: Arc<UnsealServerConfig>,
/// Server state
@ -62,9 +52,13 @@ pub struct UnsealServerConfig {
/// Vault URL
pub vault_url: String,
/// The expected report_data for the Vault TEE
pub report_data: Vec<u8>,
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,
}
/// Server state
@ -90,21 +84,6 @@ pub enum UnsealServerState {
VaultUnsealed,
}
impl UnsealServerConfig {
/// Create a new ServerState
pub fn new(
vault_url: String,
report_data: [u8; 64],
allowed_tcb_levels: Option<EnumSet<TcbLevel>>,
) -> Self {
Self {
report_data: report_data.to_vec(),
vault_url,
allowed_tcb_levels,
}
}
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
@ -114,9 +93,12 @@ struct Args {
/// port to listen on
#[arg(long, env = "PORT", default_value = "8443")]
port: u16,
/// vault url
#[arg(long, env = "VAULT_ADDR", default_value = "https://vault:8210")]
vault_url: String,
#[arg(long, env = "VAULT_AUTH_TEE_SHA256")]
vault_auth_tee_sha: String,
#[arg(long, env = "VAULT_AUTH_TEE_VERSION")]
vault_auth_tee_version: String,
#[clap(flatten)]
pub attestation: VaultAttestationArgs,
}
#[actix_web::main]
@ -134,55 +116,50 @@ async fn main() -> Result<()> {
let args = Args::parse();
let tls_ok = std::path::Path::new("/opt/vault/tls/tls.ok");
loop {
info!("Waiting for TLS key/cert files to be generated");
// Wait for the file `data/tls.key` to exist
if tls_ok.exists() {
break;
}
sleep(Duration::from_secs(1)).await;
}
info!("Starting up");
let (config, client_tls_config, report_data) = load_rustls_config().or_else(|e| {
error!("failed to load rustls config: {e:?}");
Err(e).context("Failed to load rustls config")
})?;
if let Err(e) = get_quote_and_collateral(Some(args.allowed_tcb_levels), &report_data) {
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 client = create_https_client(client_tls_config.clone());
let (report_data, cert_chain, priv_key) = make_self_signed_cert("CN=localhost", None)?;
let server_state = get_vault_status(&args.vault_url, client).await;
// 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;
info!("Starting HTTPS server at port {}", args.port);
let server_config = Arc::new(UnsealServerConfig::new(
args.vault_url,
report_data,
Some(args.allowed_tcb_levels),
));
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: args.vault_auth_tee_sha,
vault_auth_tee_version: args.vault_auth_tee_version,
});
let server_state = Arc::new(RwLock::new(server_state));
let server = match HttpServer::new(move || {
let worker = Worker {
client_tls_config: client_tls_config.clone(),
config: server_config.clone(),
state: server_state.clone(),
};
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))
.service(web::resource(ATTESTATION_URL).route(web::get().to(get_attestation)))
.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)))
})
@ -203,7 +180,7 @@ async fn main() -> Result<()> {
Ok(())
}
async fn get_vault_status(vault_url: &str, client: Client) -> UnsealServerState {
async fn get_vault_status(vault_url: &str, client: &Client) -> UnsealServerState {
loop {
let r = client
.get(format!("{}/v1/sys/health", vault_url))
@ -234,130 +211,3 @@ async fn get_vault_status(vault_url: &str, client: Client) -> UnsealServerState
sleep(Duration::from_secs(1)).await;
}
}
// Save the hash of the public server key to `REPORT_DATA` to check
// the attestations against it and it does not change on reconnect.
fn make_verifier(server_cert: Box<[u8]>) -> impl ServerCertVerifier {
#[derive(Debug)]
struct V {
server_cert: Box<[u8]>,
server_verifier: Arc<WebPkiServerVerifier>,
}
impl ServerCertVerifier for V {
fn verify_server_cert(
&self,
end_entity: &CertificateDer,
_intermediates: &[CertificateDer],
_server_name: &ServerName,
_ocsp_response: &[u8],
_now: UnixTime,
) -> std::result::Result<ServerCertVerified, Error> {
let data = &self.server_cert;
if data.as_ref() == end_entity.as_ref() {
info!("Server certificate matches expected certificate");
Ok(ServerCertVerified::assertion())
} else {
error!("Server certificate does not match expected certificate");
Err(rustls::Error::General(
"Server certificate does not match expected certificate".to_string(),
))
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, Error> {
self.server_verifier
.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, Error> {
self.server_verifier
.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.server_verifier.supported_verify_schemes()
}
}
let root_store = Arc::new(rustls::RootCertStore::empty());
let server_verifier = WebPkiServerVerifier::builder(root_store).build().unwrap();
V {
server_cert,
server_verifier,
}
}
/// Load TLS key/cert files
pub fn load_rustls_config() -> Result<(ServerConfig, Arc<ClientConfig>, [u8; 64])> {
// init server config builder with safe defaults
let config = ServerConfig::builder().with_no_client_auth();
// load TLS key/cert files
let cert_file = &mut BufReader::new(
File::open("/opt/vault/tls/tls.crt").context("Failed to open TLS cert file")?,
);
let key_file = &mut BufReader::new(
File::open("/opt/vault/tls/tls.key").context("Failed to open TLS key file")?,
);
// convert files to key/cert objects
let cert_chain = certs(cert_file)
.collect::<Result<Vec<_>, _>>()
.context("Failed to load TLS cert file")?;
let priv_key: rustls::pki_types::PrivateKeyDer =
match read_one(key_file).context("Failed to read TLS key file")? {
Some(rustls_pemfile::Item::Sec1Key(key)) => key.into(),
Some(rustls_pemfile::Item::Pkcs1Key(key)) => key.into(),
Some(rustls_pemfile::Item::Pkcs8Key(key)) => key.into(),
_ => bail!("no keys found in TLS key file"),
};
let tls_config = Arc::new(
ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(make_verifier(
cert_chain[0].as_ref().into(),
)))
.with_no_client_auth(),
);
let cert = Certificate::from_der(cert_chain[0].as_ref()).unwrap();
let pub_key = cert
.tbs_certificate
.subject_public_key_info
.to_der()
.unwrap();
let hash = Sha256::digest(pub_key);
let mut report_data = [0u8; 64];
report_data[..32].copy_from_slice(&hash[..32]);
let report_data_hex = hex::encode(report_data);
trace!(report_data_hex);
let config = config
.with_single_cert(cert_chain, priv_key)
.context("Failed to load TLS key/cert files")?;
Ok((config, tls_config, report_data))
}
/// Create an HTTPS client with the default headers and config
pub fn create_https_client(client_tls_config: Arc<ClientConfig>) -> Client {
Client::builder()
.add_default_header((header::USER_AGENT, "teepot/1.0"))
// a "connector" wraps the stream into an encrypted connection
.connector(Connector::new().rustls_0_22(client_tls_config))
.timeout(Duration::from_secs(12000))
.finish()
}

View file

@ -1,10 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
use crate::{
create_https_client, get_vault_status, UnsealServerConfig, UnsealServerState, Worker,
VAULT_AUTH_TEE_SHA256, VAULT_TOKEN_HEADER,
};
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};
@ -16,6 +13,7 @@ use std::future::Future;
use std::io::Read;
use std::time::Duration;
use teepot::client::vault::VaultConnection;
use teepot::client::TeeConnection;
use teepot::json::http::Unseal;
use teepot::json::secrets::{AdminConfig, AdminState};
use teepot::server::{HttpResponseError, Status};
@ -26,7 +24,8 @@ pub async fn post_unseal(
worker: web::Data<Worker>,
item: web::Json<Unseal>,
) -> Result<HttpResponse, HttpResponseError> {
let client = create_https_client(worker.client_tls_config.clone());
let conn = TeeConnection::new(&worker.vault_attestation);
let client = conn.client();
let app = &worker.config;
let vault_url = &app.vault_url;
@ -46,7 +45,7 @@ pub async fn post_unseal(
break;
}
UnsealServerState::Undefined => {
let state = get_vault_status(vault_url, client.clone()).await;
let state = get_vault_status(vault_url, client).await;
*worker.state.write().unwrap() = state;
continue;
}
@ -106,15 +105,13 @@ pub async fn post_unseal(
} => {
debug!(root_token);
info!("Vault is unsealed");
let app = &worker.config;
let client = create_https_client(worker.client_tls_config.clone());
vault_configure_unsealed(
app,
&admin_config,
&root_token,
&admin_tee_mrenclave,
&client,
client,
)
.await
.context("Failed to configure unsealed vault")
@ -204,9 +201,9 @@ pub async fn vault_configure_unsealed(
)),
root_token,
json!({
"sha256": VAULT_AUTH_TEE_SHA256,
"sha256": app.vault_auth_tee_sha,
"command": "vault-auth-tee",
"version": "0.1.0+dev"
"version": app.vault_auth_tee_version
}),
)
.await
@ -374,7 +371,7 @@ async fn plugin_is_already_running(
.and_then(|v| v.as_str())
.and_then(|v| if v.is_empty() { None } else { Some(v) })
.and_then(|v| {
if v == VAULT_AUTH_TEE_SHA256 {
if v == app.vault_auth_tee_sha {
Some(v)
} else {
None

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
use anyhow::{anyhow, bail, Context, Result};
use clap::{Args, Parser, Subcommand};
@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
use teepot::client::{AttestationArgs, TeeConnection};
use teepot::json::http::{
SignRequest, SignRequestData, SignResponse, VaultCommandRequest, VaultCommands,
VaultCommandsResponse, ATTESTATION_URL, DIGEST_URL,
VaultCommandsResponse, DIGEST_URL,
};
use teepot::server::signatures::verify_sig;
use teepot::sgx::sign::Signature;
@ -221,7 +221,7 @@ async fn send_commands(args: SendArgs) -> Result<()> {
signatures,
};
let conn = TeeConnection::new(&args.attestation, ATTESTATION_URL).await?;
let conn = TeeConnection::new(&args.attestation);
let mut response = conn
.client()
@ -299,7 +299,7 @@ async fn send_sig_request(args: SignTeeArgs) -> Result<()> {
signatures,
};
let conn = TeeConnection::new(&args.attestation, ATTESTATION_URL).await?;
let conn = TeeConnection::new(&args.attestation);
let mut response = conn
.client()
@ -338,7 +338,7 @@ async fn send_sig_request(args: SignTeeArgs) -> Result<()> {
}
async fn digest(args: DigestArgs) -> Result<()> {
let conn = TeeConnection::new(&args.attestation, ATTESTATION_URL).await?;
let conn = TeeConnection::new(&args.attestation);
let mut response = conn
.client()

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
use anyhow::{anyhow, bail, Context, Result};
use base64::{engine::general_purpose, Engine as _};
@ -8,7 +8,7 @@ use serde_json::Value;
use std::fs::File;
use std::io::Read;
use teepot::client::{AttestationArgs, TeeConnection};
use teepot::json::http::{Init, InitResponse, Unseal, ATTESTATION_URL};
use teepot::json::http::{Init, InitResponse, Unseal};
use tracing::{error, info, trace, warn};
use tracing_log::LogTracer;
use tracing_subscriber::Registry;
@ -70,7 +70,7 @@ async fn main() -> Result<()> {
}
async fn init(args: Arguments) -> Result<()> {
let conn = TeeConnection::new(&args.attestation, ATTESTATION_URL).await?;
let conn = TeeConnection::new(&args.attestation);
info!("Quote verified! Connection secure!");
@ -177,7 +177,7 @@ async fn unseal(args: Arguments) -> Result<()> {
bail!("Error reading key from stdin");
}
let conn = TeeConnection::new(&args.attestation, ATTESTATION_URL).await?;
let conn = TeeConnection::new(&args.attestation);
info!("Quote verified! Connection secure!");

View file

@ -8,26 +8,27 @@
pub mod vault;
use crate::json::http::AttestationResponse;
use crate::sgx::Collateral;
use crate::server::pki::{RaTlsCollateralExtension, RaTlsQuoteExtension};
use crate::sgx::Quote;
pub use crate::sgx::{
parse_tcb_levels, sgx_ql_qv_result_t, verify_quote_with_collateral, EnumSet,
QuoteVerificationResult, TcbLevel,
};
use actix_web::http::header;
use anyhow::{anyhow, bail, Context, Result};
use anyhow::Result;
use awc::{Client, Connector};
use clap::Args;
use const_oid::AssociatedOid;
use intel_tee_quote_verification_rs::Collateral;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
use rustls::client::WebPkiServerVerifier;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, Error, SignatureScheme};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use std::time;
use std::time::Duration;
use tracing::{error, info, warn};
use tracing::{debug, error, info, trace, warn};
use x509_cert::der::{Decode as _, Encode as _};
use x509_cert::Certificate;
@ -61,13 +62,11 @@ impl TeeConnection {
///
/// This will verify the attestation report and check that the enclave
/// is running the expected code.
pub async fn new(args: &AttestationArgs, attestation_url: &str) -> Result<Self> {
let pk_hash = Arc::new(OnceLock::new());
pub fn new(args: &AttestationArgs) -> Self {
let tls_config = Arc::new(
ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(Self::make_verifier(pk_hash.clone())))
.with_custom_certificate_verifier(Arc::new(Self::make_verifier(args.clone())))
.with_no_client_auth(),
);
@ -78,15 +77,10 @@ impl TeeConnection {
.timeout(Duration::from_secs(12000))
.finish();
let this = Self {
Self {
server: args.server.clone(),
client: agent,
};
this.check_attestation(args, attestation_url, pk_hash)
.await?;
Ok(this)
}
}
/// Create a new connection to a TEE
@ -110,131 +104,12 @@ impl TeeConnection {
&self.server
}
async fn check_attestation(
&self,
args: &AttestationArgs,
attestation_url: &str,
pk_hash: Arc<OnceLock<[u8; 32]>>,
) -> Result<()> {
info!("Getting attestation report");
let mut response = self
.client
.get(&format!("{}{attestation_url}", args.server))
.send()
.await
.map_err(|e| anyhow!("Error sending attestation request: {}", e))?;
let status_code = response.status();
if !status_code.is_success() {
error!("Failed to get attestation: {}", status_code);
if let Ok(r) = response.json::<Value>().await {
eprintln!("Failed to get attestation: {}", r);
}
bail!("failed to get attestation: {}", status_code);
}
let attestation: AttestationResponse =
response.json().await.context("failed to get attestation")?;
let current_time: i64 = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.unwrap()
.as_secs() as _;
info!("Verifying attestation report");
let quote: &[u8] = &attestation.quote;
let collateral: Option<&Collateral> = Some(&attestation.collateral);
let pk_hash = pk_hash.get().unwrap();
Self::check_attestation_args(args, current_time, quote, collateral, pk_hash)?;
Ok(())
}
/// Check the attestation report against `AttestationArgs`
pub fn check_attestation_args(
args: &AttestationArgs,
current_time: i64,
quote: &[u8],
collateral: Option<&Collateral>,
pk_hash: &[u8; 32],
) -> Result<()> {
let QuoteVerificationResult {
collateral_expired,
result,
quote,
advisories,
..
} = verify_quote_with_collateral(quote, collateral, current_time).unwrap();
if collateral_expired || !matches!(result, sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK) {
if collateral_expired {
error!("Collateral is out of date!");
bail!("Collateral is out of date!");
}
let tcblevel = TcbLevel::from(result);
if args
.sgx_allowed_tcb_levels
.map_or(true, |levels| !levels.contains(tcblevel))
{
error!("Quote verification result: {}", tcblevel);
bail!("Quote verification result: {}", tcblevel);
}
info!("TcbLevel is allowed: {}", tcblevel);
}
for advisory in advisories {
warn!("Info: Advisory ID: {advisory}");
}
if &quote.report_body.reportdata[..32] != pk_hash {
error!("Report data mismatch");
bail!("Report data mismatch");
} else {
info!(
"Report data matches `{}`",
hex::encode(&quote.report_body.reportdata[..32])
);
}
if let Some(mrsigner) = &args.sgx_mrsigner {
let mrsigner_bytes = hex::decode(mrsigner).context("Failed to decode mrsigner")?;
if quote.report_body.mrsigner[..] != mrsigner_bytes {
bail!(
"mrsigner mismatch: got {}, expected {}",
hex::encode(quote.report_body.mrsigner),
&mrsigner
);
} else {
info!("mrsigner `{mrsigner}` matches");
}
}
if let Some(mrenclave) = &args.sgx_mrenclave {
let mrenclave_bytes = hex::decode(mrenclave).context("Failed to decode mrenclave")?;
if quote.report_body.mrenclave[..] != mrenclave_bytes {
bail!(
"mrenclave mismatch: got {}, expected {}",
hex::encode(quote.report_body.mrenclave),
&mrenclave
);
} else {
info!("mrenclave `{mrenclave}` matches");
}
}
Ok(())
}
/// Save the hash of the public server key to `REPORT_DATA` to check
/// the attestations against it and it does not change on reconnect.
pub fn make_verifier(pk_hash: Arc<OnceLock<[u8; 32]>>) -> impl ServerCertVerifier {
pub fn make_verifier(args: AttestationArgs) -> impl ServerCertVerifier {
#[derive(Debug)]
struct V {
pk_hash: Arc<OnceLock<[u8; 32]>>,
args: AttestationArgs,
server_verifier: Arc<WebPkiServerVerifier>,
}
impl ServerCertVerifier for V {
@ -255,20 +130,133 @@ impl TeeConnection {
.unwrap();
let hash = Sha256::digest(pub_key);
let data = self.pk_hash.get_or_init(|| hash[..32].try_into().unwrap());
if data == &hash[..32] {
info!(
"Checked or set server certificate public key hash `{}`",
hex::encode(&hash[..32])
);
Ok(rustls::client::danger::ServerCertVerified::assertion())
} else {
error!("Server certificate does not match expected certificate");
Err(rustls::Error::General(
"Server certificate does not match expected certificate".to_string(),
))
let exts = cert
.tbs_certificate
.extensions
.ok_or_else(|| Error::General("Failed get quote in certificate".into()))?;
trace!("Get quote bytes!");
let quote_bytes = exts
.iter()
.find(|ext| ext.extn_id == RaTlsQuoteExtension::OID)
.ok_or_else(|| Error::General("Failed get quote in certificate".into()))?
.extn_value
.as_bytes();
trace!("Get collateral bytes!");
let collateral = exts
.iter()
.find(|ext| ext.extn_id == RaTlsCollateralExtension::OID)
.and_then(|ext| {
serde_json::from_slice::<Collateral>(ext.extn_value.as_bytes())
.map_err(|e| {
debug!("Failed to get collateral in certificate {e:?}");
trace!(
"Failed to get collateral in certificate {:?}",
String::from_utf8_lossy(ext.extn_value.as_bytes())
);
})
.ok()
});
if collateral.is_none() {
debug!("Failed to get collateral in certificate");
}
let quote = Quote::try_from_bytes(quote_bytes).map_err(|e| {
Error::General(format!("Failed get quote in certificate {e:?}"))
})?;
if &quote.report_body.reportdata[..32] != hash.as_slice() {
error!("Report data mismatch");
return Err(Error::General("Report data mismatch".to_string()));
} else {
info!(
"Report data matches `{}`",
hex::encode(&quote.report_body.reportdata[..32])
);
}
let current_time: i64 = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.unwrap()
.as_secs() as _;
let QuoteVerificationResult {
collateral_expired,
result,
quote,
advisories,
earliest_expiration_date,
..
} = verify_quote_with_collateral(quote_bytes, collateral.as_ref(), current_time)
.unwrap();
if collateral_expired || result != sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK {
if collateral_expired {
error!(
"Collateral is out of date! Expired {}",
earliest_expiration_date
);
return Err(Error::General(format!(
"Collateral is out of date! Expired {}",
earliest_expiration_date
)));
}
let tcblevel = TcbLevel::from(result);
if self
.args
.sgx_allowed_tcb_levels
.map_or(true, |levels| !levels.contains(tcblevel))
{
error!("Quote verification result: {}", tcblevel);
return Err(Error::General(format!(
"Quote verification result: {}",
tcblevel
)));
}
info!("TcbLevel is allowed: {}", tcblevel);
}
for advisory in advisories {
warn!("Info: Advisory ID: {advisory}");
}
if let Some(mrsigner) = &self.args.sgx_mrsigner {
let mrsigner_bytes = hex::decode(mrsigner)
.map_err(|e| Error::General(format!("Failed to decode mrsigner: {}", e)))?;
if quote.report_body.mrsigner[..] != mrsigner_bytes {
return Err(Error::General(format!(
"mrsigner mismatch: got {}, expected {}",
hex::encode(quote.report_body.mrsigner),
&mrsigner
)));
} else {
info!("mrsigner `{mrsigner}` matches");
}
}
if let Some(mrenclave) = &self.args.sgx_mrenclave {
let mrenclave_bytes = hex::decode(mrenclave).map_err(|e| {
Error::General(format!("Failed to decode mrenclave: {}", e))
})?;
if quote.report_body.mrenclave[..] != mrenclave_bytes {
return Err(Error::General(format!(
"mrenclave mismatch: got {}, expected {}",
hex::encode(quote.report_body.mrenclave),
&mrenclave
)));
} else {
info!("mrenclave `{mrenclave}` matches");
}
}
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
@ -295,10 +283,14 @@ impl TeeConnection {
self.server_verifier.supported_verify_schemes()
}
}
let root_store = Arc::new(rustls::RootCertStore::empty());
let server_verifier = WebPkiServerVerifier::builder(root_store).build().unwrap();
let mut root_store = rustls::RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let server_verifier = WebPkiServerVerifier::builder(Arc::new(root_store))
.build()
.unwrap();
V {
pk_hash,
args,
server_verifier,
}
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
//! Helper functions for CLI clients to verify Intel SGX enclaves and other TEEs.
@ -22,11 +22,10 @@ use awc::error::{SendRequestError, StatusCode};
use awc::{Client, ClientResponse, Connector};
use bytes::Bytes;
use futures_core::Stream;
use getrandom::getrandom;
use rustls::ClientConfig;
use serde_json::{json, Value};
use std::fmt::{Display, Formatter};
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use std::time;
use tracing::{debug, error, info, trace};
@ -85,15 +84,14 @@ impl VaultConnection {
/// This will verify the attestation report and check that the enclave
/// is running the expected code.
pub async fn new(args: &AttestationArgs, name: String) -> Result<Self> {
let pk_hash = Arc::new(OnceLock::new());
let (key_hash, rustls_certificate, rustls_pk) = make_self_signed_cert()?;
let (key_hash, rustls_certificate, rustls_pk) =
make_self_signed_cert("CN=localhost", None)?;
let tls_config = Arc::new(
ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(TeeConnection::make_verifier(
pk_hash.clone(),
args.clone(),
)))
.with_client_auth_cert(vec![rustls_certificate], rustls_pk)?,
);
@ -114,7 +112,7 @@ impl VaultConnection {
client_token: Default::default(),
};
this.client_token = this.auth(args).await?.auth.client_token;
this.client_token = this.auth().await?.auth.client_token;
trace!("Got Token: {:#?}", &this.client_token);
@ -147,24 +145,18 @@ impl VaultConnection {
self.conn.client()
}
async fn auth(&self, args: &AttestationArgs) -> Result<AuthResponse> {
async fn auth(&self) -> Result<AuthResponse> {
info!("Getting attestation report");
let attestation_url = AuthRequest::URL;
let quote = sgx_gramine_get_quote(&self.key_hash).context("Failed to get own quote")?;
let collateral = tee_qv_get_collateral(&quote).context("Failed to get own collateral")?;
let mut challenge_bytes = [0u8; 32];
getrandom(&mut challenge_bytes)?;
let challenge = hex::encode(challenge_bytes);
info!("Challenging Vault with: {}", challenge);
let challenge = Some(challenge_bytes);
let auth_req = AuthRequest {
name: self.name.clone(),
tee_type: "sgx".to_string(),
quote,
collateral: serde_json::to_string(&collateral)?,
challenge,
challenge: None,
};
let mut response = self
@ -197,26 +189,6 @@ impl VaultConnection {
trace!("Got AuthResponse: {:#?}", &auth_response);
let current_time: i64 = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.unwrap()
.as_secs() as _;
info!("Verifying attestation report");
let collateral: Option<Collateral> =
serde_json::from_str(&auth_response.data.collateral).ok();
let collateral = collateral.as_ref();
TeeConnection::check_attestation_args(
args,
current_time,
&auth_response.data.quote,
collateral,
&challenge_bytes,
)
.context("Failed to verify Vault attestation report")?;
Ok(auth_response)
}
@ -306,7 +278,10 @@ impl VaultConnection {
}
// check if rel_path is alphanumeric
if !rel_path.chars().all(|c| c.is_alphanumeric() || c == '_') {
if !rel_path
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '/')
{
return Err(anyhow!("path is not alphanumeric")).status(StatusCode::BAD_REQUEST);
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
//! Common types for the teepot http JSON API
@ -23,9 +23,6 @@ impl Unseal {
pub const URL: &'static str = "/v1/sys/unseal";
}
/// The attestation URL
pub const ATTESTATION_URL: &str = "/v1/sys/attestation";
/// The attestation response
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationResponse {

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
//! Common attestation API for all TEEs

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
//! # tee-server
@ -78,9 +78,15 @@ impl HttpResponseError {
S: Stream<Item = Result<Bytes, PayloadError>>,
{
let status_code = response.status();
error!("Vault returned server error: {}", status_code);
let body = response.body().await.ok();
let content_type = response.content_type().to_string();
error!(
"Vault returned server error: {status_code} {}",
body.as_ref()
.map_or("", |b| std::str::from_utf8(b).unwrap_or(""))
);
Self::Proxy(ProxyResponseError {
status_code,
body,

View file

@ -1,215 +1,283 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
//! Some cryptographic utilities
//! Create a private key and a signed and self-signed certificates
use crate::quote::get_quote;
use crate::sgx::tee_qv_get_collateral;
pub use crate::sgx::{
parse_tcb_levels, sgx_ql_qv_result_t, verify_quote_with_collateral, EnumSet,
QuoteVerificationResult, TcbLevel,
};
use anyhow::{anyhow, Context, Result};
use const_oid::db::rfc5280::{
ID_CE_BASIC_CONSTRAINTS, ID_CE_EXT_KEY_USAGE, ID_CE_KEY_USAGE, ID_KP_CLIENT_AUTH,
ID_KP_SERVER_AUTH,
};
use const_oid::db::rfc5912::SECP_256_R_1;
use anyhow::{Context, Result};
use const_oid::db::rfc5280::{ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH};
use const_oid::AssociatedOid;
use getrandom::getrandom;
use pkcs8::der::asn1::OctetString;
use pkcs8::der::referenced::OwnedToRef;
use pkcs8::der::referenced::RefToOwned;
use pkcs8::{
AlgorithmIdentifierRef, ObjectIdentifier, PrivateKeyInfo, SubjectPublicKeyInfo,
SubjectPublicKeyInfoRef,
};
use p256::ecdsa::DerSignature;
use p256::pkcs8::EncodePrivateKey;
use pkcs8::der;
use rustls::pki_types::PrivatePkcs8KeyDer;
use sec1::EcPrivateKey;
use sha2::{Digest, Sha256};
use signature::Signer;
use std::str::FromStr;
use std::time::Duration;
use x509_cert::der::asn1::BitString;
use x509_cert::der::{Decode as _, Encode as _};
use x509_cert::ext::pkix::{BasicConstraints, ExtendedKeyUsage, KeyUsage, KeyUsages};
use x509_cert::name::RdnSequence;
use tracing::debug;
use x509_cert::builder::{Builder, CertificateBuilder, Profile};
use x509_cert::der::pem::LineEnding;
use x509_cert::der::{asn1::OctetString, Encode as _, EncodePem as _, Length};
use x509_cert::ext::pkix::name::GeneralNames;
use x509_cert::ext::pkix::{ExtendedKeyUsage, SubjectAltName};
use x509_cert::ext::{AsExtension, Extension};
use x509_cert::name::{Name, RdnSequence};
use x509_cert::serial_number::SerialNumber;
use x509_cert::spki::{
DynSignatureAlgorithmIdentifier, EncodePublicKey, ObjectIdentifier, SignatureBitStringEncoding,
SubjectPublicKeyInfoOwned,
};
use x509_cert::time::Validity;
use x509_cert::{Certificate, TbsCertificate};
use x509_cert::Certificate;
use zeroize::Zeroizing;
use const_oid::db::rfc5912::{
ECDSA_WITH_SHA_256, ECDSA_WITH_SHA_384, ID_EC_PUBLIC_KEY as ECPK, SECP_256_R_1 as P256,
SECP_384_R_1 as P384,
};
use pkcs8::der::asn1::BitStringRef;
/// The OID for the `gramine-ra-tls` quote extension
pub const ID_GRAMINE_RA_TLS_QUOTE: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113741.1337.6");
const ES256: AlgorithmIdentifierRef<'static> = AlgorithmIdentifierRef {
oid: ECDSA_WITH_SHA_256,
parameters: None,
};
/// The OID for the `enarx-ra-tls` collateral extension
/// TODO: this OID is just made up in `enarx` OID namespace, reserve it somehow
pub const ID_GRAMINE_RA_TLS_COLLATERAL: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.3.6.1.4.1.58270.1.99");
const ES384: AlgorithmIdentifierRef<'static> = AlgorithmIdentifierRef {
oid: ECDSA_WITH_SHA_384,
parameters: None,
};
/// Utility trait for signing with a private key
pub trait PrivateKeyInfoExt {
/// Generates a keypair
///
/// Returns the DER encoding of the `PrivateKeyInfo` type.
fn generate(oid: ObjectIdentifier) -> Result<Zeroizing<Vec<u8>>>;
/// Get the public key
///
/// This function creates a `SubjectPublicKeyInfo` which corresponds with
/// this private key. Note that this function does not do any cryptographic
/// calculations. It expects that the `PrivateKeyInfo` already contains the
/// public key.
fn public_key(&self) -> Result<SubjectPublicKeyInfoRef<'_>>;
/// Get the default signing algorithm for this `SubjectPublicKeyInfo`
fn signs_with(&self) -> Result<AlgorithmIdentifierRef<'_>>;
/// Signs the body with the specified algorithm
///
/// Note that the signature is returned in its encoded form as it will
/// appear in an X.509 certificate or PKCS#10 certification request.
fn sign(&self, body: &[u8], algo: AlgorithmIdentifierRef<'_>) -> Result<Vec<u8>>;
/// The `gramine-ra-tls` x509 extension
pub struct RaTlsQuoteExtension {
/// The hash of the certificate's public key
pub quote: Vec<u8>,
}
impl<'a> PrivateKeyInfoExt for PrivateKeyInfo<'a> {
fn generate(oid: ObjectIdentifier) -> Result<Zeroizing<Vec<u8>>> {
let rand = ring::rand::SystemRandom::new();
impl AssociatedOid for RaTlsQuoteExtension {
const OID: ObjectIdentifier = ID_GRAMINE_RA_TLS_QUOTE;
}
let doc = match oid {
P256 => {
use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_ASN1_SIGNING as ALG};
EcdsaKeyPair::generate_pkcs8(&ALG, &rand)?
}
P384 => {
use ring::signature::{EcdsaKeyPair, ECDSA_P384_SHA384_ASN1_SIGNING as ALG};
EcdsaKeyPair::generate_pkcs8(&ALG, &rand)?
}
_ => return Err(anyhow!("unsupported")),
};
Ok(doc.as_ref().to_vec().into())
impl x509_cert::der::Encode for RaTlsQuoteExtension {
fn encoded_len(&self) -> pkcs8::der::Result<Length> {
unimplemented!()
}
fn public_key(&self) -> Result<SubjectPublicKeyInfoRef<'_>> {
match self.algorithm.oids()? {
(ECPK, ..) => {
let ec = EcPrivateKey::from_der(self.private_key)?;
let pk = ec.public_key.ok_or_else(|| anyhow!("missing public key"))?;
Ok(SubjectPublicKeyInfo {
algorithm: self.algorithm,
subject_public_key: BitStringRef::new(0, pk)?,
})
}
_ => Err(anyhow!("unsupported")),
}
fn encode(
&self,
_writer: &mut impl x509_cert::der::Writer,
) -> Result<(), x509_cert::der::Error> {
unimplemented!()
}
}
impl AsExtension for RaTlsQuoteExtension {
fn critical(&self, _: &x509_cert::name::Name, _: &[x509_cert::ext::Extension]) -> bool {
false
}
fn to_extension(
&self,
_subject: &Name,
_extensions: &[Extension],
) -> std::result::Result<Extension, der::Error> {
Ok(Extension {
extn_id: <Self as AssociatedOid>::OID,
critical: false,
extn_value: OctetString::new(self.quote.as_slice())?,
})
}
}
/// The `gramine-ra-tls` x509 extension
pub struct RaTlsCollateralExtension {
/// The hash of the certificate's public key
pub collateral: Vec<u8>,
}
impl AssociatedOid for RaTlsCollateralExtension {
const OID: ObjectIdentifier = ID_GRAMINE_RA_TLS_COLLATERAL;
}
impl x509_cert::der::Encode for RaTlsCollateralExtension {
fn encoded_len(&self) -> pkcs8::der::Result<Length> {
unimplemented!()
}
fn signs_with(&self) -> Result<AlgorithmIdentifierRef<'_>> {
match self.algorithm.oids()? {
(ECPK, Some(P256)) => Ok(ES256),
(ECPK, Some(P384)) => Ok(ES384),
_ => Err(anyhow!("unsupported")),
}
fn encode(
&self,
_writer: &mut impl x509_cert::der::Writer,
) -> Result<(), x509_cert::der::Error> {
unimplemented!()
}
}
fn sign(&self, body: &[u8], algo: AlgorithmIdentifierRef<'_>) -> Result<Vec<u8>> {
let rng = ring::rand::SystemRandom::new();
match (self.algorithm.oids()?, algo) {
((ECPK, Some(P256)), ES256) => {
use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_ASN1_SIGNING as ALG};
let kp = EcdsaKeyPair::from_pkcs8(&ALG, &self.to_der()?, &rng)?;
Ok(kp.sign(&rng, body)?.as_ref().to_vec())
}
((ECPK, Some(P384)), ES384) => {
use ring::signature::{EcdsaKeyPair, ECDSA_P384_SHA384_ASN1_SIGNING as ALG};
let kp = EcdsaKeyPair::from_pkcs8(&ALG, &self.to_der()?, &rng)?;
Ok(kp.sign(&rng, body)?.as_ref().to_vec())
}
_ => Err(anyhow!("unsupported")),
}
impl AsExtension for RaTlsCollateralExtension {
fn critical(&self, _: &x509_cert::name::Name, _: &[x509_cert::ext::Extension]) -> bool {
false
}
fn to_extension(
&self,
_subject: &Name,
_extensions: &[Extension],
) -> std::result::Result<Extension, der::Error> {
Ok(Extension {
extn_id: <Self as AssociatedOid>::OID,
critical: false,
extn_value: OctetString::new(self.collateral.as_slice())?,
})
}
}
/// Create a private key and a self-signed certificate
pub fn make_self_signed_cert() -> Result<(
pub fn make_self_signed_cert(
dn: &str,
an: Option<GeneralNames>,
) -> Result<(
[u8; 64],
rustls::pki_types::CertificateDer<'static>,
rustls::pki_types::PrivateKeyDer<'static>,
)> {
// Generate a keypair.
let raw = PrivateKeyInfo::generate(SECP_256_R_1).context("failed to generate a private key")?;
let pki = PrivateKeyInfo::from_der(raw.as_ref())
.context("failed to parse DER-encoded private key")?;
let der = pki.public_key().unwrap().to_der().unwrap();
let mut rng = rand::thread_rng();
let signing_key = p256::ecdsa::SigningKey::random(&mut rng);
let verifying_key = signing_key.verifying_key();
let verifying_key_der = verifying_key
.to_public_key_der()
.context("failed to create public key der")?;
let mut key_hash = [0u8; 64];
let hash = Sha256::digest(der);
let hash = Sha256::digest(verifying_key_der.as_bytes());
key_hash[..32].copy_from_slice(&hash);
let quote = get_quote(&key_hash)?;
debug!("quote.len: {:?}", quote.len());
// Create a relative distinguished name.
let rdns = RdnSequence::from_str("CN=localhost")?;
// Create the extensions.
let ku = KeyUsage(KeyUsages::DigitalSignature | KeyUsages::KeyEncipherment).to_der()?;
let eu = ExtendedKeyUsage(vec![ID_KP_SERVER_AUTH, ID_KP_CLIENT_AUTH]).to_der()?;
let bc = BasicConstraints {
ca: false,
path_len_constraint: None,
}
.to_der()?;
let rdns = RdnSequence::from_str(dn)?;
let collateral = tee_qv_get_collateral(&quote).context("Failed to get own collateral")?;
let mut serial = [0u8; 16];
getrandom(&mut serial)?;
// Create the certificate body.
let tbs = TbsCertificate {
version: x509_cert::Version::V3,
serial_number: SerialNumber::new(&serial)?,
signature: pki.signs_with()?.ref_to_owned(),
issuer: rdns.clone(),
validity: Validity::from_now(Duration::from_secs(60 * 60 * 24 * 365))?,
subject: rdns,
subject_public_key_info: pki.public_key()?.ref_to_owned(),
issuer_unique_id: None,
subject_unique_id: None,
extensions: Some(vec![
x509_cert::ext::Extension {
extn_id: ID_CE_KEY_USAGE,
critical: true,
extn_value: OctetString::new(ku)?,
},
x509_cert::ext::Extension {
extn_id: ID_CE_BASIC_CONSTRAINTS,
critical: true,
extn_value: OctetString::new(bc)?,
},
x509_cert::ext::Extension {
extn_id: ID_CE_EXT_KEY_USAGE,
critical: false,
extn_value: OctetString::new(eu)?,
},
]),
};
let mut builder = CertificateBuilder::new(
Profile::Leaf {
issuer: rdns.clone(),
enable_key_agreement: true,
enable_key_encipherment: true,
},
SerialNumber::new(&serial)?,
Validity::from_now(Duration::from_secs(60 * 60 * 24 * 365 * 10))?,
rdns,
SubjectPublicKeyInfoOwned::try_from(verifying_key_der.as_bytes())
.context("failed to create SubjectPublicKeyInfo")?,
&signing_key,
)
.context("failed to create CertificateBuilder")?;
// Self-sign the certificate.
let alg = tbs.signature.clone();
let sig = pki.sign(&tbs.to_der()?, alg.owned_to_ref())?;
let crt = Certificate {
tbs_certificate: tbs,
signature_algorithm: alg,
signature: BitString::from_bytes(&sig)?,
};
builder
.add_extension(&ExtendedKeyUsage(vec![
ID_KP_SERVER_AUTH,
ID_KP_CLIENT_AUTH,
]))
.context("failed to add ExtendedKeyUsage")?;
if let Some(an) = an {
builder
.add_extension(&SubjectAltName(an))
.context("failed to add SubjectAltName")?;
}
builder
.add_extension(&RaTlsQuoteExtension {
quote: quote.to_vec(),
})
.context("failed to add GRAMINE_RA_TLS")?;
builder
.add_extension(&RaTlsCollateralExtension {
collateral: serde_json::to_vec(&collateral).context("failed to add GRAMINE_RA_TLS")?,
})
.context("failed to add GRAMINE_RA_TLS")?;
let crt = builder.build::<DerSignature>().unwrap();
let rustls_certificate = rustls::pki_types::CertificateDer::from(crt.to_der()?);
let rustls_pk = rustls::pki_types::PrivateKeyDer::from(PrivatePkcs8KeyDer::from(pki.to_der()?));
let signing_key_der = signing_key
.to_pkcs8_der()
.context("failed to encode PKCS#8")?;
let rustls_pk = rustls::pki_types::PrivateKeyDer::from(PrivatePkcs8KeyDer::from(
signing_key_der.as_bytes(),
))
.clone_key();
Ok((key_hash, rustls_certificate, rustls_pk))
}
/// Create a private key and a self-signed certificate
pub fn make_signed_cert<S, Signature>(
dn: &str,
an: Option<GeneralNames>,
issuer_cert: &Certificate,
issuer_key: &S,
) -> Result<([u8; 64], String, Zeroizing<String>)>
where
Signature: SignatureBitStringEncoding,
S: signature::Keypair + DynSignatureAlgorithmIdentifier + Signer<Signature>,
S::VerifyingKey: EncodePublicKey,
{
// Generate a keypair.
let mut rng = rand::thread_rng();
let signing_key = p256::ecdsa::SigningKey::random(&mut rng);
let verifying_key = signing_key.verifying_key();
let verifying_key_der = verifying_key
.to_public_key_der()
.context("failed to create public key der")?;
let mut key_hash = [0u8; 64];
let hash = Sha256::digest(verifying_key_der.as_bytes());
key_hash[..32].copy_from_slice(&hash);
let quote = get_quote(&key_hash).context("Failed to get own quote")?;
// Create a relative distinguished name.
let subject = Name::from_str(dn)?;
let mut serial = [0u8; 16];
getrandom(&mut serial)?;
let mut builder = CertificateBuilder::new(
Profile::Leaf {
issuer: issuer_cert.tbs_certificate.subject.clone(),
enable_key_agreement: true,
enable_key_encipherment: true,
},
SerialNumber::new(&serial)?,
Validity::from_now(Duration::from_secs(60 * 60 * 24 * 365 * 10))?,
subject,
SubjectPublicKeyInfoOwned::try_from(verifying_key_der.as_bytes())
.context("failed to create SubjectPublicKeyInfo")?,
issuer_key,
)
.context("failed to create CertificateBuilder")?;
builder
.add_extension(&ExtendedKeyUsage(vec![
ID_KP_SERVER_AUTH,
ID_KP_CLIENT_AUTH,
]))
.context("failed to add ExtendedKeyUsage")?;
if let Some(an) = an {
builder
.add_extension(&SubjectAltName(an))
.context("failed to add SubjectAltName")?;
}
builder
.add_extension(&RaTlsQuoteExtension {
quote: quote.to_vec(),
})
.context("failed to add GRAMINE_RA_TLS")?;
let crt = builder.build::<Signature>().unwrap();
let cert_pem = crt.to_pem(LineEnding::LF)?;
let key_pem = signing_key.to_pkcs8_pem(LineEnding::LF)?;
Ok((key_hash, cert_pem, key_pem))
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) 2023-2024 Matter Labs
// Copyright (c) The Enarx Project Developers https://github.com/enarx/sgx
@ -114,7 +114,7 @@ pub struct QuoteVerificationResult<'a> {
pub quote: &'a Quote,
}
/// Verifies a quote with collateral material
/// Verifies a quote with optional collateral material
pub fn verify_quote_with_collateral<'a>(
quote: &'a [u8],
collateral: Option<&Collateral>,

View file

@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024 Matter Labs
mod sgx {
use anyhow::Result;
@ -23,7 +24,7 @@ mod sgx {
tcb_level_date_tag,
} = verify_quote_with_collateral(quote, Some(collateral), current_time).unwrap();
if collateral_expired || !matches!(result, sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK) {
if collateral_expired || result != sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK {
print!("Attestation failed: ");
if collateral_expired {