feat: initial commit

Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
This commit is contained in:
Harald Hoyer 2024-02-09 10:10:53 +01:00
parent aff4dd30bd
commit 89ffbd35a8
Signed by: harald
GPG key ID: F519A1143B3FBE32
123 changed files with 16508 additions and 0 deletions

305
src/client/mod.rs Normal file
View file

@ -0,0 +1,305 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
//! Helper functions for CLI clients to verify Intel SGX enclaves and other TEEs.
#![deny(missing_docs)]
#![deny(clippy::all)]
pub mod vault;
use crate::json::http::AttestationResponse;
use crate::sgx::Collateral;
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 awc::{Client, Connector};
use clap::Args;
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::time;
use std::time::Duration;
use tracing::{error, info, warn};
use x509_cert::der::{Decode as _, Encode as _};
use x509_cert::Certificate;
/// Options and arguments needed to attest a TEE
#[derive(Args, Debug, Clone)]
pub struct AttestationArgs {
/// hex encoded SGX mrsigner of the enclave to attest
#[arg(long)]
pub sgx_mrsigner: Option<String>,
/// hex encoded SGX mrenclave of the enclave to attest
#[arg(long)]
pub sgx_mrenclave: Option<String>,
/// URL of the server
#[arg(long, required = true)]
pub server: String,
/// allowed TCB levels, comma separated:
/// Ok, ConfigNeeded, ConfigAndSwHardeningNeeded, SwHardeningNeeded, OutOfDate, OutOfDateConfigNeeded
#[arg(long, value_parser = parse_tcb_levels)]
pub sgx_allowed_tcb_levels: Option<EnumSet<TcbLevel>>,
}
/// A connection to a TEE, which implements the `teepot` attestation API
pub struct TeeConnection {
/// Options and arguments needed to attest a TEE
server: String,
client: Client,
}
impl TeeConnection {
/// Create a new connection to a TEE
///
/// 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());
let tls_config = Arc::new(
ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(Self::make_verifier(pk_hash.clone())))
.with_no_client_auth(),
);
let agent = 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(tls_config))
.timeout(Duration::from_secs(12000))
.finish();
let this = 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
///
/// # Safety
/// This function is unsafe, because it does not verify the attestation report.
pub unsafe fn new_from_client_without_attestation(server: String, client: Client) -> Self {
Self { server, client }
}
/// Get a reference to the agent, which can be used to make requests to the TEE
///
/// Note, that it will refuse to connect to any other TLS server than the one
/// specified in the `AttestationArgs` of the `Self::new` function.
pub fn client(&self) -> &Client {
&self.client
}
/// Get a reference to the server URL
pub fn server(&self) -> &str {
&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 {
#[derive(Debug)]
struct V {
pk_hash: Arc<OnceLock<[u8; 32]>>,
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,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
let cert = Certificate::from_der(end_entity.as_ref())
.map_err(|e| Error::General(format!("Failed get certificate {e:?}")))?;
let pub_key = cert
.tbs_certificate
.subject_public_key_info
.to_der()
.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(),
))
}
}
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 {
pk_hash,
server_verifier,
}
}
}

393
src/client/vault.rs Normal file
View file

@ -0,0 +1,393 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! Helper functions for CLI clients to verify Intel SGX enclaves and other TEEs.
#![deny(missing_docs)]
#![deny(clippy::all)]
use super::{AttestationArgs, TeeConnection};
use crate::json::http::{AuthRequest, AuthResponse};
use crate::server::pki::make_self_signed_cert;
use crate::server::{AnyHowResponseError, HttpResponseError, Status};
pub use crate::sgx::{
parse_tcb_levels, sgx_gramine_get_quote, sgx_ql_qv_result_t, tee_qv_get_collateral,
verify_quote_with_collateral, Collateral, EnumSet, QuoteVerificationResult, TcbLevel,
};
use actix_http::error::PayloadError;
use actix_web::http::header;
use actix_web::ResponseError;
use anyhow::{anyhow, bail, Context, Result};
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::time;
use tracing::{debug, error, info, trace};
const VAULT_TOKEN_HEADER: &str = "X-Vault-Token";
/// Error returned when sending a request to Vault
#[derive(Debug, thiserror::Error)]
pub enum VaultSendError {
/// Error sending the request
SendRequest(String),
/// Error returned by the Vault API
#[error(transparent)]
Vault(#[from] HttpResponseError),
}
impl Display for VaultSendError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
VaultSendError::SendRequest(e) => write!(f, "VaultSendError: {}", e),
VaultSendError::Vault(e) => write!(f, "VaultSendError: {}", e),
}
}
}
const _: () = {
fn assert_send<T: Send>() {}
let _ = assert_send::<VaultSendError>;
};
impl From<VaultSendError> for HttpResponseError {
fn from(value: VaultSendError) -> Self {
match value {
VaultSendError::SendRequest(e) => HttpResponseError::Anyhow(AnyHowResponseError {
status_code: StatusCode::BAD_GATEWAY,
error: anyhow!(e),
}),
VaultSendError::Vault(e) => e,
}
}
}
/// A connection to a Vault TEE, which implements the `teepot` attestation API
/// called by a TEE itself. This authenticates the TEE to Vault and gets a token,
/// which can be used to access the Vault API.
pub struct VaultConnection {
/// Options and arguments needed to attest Vault
pub conn: TeeConnection,
key_hash: [u8; 64],
client_token: String,
name: String,
}
impl VaultConnection {
/// Create a new connection to Vault
///
/// 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 tls_config = Arc::new(
ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(TeeConnection::make_verifier(
pk_hash.clone(),
)))
.with_client_auth_cert(vec![rustls_certificate], rustls_pk)?,
);
let 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(tls_config))
.timeout(time::Duration::from_secs(12000))
.finish();
let mut this = Self {
name,
key_hash,
conn: unsafe {
TeeConnection::new_from_client_without_attestation(args.server.clone(), client)
},
client_token: Default::default(),
};
this.client_token = this.auth(args).await?.auth.client_token;
trace!("Got Token: {:#?}", &this.client_token);
Ok(this)
}
/// create a new [`VaultConnection`] to Vault from an existing connection
///
/// # Safety
/// This function is unsafe, because it does not verify the attestation report.
pub unsafe fn new_from_client_without_attestation(
server: String,
client: Client,
name: String,
client_token: String,
) -> Self {
Self {
name,
client_token,
conn: unsafe { TeeConnection::new_from_client_without_attestation(server, client) },
key_hash: [0u8; 64],
}
}
/// Get a reference to the agent, which can be used to make requests to the TEE
///
/// Note, that it will refuse to connect to any other TLS server than the one
/// specified in the `AttestationArgs` of the `Self::new` function.
pub fn agent(&self) -> &Client {
self.conn.client()
}
async fn auth(&self, args: &AttestationArgs) -> 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,
};
let mut response = self
.agent()
.post(&format!(
"{server}{attestation_url}",
server = self.conn.server,
))
.send_json(&auth_req)
.await
.map_err(|e| anyhow!("Error sending attestation request: {}", e))?;
let status_code = response.status();
if !status_code.is_success() {
error!("Failed to login to vault: {}", status_code);
if let Ok(r) = response.json::<Value>().await {
eprintln!("Failed to login to vault: {}", r);
}
bail!("failed to login to vault: {}", status_code);
}
let auth_response: Value = response.json().await.context("failed to login to vault")?;
trace!(
"Got AuthResponse: {:?}",
serde_json::to_string(&auth_response)
);
let auth_response: AuthResponse =
serde_json::from_value(auth_response).context("Failed to parse AuthResponse")?;
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)
}
/// Send a put request to the vault
pub async fn vault_put(
&self,
action: &str,
url: &str,
json: &Value,
) -> std::result::Result<(StatusCode, Option<Value>), VaultSendError> {
let full_url = format!("{}{url}", self.conn.server);
info!("{action} via put {full_url}");
debug!(
"sending json: {:?}",
serde_json::to_string(json).unwrap_or_default()
);
let res = self
.agent()
.put(full_url)
.insert_header((VAULT_TOKEN_HEADER, self.client_token.clone()))
.send_json(json)
.await;
Self::handle_client_response(action, res).await
}
/// Send a get request to the vault
pub async fn vault_get(
&self,
action: &str,
url: &str,
) -> std::result::Result<(StatusCode, Option<Value>), VaultSendError> {
let full_url = format!("{}{url}", self.conn.server);
info!("{action} via get {full_url}");
let res = self
.agent()
.get(full_url)
.insert_header((VAULT_TOKEN_HEADER, self.client_token.clone()))
.send()
.await;
Self::handle_client_response(action, res).await
}
async fn handle_client_response<S>(
action: &str,
res: std::result::Result<ClientResponse<S>, SendRequestError>,
) -> std::result::Result<(StatusCode, Option<Value>), VaultSendError>
where
S: Stream<Item = Result<Bytes, PayloadError>>,
{
match res {
Ok(mut r) => {
let status_code = r.status();
if status_code.is_success() {
let msg = r.json().await.ok();
debug!(
"{action}: status code: {status_code} {:?}",
serde_json::to_string(&msg)
);
Ok((status_code, msg))
} else {
let err = HttpResponseError::from_proxy(r).await;
error!("{action}: {err:?}");
Err(err.into())
}
}
Err(e) => {
error!("{}: {}", action, e);
Err(VaultSendError::SendRequest(e.to_string()))
}
}
}
/// Revoke the token
pub async fn revoke_token(&self) -> std::result::Result<(), VaultSendError> {
self.vault_put(
"Revoke the token",
"/v1/auth/token/revoke-self",
&Value::default(),
)
.await?;
Ok(())
}
fn check_rel_path(rel_path: &str) -> Result<(), HttpResponseError> {
if !rel_path.is_ascii() {
return Err(anyhow!("path is not ascii")).status(StatusCode::BAD_REQUEST);
}
// check if rel_path is alphanumeric
if !rel_path.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(anyhow!("path is not alphanumeric")).status(StatusCode::BAD_REQUEST);
}
Ok(())
}
/// set a secret in the vault
pub async fn store_secret<'de, T: serde::Serialize>(
&self,
val: T,
rel_path: &str,
) -> Result<(), HttpResponseError> {
self.store_secret_for_tee(&self.name, val, rel_path).await
}
/// set a secret in the vault for a different TEE
pub async fn store_secret_for_tee<'de, T: serde::Serialize>(
&self,
tee_name: &str,
val: T,
rel_path: &str,
) -> Result<(), HttpResponseError> {
Self::check_rel_path(rel_path)?;
let value = serde_json::to_value(val)
.context("converting value to json")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
let value = json!({ "data" : { "_" : value } });
self.vault_put(
"Setting secret",
&format!("/v1/secret/data/tee/{}/{}", tee_name, rel_path),
&value,
)
.await?;
Ok(())
}
/// get a secret from the vault
pub async fn load_secret<'de, T: serde::de::DeserializeOwned>(
&self,
rel_path: &str,
) -> Result<Option<T>, HttpResponseError> {
self.load_secret_for_tee(&self.name, rel_path).await
}
/// get a secret from the vault for a specific TEE
pub async fn load_secret_for_tee<'de, T: serde::de::DeserializeOwned>(
&self,
tee_name: &str,
rel_path: &str,
) -> Result<Option<T>, HttpResponseError> {
Self::check_rel_path(rel_path)?;
let v = self
.vault_get(
"Getting secret",
&format!("/v1/secret/data/tee/{}/{}", tee_name, rel_path),
)
.await
.or_else(|e| match e {
VaultSendError::Vault(ref se) => {
if se.status_code() == StatusCode::NOT_FOUND {
debug!("Secret not found: {}", rel_path);
Ok((StatusCode::OK, None))
} else {
Err(e)
}
}
VaultSendError::SendRequest(_) => Err(e),
})?
.1
.as_ref()
.and_then(|v| v.get("data"))
.and_then(|v| v.get("data"))
.and_then(|v| v.get("_"))
.and_then(|v| serde_json::from_value(v.clone()).transpose())
.transpose()
.context("Error getting value from vault")
.status(StatusCode::INTERNAL_SERVER_ERROR)?
.flatten();
Ok(v)
}
}

280
src/json/http.rs Normal file
View file

@ -0,0 +1,280 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 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 std::fmt::Display;
use std::sync::Arc;
/// The unseal request data
#[derive(Debug, Serialize, Deserialize)]
pub struct Unseal {
/// The unseal key
pub key: String,
}
impl Unseal {
/// The unseal URL
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 {
/// The quote
pub quote: Arc<[u8]>,
/// The collateral
pub collateral: Arc<Collateral>,
}
/// The init request data
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
pub struct Init {
/// PGP keys to encrypt the unseal keys with
pub pgp_keys: Vec<String>,
/// number of secret shares
pub secret_shares: usize,
/// secret threshold
pub secret_threshold: usize,
/// PGP keys to sign commands for the admin tee
#[serde_as(as = "Box<[Base64]>")]
pub admin_pgp_keys: Box<[Box<[u8]>]>,
/// admin threshold
pub admin_threshold: usize,
/// admin TEE mrenclave
pub admin_tee_mrenclave: String,
}
impl Init {
/// The init URL
pub const URL: &'static str = "/v1/sys/init";
}
/// The init request data
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultInitRequest {
/// PGP keys to encrypt the unseal keys with
pub pgp_keys: Vec<String>,
/// number of secret shares
pub secret_shares: usize,
/// secret threshold
pub secret_threshold: usize,
}
/// The init response data
#[derive(Debug, Serialize, Deserialize)]
pub struct InitResponse {
/// The unseal keys (gpg encrypted)
pub unseal_keys: Vec<String>,
}
/// The Vault TEE auth request data
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthRequest {
/// The name of the TEE
pub name: String,
/// The type of the TEE
#[serde(rename = "type")]
pub tee_type: String,
/// The attestation report data base64 encoded
#[serde_as(as = "Base64")]
pub quote: Box<[u8]>,
/// The attestation collateral json encoded
pub collateral: String,
/// The vault attestation challenge (hex encoded)
#[serde_as(as = "Option<serde_with::hex::Hex>")]
#[serde(skip_serializing_if = "Option::is_none", default = "Option::default")]
pub challenge: Option<[u8; 32]>,
}
impl AuthRequest {
/// The auth URL
pub const URL: &'static str = "/v1/auth/tee/login";
}
/// Vault auth metadata
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuthMetadataField {
collateral_expiration_date: String,
tee_name: String,
}
/// Vault auth data
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuthDataField {
/// The attestation report data base64 encoded
#[serde_as(as = "Base64")]
#[serde(default)]
pub quote: Box<[u8]>,
/// The attestation collateral json encoded
#[serde(default)]
pub collateral: String,
}
/// Vault auth
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuthField {
/// TODO
pub renewable: bool,
/// TODO
pub lease_duration: isize,
/// TODO
pub policies: Vec<String>,
/// TODO
pub accessor: String,
/// TODO
pub client_token: String,
/// TODO
pub metadata: AuthMetadataField,
}
/// The Vault TEE auth response data
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuthResponse {
/// vault auth
pub auth: AuthField,
///
pub data: AuthDataField,
}
/// One command datum
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct VaultCommand {
/// The command to execute
pub url: String,
/// The command to execute
pub data: Value,
}
impl Display for VaultCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
f.write_str(
serde_json::to_string_pretty(self)
.unwrap_or("{}".into())
.as_str(),
)
} else {
f.write_str(serde_json::to_string(self).unwrap_or("{}".into()).as_str())
}
}
}
/// Multiple command data
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct VaultCommands {
/// The sha-256 hash of the last command hex encoded
pub last_digest: String,
/// The actual commands
pub commands: Vec<VaultCommand>,
}
/// The command request data
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultCommandRequest {
/// The commands to execute
///
/// The commands are json serialized `VaultCommands`,
/// because they are signed with multiple signatures.
///
/// The commands are executed in order.
pub commands: String,
/// The signatures of the commands
pub signatures: Vec<String>,
}
impl VaultCommandRequest {
/// The command request URL
pub const URL: &'static str = "/v1/command";
}
/// The command response
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultCommandResponse {
/// The status code
pub status_code: u16,
/// The response body
pub value: Option<Value>,
}
/// The command response
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultCommandsResponse {
/// The stored digest for the execution
pub digest: String,
/// The results of the individual commands
pub results: Vec<VaultCommandResponse>,
}
impl Display for VaultCommandResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
f.write_str(
serde_json::to_string_pretty(self)
.unwrap_or("{}".into())
.as_str(),
)
} else {
f.write_str(serde_json::to_string(self).unwrap_or("{}".into()).as_str())
}
}
}
/// The command request URL
pub const DIGEST_URL: &str = "/v1/digest";
/// The signing request
#[derive(Debug, Serialize, Deserialize)]
pub struct SignRequest {
/// json serialized `SignRequestData`, because it is signed with multiple signatures.
pub sign_request_data: String,
/// The signatures of the SignRequestData
pub signatures: Vec<String>,
}
impl SignRequest {
/// The sign request URL
pub const URL: &'static str = "/v1/sign";
}
/// The signing request data
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct SignRequestData {
/// The sha-256 hash of the last command hex encoded
pub last_digest: String,
/// The name of the TEE
pub tee_name: String,
/// The type of the TEE
#[serde(rename = "type")]
pub tee_type: String,
/// The TEE security version number
pub tee_svn: u16,
/// The data to be signed.
///
/// In case of `tee_type == "sgx"`, it's the SGX Sigstruct Body
#[serde_as(as = "Base64")]
pub data: Vec<u8>,
}
/// The signing request
#[derive(Debug, Serialize, Deserialize)]
pub struct SignResponse {
/// The stored digest for the execution
pub digest: String,
/// The signed data for the tee.
///
/// In case of `tee_type == "sgx"`, it's the SGX Sigstruct
pub signed_data: Vec<u8>,
}

10
src/json/mod.rs Normal file
View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! Common types for the teepot JSON API
#![deny(missing_docs)]
#![deny(clippy::all)]
pub mod http;
pub mod secrets;

34
src/json/secrets.rs Normal file
View file

@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 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;
/// Configuration for the admin tee
#[serde_as]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AdminConfig {
/// PGP keys to sign commands for the admin tee
#[serde_as(as = "Box<[Base64]>")]
pub admin_pgp_keys: Box<[Box<[u8]>]>,
/// admin threshold
pub admin_threshold: usize,
}
/// Configuration for the admin tee
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct AdminState {
/// last digest of executed commands
pub last_digest: String,
}
/// Configuration for the admin tee
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct SGXSigningKey {
/// private key in PEM format
pub pem_pk: Zeroizing<String>,
}

14
src/lib.rs Normal file
View file

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 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 sgx;
pub mod quote;

40
src/quote/mod.rs Normal file
View file

@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
//! Get a quote from a TEE
use crate::sgx::sgx_gramine_get_quote;
use std::io;
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
#[error("{msg}")]
pub struct GetQuoteError {
pub(crate) msg: Box<str>,
#[source] // optional if field name is `source`
pub(crate) source: io::Error,
}
/// Get the attestation quote from a TEE
pub fn get_quote(report_data: &[u8]) -> Result<Box<[u8]>, GetQuoteError> {
// check, if we are running in a TEE
if std::fs::metadata("/dev/attestation").is_ok() {
if report_data.len() > 64 {
return Err(GetQuoteError {
msg: "Report data too long".into(),
source: io::Error::new(io::ErrorKind::Other, "Report data too long"),
});
}
let mut report_data_fixed = [0u8; 64];
report_data_fixed[..report_data.len()].copy_from_slice(report_data);
sgx_gramine_get_quote(&report_data_fixed)
} else {
// if not, return an error
Err(GetQuoteError {
msg: "Not running in a TEE".into(),
source: io::Error::new(io::ErrorKind::Other, "Not running in a TEE"),
})
}
}

147
src/server/attestation.rs Normal file
View file

@ -0,0 +1,147 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! Common attestation API for all TEEs
use crate::client::AttestationArgs;
use crate::json::http::AttestationResponse;
use crate::sgx::{
parse_tcb_levels, sgx_gramine_get_quote, tee_qv_get_collateral, verify_quote_with_collateral,
Collateral, EnumSet, QuoteVerificationResult, TcbLevel,
};
use anyhow::{bail, Context, Result};
use clap::Args;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, RwLock};
use std::time::{Duration, UNIX_EPOCH};
use tracing::{debug, error, info, trace, warn};
struct Attestation {
quote: Arc<[u8]>,
collateral: Arc<Collateral>,
report_data: [u8; 64],
earliest_expiration_date: i64,
}
/// Returns the quote and collateral for the current TEE.
///
/// if `allowed_tcb_levels` is `None`, then any TCB level is accepted.
/// Otherwise, the quote must be verified and the collateral must be
/// within the allowed TCB levels.
pub fn get_quote_and_collateral(
allowed_tcb_levels: Option<EnumSet<TcbLevel>>,
report_data: &[u8; 64],
) -> Result<AttestationResponse> {
static ATTESTATION: RwLock<Option<Attestation>> = RwLock::new(None);
let unix_time: i64 = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as _;
if let Some(attestation) = ATTESTATION.read().unwrap().as_ref() {
trace!(attestation.earliest_expiration_date);
if attestation.earliest_expiration_date > unix_time.saturating_add(60)
&& report_data.eq(&attestation.report_data)
{
debug!("return cache attestation quote and collateral");
return Ok(AttestationResponse {
quote: attestation.quote.clone(),
collateral: attestation.collateral.clone(),
});
}
}
let myquote = sgx_gramine_get_quote(report_data).context("Failed to get own quote")?;
let collateral = tee_qv_get_collateral(&myquote).context("Failed to get own collateral")?;
let QuoteVerificationResult {
collateral_expired,
result,
earliest_expiration_date,
tcb_level_date_tag,
quote,
advisories,
} = verify_quote_with_collateral(&myquote, Some(&collateral), unix_time.saturating_add(60))
.context("Failed to verify own quote with collateral")?;
debug!(tcb_level_date_tag);
if collateral_expired {
bail!("Freshly fetched collateral expired");
}
let tcblevel = TcbLevel::from(result);
if tcblevel != TcbLevel::Ok
&& allowed_tcb_levels.map_or(false, |levels| !levels.contains(tcblevel))
{
error!("Quote verification result: {}", tcblevel);
bail!("Quote verification result: {}", tcblevel);
}
for advisory in advisories {
warn!("\tInfo: Advisory ID: {advisory}");
}
info!("Own quote verified successfully: {}", tcblevel);
info!(
"Earliest expiration in {:?}",
Duration::from_secs((earliest_expiration_date - unix_time) as _)
);
info!("mrsigner: {}", hex::encode(quote.report_body.mrsigner));
info!("mrenclave: {}", hex::encode(quote.report_body.mrenclave));
let quote: Arc<[u8]> = Arc::from(myquote);
let collateral = Arc::from(collateral);
let mut attestation = ATTESTATION.write().unwrap();
*attestation = Some(Attestation {
quote: quote.clone(),
collateral: collateral.clone(),
report_data: *report_data,
earliest_expiration_date,
});
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,
}
}
}

181
src/server/mod.rs Normal file
View file

@ -0,0 +1,181 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! # tee-server
#![deny(missing_docs)]
#![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 anyhow::anyhow;
use awc::error::{PayloadError, SendRequestError};
use awc::ClientResponse;
use futures_core::Stream;
use std::fmt::{Debug, Display, Formatter};
use tracing::error;
/// Anyhow error with an HTTP status code
pub struct AnyHowResponseError {
/// error message
pub error: anyhow::Error,
/// HTTP status code
pub status_code: StatusCode,
}
/// Proxy response error
pub struct ProxyResponseError {
/// HTTP status code
pub status_code: StatusCode,
/// HTTP body
pub body: Option<Bytes>,
/// HTTP content type
pub content_type: String,
}
/// custom HTTP response error
pub enum HttpResponseError {
/// Anyhow error
Anyhow(AnyHowResponseError),
/// Proxy error
Proxy(ProxyResponseError),
}
impl std::error::Error for HttpResponseError {}
/// Attach an HTTP status code to an anyhow error turning it into an HttpResponseError
pub trait Status {
/// The Ok type
type Ok;
/// Attach an HTTP status code to an anyhow error turning it into an HttpResponseError
fn status(self, status: StatusCode) -> Result<Self::Ok, HttpResponseError>;
}
impl<T> Status for Result<T, anyhow::Error> {
type Ok = T;
fn status(self, status: StatusCode) -> Result<T, HttpResponseError> {
match self {
Ok(value) => Ok(value),
Err(error) => Err(HttpResponseError::new(error, status)),
}
}
}
impl HttpResponseError {
fn new(error: anyhow::Error, status_code: StatusCode) -> Self {
Self::Anyhow(AnyHowResponseError { error, status_code })
}
/// Create a new HTTP response error from a proxy response
pub async fn from_proxy<S>(mut response: ClientResponse<S>) -> Self
where
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();
Self::Proxy(ProxyResponseError {
status_code,
body,
content_type,
})
}
}
impl From<&str> for HttpResponseError {
fn from(value: &str) -> Self {
error!("{}", value);
HttpResponseError::new(
anyhow!(value.to_string()),
StatusCode::INTERNAL_SERVER_ERROR,
)
}
}
impl From<SendRequestError> for HttpResponseError {
fn from(error: SendRequestError) -> Self {
error!("Error sending request: {:?}", error);
HttpResponseError::new(
anyhow!(error.to_string()),
StatusCode::INTERNAL_SERVER_ERROR,
)
}
}
impl Debug for HttpResponseError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Self::Anyhow(e) = self {
if f.alternate() {
write!(f, "{:#?}", e.error)
} else {
write!(f, "{:?}", e.error)
}
} else {
write!(f, "HttpResponseError")
}
}
}
impl Display for HttpResponseError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Self::Anyhow(e) = self {
if f.alternate() {
write!(f, "{:#}", e.error)
} else {
write!(f, "{}", e.error)
}
} else {
write!(f, "HttpResponseError")
}
}
}
impl ResponseError for HttpResponseError {
fn status_code(&self) -> StatusCode {
match self {
HttpResponseError::Anyhow(e) => e.status_code,
HttpResponseError::Proxy(e) => e.status_code,
}
}
fn error_response(&self) -> HttpResponse {
match self {
HttpResponseError::Anyhow(e) => HttpResponse::build(self.status_code())
.content_type("application/json")
.body(format!(r#"{{"error":"{}"}}"#, e.error)),
HttpResponseError::Proxy(e) => {
if let Some(ref body) = e.body {
HttpResponse::build(self.status_code())
.content_type(e.content_type.clone())
.body(body.clone())
} else {
HttpResponse::new(self.status_code())
}
}
}
}
}
/// Create a new json config
pub fn new_json_cfg() -> actix_web::web::JsonConfig {
actix_web::web::JsonConfig::default()
.limit(1024 * 1024)
.error_handler(json_error_handler)
}
fn json_error_handler(err: error::JsonPayloadError, _: &HttpRequest) -> actix_web::Error {
error::InternalError::from_response(
"",
HttpResponse::BadRequest()
.content_type("application/json")
.body(format!(r#"{{"error":"json error: {}"}}"#, err)),
)
.into()
}

215
src/server/pki.rs Normal file
View file

@ -0,0 +1,215 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
//! Some cryptographic utilities
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 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 rustls::pki_types::PrivatePkcs8KeyDer;
use sec1::EcPrivateKey;
use sha2::{Digest, Sha256};
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 x509_cert::serial_number::SerialNumber;
use x509_cert::time::Validity;
use x509_cert::{Certificate, TbsCertificate};
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;
const ES256: AlgorithmIdentifierRef<'static> = AlgorithmIdentifierRef {
oid: ECDSA_WITH_SHA_256,
parameters: None,
};
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>>;
}
impl<'a> PrivateKeyInfoExt for PrivateKeyInfo<'a> {
fn generate(oid: ObjectIdentifier) -> Result<Zeroizing<Vec<u8>>> {
let rand = ring::rand::SystemRandom::new();
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())
}
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 signs_with(&self) -> Result<AlgorithmIdentifierRef<'_>> {
match self.algorithm.oids()? {
(ECPK, Some(P256)) => Ok(ES256),
(ECPK, Some(P384)) => Ok(ES384),
_ => Err(anyhow!("unsupported")),
}
}
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")),
}
}
}
/// Create a private key and a self-signed certificate
pub fn make_self_signed_cert() -> 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 key_hash = [0u8; 64];
let hash = Sha256::digest(der);
key_hash[..32].copy_from_slice(&hash);
// 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 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)?,
},
]),
};
// 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)?,
};
let rustls_certificate = rustls::pki_types::CertificateDer::from(crt.to_der()?);
let rustls_pk = rustls::pki_types::PrivateKeyDer::from(PrivatePkcs8KeyDer::from(pki.to_der()?));
Ok((key_hash, rustls_certificate, rustls_pk))
}

120
src/server/signatures.rs Normal file
View file

@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! Signature checking utilities
use crate::json::secrets::AdminConfig;
use crate::server::{HttpResponseError, Status as _};
use actix_web::http::StatusCode;
use anyhow::{anyhow, bail, Context, Result};
use pgp::types::KeyTrait;
use pgp::{Deserializable, SignedPublicKey, StandaloneSignature};
use tracing::debug;
/// Verify a pgp signature for some message given some public keys
pub fn verify_sig(sig: &str, msg: &[u8], keys: &[SignedPublicKey]) -> anyhow::Result<usize> {
let (signatures, _) =
StandaloneSignature::from_string_many(sig).context(format!("reading signature {}", sig))?;
for signature in signatures {
let signature = match signature {
Ok(s) => s,
Err(e) => {
debug!("Failed to parse signature: {}", e);
continue;
}
};
for (pos, key) in keys.iter().enumerate() {
let actual_key = &key.primary_key;
if actual_key.is_signing_key() && signature.verify(&actual_key, msg).is_ok() {
return Ok(pos);
}
for sub_key in &key.public_subkeys {
if sub_key.is_signing_key() && signature.verify(sub_key, msg).is_ok() {
return Ok(pos);
}
}
}
}
eprintln!("Failed to verify signature for `{sig}`");
bail!("Failed to verify signature for `{sig}`");
}
/// Verify pgp signatures for a message with some threshold
pub fn check_sigs(
pgp_keys: &[Box<[u8]>],
threshold: usize,
signatures: &[String],
msg: &[u8],
) -> Result<(), HttpResponseError> {
let mut keys = Vec::new();
for bytes in pgp_keys {
let key = SignedPublicKey::from_bytes(bytes.as_ref())
.context("parsing public key")
.status(StatusCode::INTERNAL_SERVER_ERROR)?;
keys.push(key);
}
let mut verified: usize = 0;
for sig in signatures {
if let Ok(pos) = verify_sig(sig, msg, &keys) {
keys.remove(pos);
verified += 1;
}
if verified >= threshold {
break;
}
}
if verified < threshold {
return Err(anyhow!("not enough valid signatures")).status(StatusCode::BAD_REQUEST);
}
Ok(())
}
/// Verify pgp signatures for a message
pub trait VerifySig {
/// Verify pgp signatures for a message
fn check_sigs(&self, signatures: &[String], msg: &[u8]) -> Result<(), HttpResponseError>;
}
impl VerifySig for AdminConfig {
fn check_sigs(&self, signatures: &[String], msg: &[u8]) -> Result<(), HttpResponseError> {
check_sigs(&self.admin_pgp_keys, self.admin_threshold, signatures, msg)
}
}
#[cfg(test)]
mod tests {
use super::verify_sig;
use base64::{engine::general_purpose, Engine as _};
use pgp::{Deserializable, SignedPublicKey};
const TEST_DATA: &str = include_str!("../../tests/data/test.json");
// gpg --armor --local-user test@example.com --detach-sign bin/tee-vault-admin/tests/data/test.json
const TEST_SIG: &str = include_str!("../../tests/data/test.json.asc");
// gpg --armor --export 81A312C59D679D930FA9E8B06D728F29A2DBABF8 > bin/tee-vault-admin/tests/data/pub-81A312C59D679D930FA9E8B06D728F29A2DBABF8.asc
const TEST_KEY: &str =
include_str!("../../tests/data/pub-81A312C59D679D930FA9E8B06D728F29A2DBABF8.asc");
const TEST_KEY_BASE64: &str =
include_str!("../../tests/data/pub-81A312C59D679D930FA9E8B06D728F29A2DBABF8.b64");
#[test]
fn test_sig() {
let test_key = SignedPublicKey::from_string(TEST_KEY).unwrap().0;
verify_sig(TEST_SIG, TEST_DATA.as_bytes(), &[test_key]).unwrap();
}
#[test]
fn test_key_import() {
let str = TEST_KEY_BASE64.lines().collect::<String>();
let bytes = general_purpose::STANDARD.decode(str).unwrap();
let _ = SignedPublicKey::from_bytes(bytes.as_slice()).unwrap();
}
}

50
src/sgx/error.rs Normal file
View file

@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! Intel SGX Enclave error wrapper
use bytemuck::PodCastError;
use intel_tee_quote_verification_rs::quote3_error_t;
use std::fmt::Formatter;
/// Wrapper for the quote verification Error
#[derive(Copy, Clone)]
pub struct Quote3Error {
/// error message
pub msg: &'static str,
/// raw error code
pub inner: quote3_error_t,
}
impl std::fmt::Display for Quote3Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {:?}", self.msg, self.inner)
}
}
impl std::fmt::Debug for Quote3Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {:?}", self.msg, self.inner)
}
}
impl std::error::Error for Quote3Error {}
impl From<quote3_error_t> for Quote3Error {
fn from(inner: quote3_error_t) -> Self {
Self {
msg: "Generic",
inner,
}
}
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum QuoteFromError {
#[error(transparent)]
PodCastError(#[from] PodCastError),
#[error("Quote version is invalid")]
InvalidVersion,
}

249
src/sgx/mod.rs Normal file
View file

@ -0,0 +1,249 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) The Enarx Project Developers https://github.com/enarx/sgx
//! Intel SGX Enclave report structures.
pub mod error;
pub mod sign;
pub mod tcblevel;
use bytemuck::{cast_slice, try_from_bytes, AnyBitPattern, PodCastError};
use intel_tee_quote_verification_rs::{
quote3_error_t, sgx_ql_qv_supplemental_t, tee_get_supplemental_data_version_and_size,
tee_supp_data_descriptor_t, tee_verify_quote,
};
use std::ffi::CStr;
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::mem;
use tracing::{trace, warn};
use crate::quote::GetQuoteError;
pub use error::{Quote3Error, QuoteFromError};
pub use intel_tee_quote_verification_rs::{sgx_ql_qv_result_t, Collateral};
pub use tcblevel::{parse_tcb_levels, EnumSet, TcbLevel};
/// Structure of a quote
#[derive(Copy, Clone, Debug, AnyBitPattern)]
#[repr(C)]
pub struct Quote {
version: [u8; 2],
key_type: [u8; 2],
reserved: [u8; 4],
qe_svn: [u8; 2],
pce_svn: [u8; 2],
qe_vendor_id: [u8; 16],
/// The user data that was passed, when creating the enclave
pub user_data: [u8; 20],
/// The report body
pub report_body: ReportBody,
}
impl Quote {
/// Creates a quote from a byte slice
pub fn try_from_bytes(bytes: &[u8]) -> Result<&Self, QuoteFromError> {
if bytes.len() < mem::size_of::<Self>() {
return Err(PodCastError::SizeMismatch.into());
}
let this: &Self = try_from_bytes(&bytes[..mem::size_of::<Self>()])?;
if this.version() != 3 {
return Err(QuoteFromError::InvalidVersion);
}
Ok(this)
}
/// Version of the `Quote` structure
pub fn version(&self) -> u16 {
u16::from_le_bytes(self.version)
}
}
/// The enclave report body.
///
/// For more information see the following documents:
///
/// [Intel® Software Guard Extensions (Intel® SGX) Data Center Attestation Primitives: ECDSA Quote Library API](https://download.01.org/intel-sgx/dcap-1.0/docs/SGX_ECDSA_QuoteGenReference_DCAP_API_Linux_1.0.pdf)
///
/// Table 5, A.4. Quote Format
///
/// [Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3 (3A, 3B, 3C & 3D): System Programming Guide](https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3d-part-4-manual.html)
///
/// Table 38-21. Layout of REPORT
#[derive(Copy, Clone, Debug, AnyBitPattern)]
#[repr(C)]
pub struct ReportBody {
/// The security version number of the enclave.
pub cpusvn: [u8; 16],
/// The Misc section of the StateSaveArea of the enclave
pub miscselect: [u8; 4],
reserved1: [u8; 28],
/// The allowed Features of the enclave.
pub features: [u8; 8],
/// The allowed XCr0Flags of the enclave.
pub xfrm: [u8; 8],
/// The measurement of the enclave
pub mrenclave: [u8; 32],
reserved2: [u8; 32],
/// The hash of the public key, that signed the enclave
pub mrsigner: [u8; 32],
reserved3: [u8; 96],
/// ISV assigned Product ID of the enclave.
pub isv_prodid: [u8; 2],
/// ISV assigned SVN (security version number) of the enclave.
pub isv_svn: [u8; 2],
reserved4: [u8; 60],
/// The enclave report data, injected when requesting the quote, that is used for attestation.
pub reportdata: [u8; 64],
}
/// The result of the quote verification
pub struct QuoteVerificationResult<'a> {
/// the raw result
pub result: sgx_ql_qv_result_t,
/// indicates if the collateral is expired
pub collateral_expired: bool,
/// the earliest expiration date of the collateral
pub earliest_expiration_date: i64,
/// Date of the TCB level
pub tcb_level_date_tag: i64,
/// the advisory string
pub advisories: Vec<String>,
/// the quote
pub quote: &'a Quote,
}
/// Verifies a quote with collateral material
pub fn verify_quote_with_collateral<'a>(
quote: &'a [u8],
collateral: Option<&Collateral>,
current_time: i64,
) -> Result<QuoteVerificationResult<'a>, Quote3Error> {
let mut supp_data: mem::MaybeUninit<sgx_ql_qv_supplemental_t> = mem::MaybeUninit::zeroed();
let mut supp_data_desc = tee_supp_data_descriptor_t {
major_version: 0,
data_size: 0,
p_data: supp_data.as_mut_ptr() as *mut u8,
};
trace!("tee_get_supplemental_data_version_and_size");
let (_, supp_size) =
tee_get_supplemental_data_version_and_size(quote).map_err(|e| Quote3Error {
msg: "tee_get_supplemental_data_version_and_size",
inner: e,
})?;
trace!(
"tee_get_supplemental_data_version_and_size supp_size: {}",
supp_size
);
if supp_size == mem::size_of::<sgx_ql_qv_supplemental_t>() as u32 {
supp_data_desc.data_size = supp_size;
} else {
supp_data_desc.data_size = 0;
trace!(
"tee_get_supplemental_data_version_and_size supp_size: {}",
supp_size
);
trace!(
"mem::size_of::<sgx_ql_qv_supplemental_t>(): {}",
mem::size_of::<sgx_ql_qv_supplemental_t>()
);
warn!("Quote supplemental data size is different between DCAP QVL and QvE, please make sure you installed DCAP QVL and QvE from same release.")
}
let p_supplemental_data = match supp_data_desc.data_size {
0 => None,
_ => Some(&mut supp_data_desc),
};
let has_sup = p_supplemental_data.is_some();
trace!("tee_verify_quote");
let (collateral_expiration_status, result) =
tee_verify_quote(quote, collateral, current_time, None, p_supplemental_data).map_err(
|e| Quote3Error {
msg: "tee_verify_quote",
inner: e,
},
)?;
// check supplemental data if necessary
let (advisories, earliest_expiration_date, tcb_level_date_tag) = if has_sup {
unsafe {
let supp_data = supp_data.assume_init();
// convert to valid UTF-8 string
let ads = CStr::from_bytes_until_nul(cast_slice(&supp_data.sa_list[..]))
.ok()
.and_then(|s| CStr::to_str(s).ok())
.into_iter()
.flat_map(|s| s.split(',').map(str::trim).map(String::from))
.filter(|s| !s.is_empty())
.collect();
(
ads,
supp_data.earliest_expiration_date,
supp_data.tcb_level_date_tag,
)
}
} else {
(vec![], 0, 0)
};
let quote = Quote::try_from_bytes(quote).map_err(|_| Quote3Error {
msg: "Quote::try_from_bytes",
inner: quote3_error_t::SGX_QL_QUOTE_FORMAT_UNSUPPORTED,
})?;
let res = QuoteVerificationResult {
collateral_expired: collateral_expiration_status != 0,
earliest_expiration_date,
tcb_level_date_tag,
result,
quote,
advisories,
};
Ok(res)
}
/// Get the attestation report in a Gramine enclave
pub fn sgx_gramine_get_quote(report_data: &[u8; 64]) -> Result<Box<[u8]>, GetQuoteError> {
let mut file = OpenOptions::new()
.write(true)
.open("/dev/attestation/user_report_data")
.map_err(|e| GetQuoteError {
msg: "Failed to open `/dev/attestation/user_report_data`".into(),
source: e,
})?;
file.write(report_data).map_err(|e| GetQuoteError {
msg: "Failed to write `/dev/attestation/user_report_data`".into(),
source: e,
})?;
drop(file);
let mut file = OpenOptions::new()
.read(true)
.open("/dev/attestation/quote")
.map_err(|e| GetQuoteError {
msg: "Failed to open `/dev/attestation/quote`".into(),
source: e,
})?;
let mut quote = Vec::new();
file.read_to_end(&mut quote).map_err(|e| GetQuoteError {
msg: "Failed to read `/dev/attestation/quote`".into(),
source: e,
})?;
Ok(quote.into_boxed_slice())
}
/// Wrapper func for error
/// TODO: move to intel_tee_quote_verification_rs
pub fn tee_qv_get_collateral(quote: &[u8]) -> Result<Collateral, Quote3Error> {
intel_tee_quote_verification_rs::tee_qv_get_collateral(quote).map_err(Into::into)
}

383
src/sgx/sign.rs Normal file
View file

@ -0,0 +1,383 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
// Copyright (c) The Enarx Project Developers https://github.com/enarx/sgx
//! SGX signature structures
//!
//! Mostly copied from the [`sgx`](https://crates.io/crates/sgx) crate,
//! but with some modifications to make it easier to use in a standalone context.
//!
use bytemuck::{bytes_of, Pod, Zeroable};
use num_integer::Integer;
use num_traits::ToPrimitive;
use rand::thread_rng;
use rsa::{
pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, LineEnding},
traits::PublicKeyParts,
BigUint, Pkcs1v15Sign, RsaPrivateKey,
};
use sha2::Digest as _;
use sha2::Sha256;
pub use zeroize::Zeroizing;
/// Enclave CPU attributes
///
/// This type represents the CPU features turned on in an enclave.
#[repr(C, packed(4))]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct Attributes {
features: u64,
xfrm: u64,
}
/// The `Author` of an enclave
///
/// This structure encompasses the first block of fields from `SIGSTRUCT`
/// that is included in the signature. It is split out from `Signature`
/// in order to make it easy to hash the fields for the signature.
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Pod)]
pub struct Author {
header1: [u8; 16],
vendor: u32,
date: u32,
header2: [u8; 16],
swdefined: u32,
reserved: [u32; 21],
}
unsafe impl Zeroable for Author {}
impl Author {
const HEADER1: [u8; 16] = 0x06000000E10000000000010000000000u128.to_be_bytes();
const HEADER2: [u8; 16] = 0x01010000600000006000000001000000u128.to_be_bytes();
#[allow(clippy::unreadable_literal)]
/// Creates a new Author from a date and software defined value.
///
/// Note that the `date` input is defined in binary-coded decimal. For
/// example, the unix epoch is: `0x1970_01_01`.
pub const fn new(date: u32, swdefined: u32) -> Self {
Self {
header1: Self::HEADER1,
vendor: 0,
date,
header2: Self::HEADER2,
swdefined,
reserved: [0; 21],
}
}
/// get the date
#[inline]
pub fn date(&self) -> u32 {
self.date
}
/// get the swdefined
#[inline]
pub fn swdefined(&self) -> u32 {
self.swdefined
}
}
/// The enclave signature body
///
/// This structure encompasses the second block of fields from `SIGSTRUCT`
/// that is included in the signature. It is split out from `Signature`
/// in order to make it easy to hash the fields for the signature.
#[repr(C)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Pod, Zeroable)]
pub struct Body {
misc_select: u32,
misc_mask: u32,
cet_attr_select: u8,
cet_attr_mask: u8,
reserved0: [u8; 2],
ext_fid: [u8; 16],
attr_select: [u8; 16],
attr_mask: [u8; 16],
mrenclave: [u8; 32],
reserved1: [u8; 16],
ext_pid: [u8; 16],
pid: u16,
svn: u16,
}
impl Body {
/// Check if the debug flag can be set
#[inline]
pub fn can_set_debug(&self) -> bool {
/// Enables enclave debug mode
///
/// This gives permission to use EDBGRD and EDBGWR to read and write
/// enclave memory as plaintext, respectively. You most likely want
/// to validate that this option is disabled during attestion.
const DEBUG: u64 = 1 << 1;
let attr_select: &Attributes = bytemuck::try_from_bytes(&self.attr_select).unwrap();
let attr_mask: &Attributes = bytemuck::try_from_bytes(&self.attr_mask).unwrap();
(attr_select.features & DEBUG) == 0 && (attr_mask.features & DEBUG) == 0
}
}
/// A signature on an enclave
///
/// This structure encompasses the `SIGSTRUCT` structure from the SGX
/// documentation, renamed for ergonomics. The two portions of the
/// data that are included in the signature are further divided into
/// subordinate structures (`Author` and `Body`) for ease during
/// signature generation and validation.
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Pod, Zeroable)]
pub struct Signature {
author: Author,
modulus: [u8; 384],
exponent: u32,
signature: [u8; 384],
body: Body,
reserved: [u8; 12],
q1: [u8; 384],
q2: [u8; 384],
}
impl Signature {
/// Signs the supplied `author` and `body` with the specified `key`.
pub fn new<T: PrivateKey>(key: &T, author: Author, body: Body) -> Result<Self, T::Error> {
let a = bytes_of(&author);
let b = bytes_of(&body);
let sd = key.sign(a, b)?;
Ok(Self {
author,
modulus: sd.modulus,
exponent: sd.exponent,
signature: sd.signature,
body,
reserved: [0; 12],
q1: sd.q1,
q2: sd.q2,
})
}
/// Returns the author of the enclave.
pub fn author(&self) -> Author {
self.author
}
/// Returns the body of the enclave.
pub fn body(&self) -> Body {
self.body
}
}
/// A detached enclave signature
pub struct SigData {
/// The signature
pub signature: [u8; 384],
/// The public modulus
pub modulus: [u8; 384],
/// The public exponent
pub exponent: u32,
/// The first prime factor
pub q1: [u8; 384],
/// The second prime factor
pub q2: [u8; 384],
}
/// A fixed-size hash
pub trait Digest: Sized {
/// The output size of the hash
type Output: AsRef<[u8]>;
/// Construct a new hasher
fn new() -> Self;
/// Update the hasher with more data
fn update(&mut self, bytes: &[u8]);
/// Finalize the hasher and return the digest
fn finish(self) -> Self::Output;
/// Update the hasher with more data and return the hasher
#[inline]
fn chain(mut self, bytes: &[u8]) -> Self {
self.update(bytes);
self
}
}
/// A private key used for signing an enclave
pub trait PrivateKey: Sized {
/// The error type for this key
type Error: core::fmt::Debug;
/// Generate a new private key
fn generate(exponent: u8) -> Result<Self, Self::Error>;
/// Stringify the private key as a PEM-encoded string
fn to_pem(&self) -> Result<Zeroizing<String>, Self::Error>;
/// Load a private key from a PEM-encoded string
fn from_pem(pem: &str) -> Result<Self, Self::Error>;
/// Load a private key from a DER-encoded buffer
fn from_der(der: &[u8]) -> Result<Self, Self::Error>;
/// Sign the specified `author` and `body`
fn sign(&self, author: &[u8], body: &[u8]) -> Result<SigData, Self::Error>;
}
fn arr_from_big(value: &BigUint) -> [u8; 384] {
let mut arr = [0u8; 384];
let buf = value.to_bytes_le();
arr[..buf.len()].copy_from_slice(&buf);
arr
}
/// SHA2-256
pub struct S256Digest(Sha256);
impl Digest for S256Digest {
type Output = [u8; 32];
#[inline]
fn new() -> Self {
Self(Sha256::new())
}
#[inline]
fn update(&mut self, bytes: &[u8]) {
self.0.update(bytes)
}
#[inline]
fn finish(self) -> Self::Output {
*self.0.finalize().as_ref()
}
}
/// RSA w/ SHA2-256
pub struct RS256PrivateKey(RsaPrivateKey);
impl RS256PrivateKey {
/// Create a new RSA private key.
pub fn new(key: RsaPrivateKey) -> Self {
assert!(key.n().bits() <= 384 * 8);
Self(key)
}
}
impl PrivateKey for RS256PrivateKey {
type Error = rsa::errors::Error;
fn generate(exponent: u8) -> Result<Self, Self::Error> {
let mut rng = thread_rng();
let exp = BigUint::from(exponent);
let key = RsaPrivateKey::new_with_exp(&mut rng, 384 * 8, &exp)?;
Ok(Self::new(key))
}
fn to_pem(&self) -> Result<Zeroizing<String>, Self::Error> {
let pem = RsaPrivateKey::to_pkcs1_pem(&self.0, LineEnding::default())?;
Ok(pem)
}
fn from_pem(pem: &str) -> Result<Self, Self::Error> {
let key = RsaPrivateKey::from_pkcs1_pem(pem)?;
Ok(Self::new(key))
}
fn from_der(der: &[u8]) -> Result<Self, Self::Error> {
let key = RsaPrivateKey::from_pkcs1_der(der)?;
Ok(Self::new(key))
}
fn sign(&self, author: &[u8], body: &[u8]) -> Result<SigData, Self::Error> {
use sha2::digest::Update;
let hash = Sha256::new().chain(author).chain(body).finalize();
let padding = Pkcs1v15Sign::new::<Sha256>();
let sig = self.0.sign(padding, &hash)?;
// Calculate q1 and q2.
let s = BigUint::from_bytes_be(&sig);
let m = self.0.n();
let (q1, qr) = (&s * &s).div_rem(m);
let q2 = (&s * qr) / m;
Ok(SigData {
signature: arr_from_big(&s),
modulus: arr_from_big(m),
exponent: self.0.e().to_u32().unwrap(),
q1: arr_from_big(&q1),
q2: arr_from_big(&q2),
})
}
}
#[cfg(test)]
mod test {
use super::{Author, Body, Signature};
use testaso::testaso;
testaso! {
struct Author: 4, 128 => {
header1: 0,
vendor: 16,
date: 20,
header2: 24,
swdefined: 40,
reserved: 44
}
struct Body: 4, 128 => {
misc_select: 0,
misc_mask: 4,
cet_attr_select: 8,
cet_attr_mask: 9,
reserved0: 10,
ext_fid: 12,
attr_select: 28,
attr_mask: 44,
mrenclave: 60,
reserved1: 92,
ext_pid: 108,
pid: 124,
svn: 126
}
struct Signature: 4, 1808 => {
author: 0,
modulus: 128,
exponent: 512,
signature: 516,
body: 900,
reserved: 1028,
q1: 1040,
q2: 1424
}
}
#[test]
fn author_instantiation() {
let author = Author::new(0x2000_03_30, 0u32);
assert_eq!(author.header1, Author::HEADER1);
assert_eq!(author.vendor, 0u32);
assert_eq!(author.date, 0x2000_03_30);
assert_eq!(author.header2, Author::HEADER2);
assert_eq!(author.swdefined, 0u32);
assert_eq!(author.reserved, [0; 21]);
}
#[test]
fn test_signature() {
let test_sig = include_bytes!("../../tests/data/gramine-test.sig");
let sig: Signature = bytemuck::try_pod_read_unaligned(test_sig).unwrap();
let body = sig.body();
assert_eq!(
body.mrenclave.to_vec(),
hex::decode("f78170fe28e2e8671d83f5056975d25a27eb2c333dc520c2ccaf4de6b3f9c81b")
.unwrap()
);
assert!(!body.can_set_debug());
assert_eq!(body.misc_select, 0);
}
}

93
src/sgx/tcblevel.rs Normal file
View file

@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023 Matter Labs
//! Intel SGX Enclave TCB level wrapper
use enumset::EnumSetType;
use intel_tee_quote_verification_rs::sgx_ql_qv_result_t;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
pub use enumset::EnumSet;
/// TCB level
#[derive(EnumSetType, Debug)]
pub enum TcbLevel {
/// TCB is up to date
Ok,
/// TCB is up to date, but the configuration is not
ConfigNeeded,
/// TCB is up to date, but the configuration and software hardening is not
ConfigAndSwHardeningNeeded,
/// TCB is up to date, but the software hardening is not
SwHardeningNeeded,
/// TCB is out of date
OutOfDate,
/// TCB is out of date and the configuration is also out of date
OutOfDateConfigNeeded,
/// TCB level is invalid
Invalid,
}
impl From<sgx_ql_qv_result_t> for TcbLevel {
fn from(value: sgx_ql_qv_result_t) -> Self {
match value {
sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK => TcbLevel::Ok,
sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE => TcbLevel::OutOfDate,
sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE_CONFIG_NEEDED => {
TcbLevel::OutOfDateConfigNeeded
}
sgx_ql_qv_result_t::SGX_QL_QV_RESULT_SW_HARDENING_NEEDED => TcbLevel::SwHardeningNeeded,
sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_AND_SW_HARDENING_NEEDED => {
TcbLevel::ConfigAndSwHardeningNeeded
}
sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_NEEDED => TcbLevel::ConfigNeeded,
_ => TcbLevel::Invalid,
}
}
}
impl FromStr for TcbLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Ok" => Ok(TcbLevel::Ok),
"ConfigNeeded" => Ok(TcbLevel::ConfigNeeded),
"ConfigAndSwHardeningNeeded" => Ok(TcbLevel::ConfigAndSwHardeningNeeded),
"SwHardeningNeeded" => Ok(TcbLevel::SwHardeningNeeded),
"OutOfDate" => Ok(TcbLevel::OutOfDate),
"OutOfDateConfigNeeded" => Ok(TcbLevel::OutOfDateConfigNeeded),
"Invalid" => Ok(TcbLevel::Invalid),
_ => Err(()),
}
}
}
impl Display for TcbLevel {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TcbLevel::Ok => write!(f, "Ok"),
TcbLevel::ConfigNeeded => write!(f, "ConfigNeeded: Firmware needs to be updated"),
TcbLevel::ConfigAndSwHardeningNeeded => write!(f, "ConfigAndSwHardeningNeeded: Firmware configuration needs to be updated and software hardening is needed"),
TcbLevel::SwHardeningNeeded => write!(f, "SwHardeningNeeded: Software hardening is needed"),
TcbLevel::OutOfDate => write!(f, "OutOfDate: Firmware needs to be updated"),
TcbLevel::OutOfDateConfigNeeded => write!(f, "OutOfDateConfigNeeded: Firmware needs to be updated and configuration needs to be updated."),
TcbLevel::Invalid => write!(f, "Invalid TCB level"),
}
}
}
/// Parse a comma-separated list of TCB levels
pub fn parse_tcb_levels(
s: &str,
) -> Result<EnumSet<TcbLevel>, Box<dyn std::error::Error + Send + Sync + 'static>> {
let mut set = EnumSet::new();
for level_str in s.split(',') {
let level_str = level_str.trim();
let level = TcbLevel::from_str(level_str)
.map_err(|_| format!("Invalid TCB level: {}", level_str))?;
set.insert(level);
}
Ok(set)
}