feat: add TDX support

Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
This commit is contained in:
Harald Hoyer 2024-12-20 09:07:36 +01:00
parent f4fba51e3e
commit 4610475fae
Signed by: harald
GPG key ID: F519A1143B3FBE32
18 changed files with 2316 additions and 369 deletions

View file

@ -37,6 +37,7 @@ serde_json.workspace = true
serde_with.workspace = true
sha2.workspace = true
signature.workspace = true
tdx-attest-rs.workspace = true
thiserror.workspace = true
tracing.workspace = true
tracing-log.workspace = true
@ -48,5 +49,6 @@ zeroize.workspace = true
[dev-dependencies]
anyhow.workspace = true
base64.workspace = true
hex.workspace = true
testaso.workspace = true
tokio.workspace = true
tracing-test.workspace = true

View file

@ -8,12 +8,12 @@
pub mod vault;
pub use crate::quote::verify_quote_with_collateral;
pub use crate::quote::QuoteVerificationResult;
use crate::quote::Report;
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,
};
pub use crate::sgx::{parse_tcb_levels, sgx_ql_qv_result_t, EnumSet, TcbLevel};
use actix_web::http::header;
use anyhow::Result;
use awc::{Client, Connector};
@ -195,6 +195,10 @@ impl TeeConnection {
} = verify_quote_with_collateral(quote_bytes, collateral.as_ref(), current_time)
.unwrap();
let Report::SgxEnclave(report_body) = quote.report else {
return Err(Error::General("TDX quote and not SGX quote".into()));
};
if collateral_expired || result != sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK {
if collateral_expired {
error!(
@ -230,10 +234,10 @@ impl TeeConnection {
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 {
if report_body.mr_signer[..] != mrsigner_bytes {
return Err(Error::General(format!(
"mrsigner mismatch: got {}, expected {}",
hex::encode(quote.report_body.mrsigner),
hex::encode(report_body.mr_signer),
&mrsigner
)));
} else {
@ -245,10 +249,10 @@ impl TeeConnection {
let mrenclave_bytes = hex::decode(mrenclave).map_err(|e| {
Error::General(format!("Failed to decode mrenclave: {}", e))
})?;
if quote.report_body.mrenclave[..] != mrenclave_bytes {
if report_body.mr_enclave[..] != mrenclave_bytes {
return Err(Error::General(format!(
"mrenclave mismatch: got {}, expected {}",
hex::encode(quote.report_body.mrenclave),
hex::encode(report_body.mr_enclave),
&mrenclave
)));
} else {

View file

@ -7,26 +7,34 @@
#![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 crate::{
json::http::{AuthRequest, AuthResponse},
quote::error::QuoteContext,
server::{pki::make_self_signed_cert, AnyHowResponseError, HttpResponseError, Status},
};
pub use crate::{
quote::{verify_quote_with_collateral, QuoteVerificationResult},
sgx::{
parse_tcb_levels, sgx_gramine_get_quote, sgx_ql_qv_result_t, Collateral, EnumSet, TcbLevel,
},
};
use actix_http::error::PayloadError;
use actix_web::http::header;
use actix_web::ResponseError;
use actix_web::{http::header, ResponseError};
use anyhow::{anyhow, bail, Context, Result};
use awc::error::{SendRequestError, StatusCode};
use awc::{Client, ClientResponse, Connector};
use awc::{
error::{SendRequestError, StatusCode},
Client, ClientResponse, Connector,
};
use bytes::Bytes;
use futures_core::Stream;
use intel_tee_quote_verification_rs::tee_qv_get_collateral;
use rustls::ClientConfig;
use serde_json::{json, Value};
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use std::time;
use std::{
fmt::{Display, Formatter},
sync::Arc,
time,
};
use tracing::{debug, error, info, trace};
const VAULT_TOKEN_HEADER: &str = "X-Vault-Token";

View file

@ -8,8 +8,8 @@
pub mod client;
pub mod json;
pub mod server;
pub mod sgx;
pub mod log;
pub mod quote;
pub mod server;
pub mod sgx;
pub mod tdx;

View file

@ -0,0 +1,85 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024 Matter Labs
//! Quote Error type
use intel_tee_quote_verification_rs::quote3_error_t;
use std::io;
use tdx_attest_rs::tdx_attest_error_t;
use thiserror::Error;
/// Quote parsing error
#[derive(Error, Debug)]
#[allow(missing_docs)]
pub enum QuoteError {
#[error("I/O Error")]
IoError { context: String, source: io::Error },
#[error("parsing bytes")]
ConvertError(#[from] bytemuck::PodCastError),
#[error("unsupported quote version")]
QuoteVersion,
#[error("invalid tee type")]
InvalidTeeType,
#[error("unsupported body type")]
UnsupportedBodyType,
#[error("quote verification error {msg}: {inner:?}")]
Quote3Error { inner: quote3_error_t, msg: String },
#[error("tdx_att_get_quote error {msg}: {inner:?}")]
TdxAttGetQuote {
inner: tdx_attest_error_t,
msg: String,
},
#[error("invalid version")]
InvalidVersion,
#[error("report data too long")]
ReportDataSize,
#[error("can't get a quote: unknown TEE")]
UnknownTee,
}
impl From<tdx_attest_error_t> for QuoteError {
fn from(code: tdx_attest_error_t) -> Self {
Self::TdxAttGetQuote {
inner: code,
msg: "code".to_string(),
}
}
}
/// Usability trait for easy QuoteError annotation
pub trait QuoteContext {
/// The Ok Type
type Ok;
/// The Context
fn context<I: Into<String>>(self, msg: I) -> Result<Self::Ok, QuoteError>;
}
impl<T> QuoteContext for Result<T, std::io::Error> {
type Ok = T;
fn context<I: Into<String>>(self, msg: I) -> Result<T, QuoteError> {
self.map_err(|e| QuoteError::IoError {
context: msg.into(),
source: e,
})
}
}
impl<T> QuoteContext for Result<T, quote3_error_t> {
type Ok = T;
fn context<I: Into<String>>(self, msg: I) -> Result<T, QuoteError> {
self.map_err(|e| QuoteError::Quote3Error {
msg: msg.into(),
inner: e,
})
}
}
impl<T> QuoteContext for Result<T, tdx_attest_error_t> {
type Ok = T;
fn context<I: Into<String>>(self, msg: I) -> Result<T, QuoteError> {
self.map_err(|e| QuoteError::TdxAttGetQuote {
msg: msg.into(),
inner: e,
})
}
}

View file

@ -1,40 +1,733 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
// Parts of it are Copyright (c) 2024 Phala Network
// and copied from https://github.com/Phala-Network/dcap-qvl
//! Get a quote from a TEE
pub mod error;
use crate::sgx::sgx_gramine_get_quote;
use std::io;
use crate::{
quote::error::{QuoteContext as _, QuoteError},
sgx::sgx_gramine_get_quote,
tdx::tgx_get_quote,
};
use bytemuck::{cast_slice, AnyBitPattern};
use intel_tee_quote_verification_rs::{
sgx_ql_qv_result_t, sgx_ql_qv_supplemental_t, tee_get_supplemental_data_version_and_size,
tee_supp_data_descriptor_t, tee_verify_quote, Collateral,
};
use serde::{Deserialize, Serialize};
use std::{
ffi::CStr,
fmt::{Display, Formatter},
io::Read,
mem,
str::FromStr,
};
use tracing::{trace, warn};
pub use intel_tee_quote_verification_rs::tee_qv_get_collateral;
#[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,
pub const TEE_TYPE_SGX: u32 = 0x00000000;
#[allow(missing_docs)]
pub const TEE_TYPE_TDX: u32 = 0x00000081;
#[allow(missing_docs)]
pub const BODY_SGX_ENCLAVE_REPORT_TYPE: u16 = 1;
#[allow(missing_docs)]
pub const BODY_TD_REPORT10_TYPE: u16 = 2;
#[allow(missing_docs)]
pub const BODY_TD_REPORT15_TYPE: u16 = 3;
#[allow(missing_docs)]
pub const ENCLAVE_REPORT_BYTE_LEN: usize = 384;
#[allow(missing_docs)]
pub const ECDSA_SIGNATURE_BYTE_LEN: usize = 64;
#[allow(missing_docs)]
pub const ECDSA_PUBKEY_BYTE_LEN: usize = 64;
#[allow(missing_docs)]
pub const QE_REPORT_SIG_BYTE_LEN: usize = ECDSA_SIGNATURE_BYTE_LEN;
mod serde_bytes {
use serde::Deserialize;
pub(crate) trait FromBytes {
fn from_bytes(bytes: Vec<u8>) -> Option<Self>
where
Self: Sized;
}
impl FromBytes for Vec<u8> {
fn from_bytes(bytes: Vec<u8>) -> Option<Self> {
Some(bytes)
}
}
impl<const N: usize> FromBytes for [u8; N] {
fn from_bytes(bytes: Vec<u8>) -> Option<Self> {
bytes.try_into().ok()
}
}
pub(crate) fn serialize<S: serde::Serializer>(
data: impl AsRef<[u8]>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let hex_str = hex::encode(data);
serializer.serialize_str(&hex_str)
}
pub(crate) fn deserialize<'de, D: serde::Deserializer<'de>, T: FromBytes>(
deserializer: D,
) -> Result<T, D::Error> {
let hex_str = String::deserialize(deserializer)?;
let bytes = hex::decode(hex_str).map_err(serde::de::Error::custom)?;
T::from_bytes(bytes).ok_or_else(|| serde::de::Error::custom("invalid bytes"))
}
}
/// Trait that allows zero-copy read of value-references from slices in LE format.
pub trait Decode: Sized {
/// Attempt to deserialise the value from input.
fn decode<I: Read>(input: &mut I) -> Result<Self, error::QuoteError>;
}
impl<T: AnyBitPattern> Decode for T {
fn decode<I: Read>(input: &mut I) -> Result<Self, error::QuoteError> {
let mut bytes = vec![0u8; size_of::<T>()];
input.read(&mut bytes).context("parsing bytes")?;
bytemuck::try_pod_read_unaligned(&bytes).map_err(Into::into)
}
}
#[derive(Debug, Clone)]
#[allow(missing_docs)]
#[repr(C)]
pub struct Data<T> {
pub data: Vec<u8>,
_marker: core::marker::PhantomData<T>,
}
impl<T> Serialize for Data<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serde_bytes::serialize(&self.data, serializer)
}
}
impl<'de, T> Deserialize<'de> for Data<T> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = serde_bytes::deserialize(deserializer)?;
Ok(Data {
data,
_marker: core::marker::PhantomData,
})
}
}
impl<T: Decode + Into<u64>> Decode for Data<T> {
fn decode<I: Read>(input: &mut I) -> Result<Self, QuoteError> {
let len = T::decode(input)?;
let mut data = vec![0u8; len.into() as usize];
input.read(&mut data).context("reading bytes")?;
Ok(Data {
data,
_marker: core::marker::PhantomData,
})
}
}
#[allow(missing_docs)]
#[derive(AnyBitPattern, Debug, Serialize, Deserialize, Copy, Clone)]
#[repr(C, packed)]
pub struct Header {
pub version: u16,
pub attestation_key_type: u16,
pub tee_type: u32,
pub qe_svn: u16,
pub pce_svn: u16,
#[serde(with = "serde_bytes")]
pub qe_vendor_id: [u8; 16],
#[serde(with = "serde_bytes")]
pub user_data: [u8; 20],
}
#[derive(AnyBitPattern, Debug, Copy, Clone)]
#[allow(missing_docs)]
#[repr(C, packed)]
pub struct Body {
pub body_type: u16,
pub size: u32,
}
#[derive(Serialize, Deserialize, AnyBitPattern, Debug, Clone, Copy)]
#[allow(missing_docs)]
#[repr(C, packed)]
pub struct EnclaveReport {
#[serde(with = "serde_bytes")]
pub cpu_svn: [u8; 16],
pub misc_select: u32,
#[serde(with = "serde_bytes")]
pub reserved1: [u8; 28],
#[serde(with = "serde_bytes")]
pub attributes: [u8; 16],
#[serde(with = "serde_bytes")]
pub mr_enclave: [u8; 32],
#[serde(with = "serde_bytes")]
pub reserved2: [u8; 32],
#[serde(with = "serde_bytes")]
pub mr_signer: [u8; 32],
#[serde(with = "serde_bytes")]
pub reserved3: [u8; 96],
pub isv_prod_id: u16,
pub isv_svn: u16,
#[serde(with = "serde_bytes")]
pub reserved4: [u8; 60],
#[serde(with = "serde_bytes")]
pub report_data: [u8; 64],
}
#[derive(AnyBitPattern, Debug, Copy, Clone, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C, packed)]
pub struct TDReport10 {
#[serde(with = "serde_bytes")]
pub tee_tcb_svn: [u8; 16],
#[serde(with = "serde_bytes")]
pub mr_seam: [u8; 48],
#[serde(with = "serde_bytes")]
pub mr_signer_seam: [u8; 48],
#[serde(with = "serde_bytes")]
pub seam_attributes: [u8; 8],
#[serde(with = "serde_bytes")]
pub td_attributes: [u8; 8],
#[serde(with = "serde_bytes")]
pub xfam: [u8; 8],
#[serde(with = "serde_bytes")]
pub mr_td: [u8; 48],
#[serde(with = "serde_bytes")]
pub mr_config_id: [u8; 48],
#[serde(with = "serde_bytes")]
pub mr_owner: [u8; 48],
#[serde(with = "serde_bytes")]
pub mr_owner_config: [u8; 48],
#[serde(with = "serde_bytes")]
pub rt_mr0: [u8; 48],
#[serde(with = "serde_bytes")]
pub rt_mr1: [u8; 48],
#[serde(with = "serde_bytes")]
pub rt_mr2: [u8; 48],
#[serde(with = "serde_bytes")]
pub rt_mr3: [u8; 48],
#[serde(with = "serde_bytes")]
pub report_data: [u8; 64],
}
#[derive(AnyBitPattern, Debug, Copy, Clone, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C, packed)]
pub struct TDReport15 {
pub base: TDReport10,
#[serde(with = "serde_bytes")]
pub tee_tcb_svn2: [u8; 16],
#[serde(with = "serde_bytes")]
pub mr_service_td: [u8; 48],
}
#[allow(missing_docs)]
#[derive(Serialize, Deserialize, Clone)]
#[repr(C)]
pub struct CertificationData {
pub cert_type: u16,
pub body: Data<u32>,
}
impl Decode for CertificationData {
fn decode<I: Read>(input: &mut I) -> Result<Self, QuoteError> {
Ok(Self {
cert_type: Decode::decode(input)?,
body: Decode::decode(input)?,
})
}
}
impl core::fmt::Debug for CertificationData {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let body_str = String::from_utf8_lossy(&self.body.data);
f.debug_struct("CertificationData")
.field("cert_type", &self.cert_type)
.field("body", &body_str)
.finish()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C)]
pub struct QEReportCertificationData {
#[serde(with = "serde_bytes")]
pub qe_report: [u8; ENCLAVE_REPORT_BYTE_LEN],
#[serde(with = "serde_bytes")]
pub qe_report_signature: [u8; QE_REPORT_SIG_BYTE_LEN],
pub qe_auth_data: Data<u16>,
pub certification_data: CertificationData,
}
impl Decode for QEReportCertificationData {
fn decode<I: Read>(input: &mut I) -> Result<Self, QuoteError> {
Ok(Self {
qe_report: Decode::decode(input)?,
qe_report_signature: Decode::decode(input)?,
qe_auth_data: Decode::decode(input)?,
certification_data: Decode::decode(input)?,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C)]
pub struct AuthDataV3 {
#[serde(with = "serde_bytes")]
pub ecdsa_signature: [u8; ECDSA_SIGNATURE_BYTE_LEN],
#[serde(with = "serde_bytes")]
pub ecdsa_attestation_key: [u8; ECDSA_PUBKEY_BYTE_LEN],
#[serde(with = "serde_bytes")]
pub qe_report: [u8; ENCLAVE_REPORT_BYTE_LEN],
#[serde(with = "serde_bytes")]
pub qe_report_signature: [u8; QE_REPORT_SIG_BYTE_LEN],
pub qe_auth_data: Data<u16>,
pub certification_data: CertificationData,
}
impl Decode for AuthDataV3 {
fn decode<I: Read>(input: &mut I) -> Result<Self, QuoteError> {
Ok(Self {
ecdsa_signature: Decode::decode(input)?,
ecdsa_attestation_key: Decode::decode(input)?,
qe_report: Decode::decode(input)?,
qe_report_signature: Decode::decode(input)?,
qe_auth_data: Decode::decode(input)?,
certification_data: Decode::decode(input)?,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C)]
pub struct AuthDataV4 {
#[serde(with = "serde_bytes")]
pub ecdsa_signature: [u8; ECDSA_SIGNATURE_BYTE_LEN],
#[serde(with = "serde_bytes")]
pub ecdsa_attestation_key: [u8; ECDSA_PUBKEY_BYTE_LEN],
pub certification_data: CertificationData,
pub qe_report_data: QEReportCertificationData,
}
impl AuthDataV4 {
#[allow(missing_docs)]
pub fn into_v3(self) -> AuthDataV3 {
AuthDataV3 {
ecdsa_signature: self.ecdsa_signature,
ecdsa_attestation_key: self.ecdsa_attestation_key,
qe_report: self.qe_report_data.qe_report,
qe_report_signature: self.qe_report_data.qe_report_signature,
qe_auth_data: self.qe_report_data.qe_auth_data,
certification_data: self.qe_report_data.certification_data,
}
}
}
impl Decode for AuthDataV4 {
fn decode<I: Read>(input: &mut I) -> Result<Self, error::QuoteError> {
let ecdsa_signature = Decode::decode(input)?;
let ecdsa_attestation_key = Decode::decode(input)?;
let certification_data: CertificationData = Decode::decode(input)?;
let qe_report_data =
QEReportCertificationData::decode(&mut &certification_data.body.data[..])?;
Ok(AuthDataV4 {
ecdsa_signature,
ecdsa_attestation_key,
certification_data,
qe_report_data,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C)]
pub enum AuthData {
V3(AuthDataV3),
V4(AuthDataV4),
}
impl AuthData {
#[allow(missing_docs)]
pub fn into_v3(self) -> AuthDataV3 {
match self {
AuthData::V3(data) => data,
AuthData::V4(data) => data.into_v3(),
}
}
}
fn decode_auth_data(ver: u16, input: &mut &[u8]) -> Result<AuthData, error::QuoteError> {
match ver {
3 => {
let auth_data = AuthDataV3::decode(input)?;
Ok(AuthData::V3(auth_data))
}
4 => {
let auth_data = AuthDataV4::decode(input)?;
Ok(AuthData::V4(auth_data))
}
_ => Err(error::QuoteError::QuoteVersion),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C)]
#[non_exhaustive]
pub enum Report {
SgxEnclave(EnclaveReport),
TD10(TDReport10),
TD15(TDReport15),
}
impl Report {
#[allow(missing_docs)]
pub fn is_sgx(&self) -> bool {
matches!(self, Report::SgxEnclave(_))
}
#[allow(missing_docs)]
pub fn as_td10(&self) -> Option<&TDReport10> {
match self {
Report::TD10(report) => Some(report),
Report::TD15(report) => Some(&report.base),
_ => None,
}
}
#[allow(missing_docs)]
pub fn as_td15(&self) -> Option<&TDReport15> {
match self {
Report::TD15(report) => Some(report),
_ => None,
}
}
#[allow(missing_docs)]
pub fn as_sgx(&self) -> Option<&EnclaveReport> {
match self {
Report::SgxEnclave(report) => Some(report),
_ => None,
}
}
}
impl Display for Report {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
fn space_or_newline(f: &mut Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
writeln!(f)
} else {
write!(f, " ")
}
}
match self {
Report::SgxEnclave(report_body) => {
write!(f, "mrsigner: {}", hex::encode(report_body.mr_signer))?;
space_or_newline(f)?;
write!(f, "mrenclave: {}", hex::encode(report_body.mr_enclave))?;
space_or_newline(f)?;
write!(
f,
"reportdata: {}",
hex::encode(report_body.report_data.as_slice())
)?;
}
Report::TD10(report_body) => {
write!(f, "mrtd: {}", hex::encode(report_body.mr_td))?;
space_or_newline(f)?;
write!(f, "rtmr0: {}", hex::encode(report_body.rt_mr0))?;
space_or_newline(f)?;
write!(f, "rtmr1: {}", hex::encode(report_body.rt_mr1))?;
space_or_newline(f)?;
write!(f, "rtmr2: {}", hex::encode(report_body.rt_mr2))?;
space_or_newline(f)?;
write!(f, "rtmr3: {}", hex::encode(report_body.rt_mr3))?;
space_or_newline(f)?;
write!(
f,
"reportdata: {}",
hex::encode(report_body.report_data.as_slice())
)?;
}
Report::TD15(report_body) => {
let report_body = &report_body.base;
write!(f, "mrtd: {}", hex::encode(report_body.mr_td))?;
space_or_newline(f)?;
write!(f, "rtmr0: {}", hex::encode(report_body.rt_mr0))?;
space_or_newline(f)?;
write!(f, "rtmr1: {}", hex::encode(report_body.rt_mr1))?;
space_or_newline(f)?;
write!(f, "rtmr2: {}", hex::encode(report_body.rt_mr2))?;
space_or_newline(f)?;
write!(f, "rtmr3: {}", hex::encode(report_body.rt_mr3))?;
space_or_newline(f)?;
write!(
f,
"reportdata: {}",
hex::encode(report_body.report_data.as_slice())
)?;
}
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(missing_docs)]
#[repr(C)]
pub struct Quote {
pub header: Header,
pub report: Report,
pub auth_data: AuthData,
}
impl Decode for Quote {
fn decode<I: Read>(input: &mut I) -> Result<Self, error::QuoteError> {
let header = Header::decode(input)?;
trace!(?header);
let report;
match header.version {
3 => {
if header.tee_type != TEE_TYPE_SGX {
return Err(error::QuoteError::InvalidTeeType);
}
report = Report::SgxEnclave(EnclaveReport::decode(input)?);
}
4 => match header.tee_type {
TEE_TYPE_SGX => {
report = Report::SgxEnclave(EnclaveReport::decode(input)?);
}
TEE_TYPE_TDX => {
report = Report::TD10(TDReport10::decode(input)?);
}
_ => return Err(error::QuoteError::InvalidTeeType),
},
5 => {
let body = Body::decode(input)?;
match body.body_type {
BODY_SGX_ENCLAVE_REPORT_TYPE => {
report = Report::SgxEnclave(EnclaveReport::decode(input)?);
}
BODY_TD_REPORT10_TYPE => {
report = Report::TD10(TDReport10::decode(input)?);
}
BODY_TD_REPORT15_TYPE => {
report = Report::TD15(TDReport15::decode(input)?);
}
_ => return Err(error::QuoteError::UnsupportedBodyType),
}
}
_ => return Err(error::QuoteError::QuoteVersion),
}
let data = Data::<u32>::decode(input)?;
let auth_data = decode_auth_data(header.version, &mut &data.data[..])?;
Ok(Quote {
header,
report,
auth_data,
})
}
}
impl Quote {
/// Parse a TEE quote from a byte slice.
pub fn parse(quote: &[u8]) -> Result<Self, QuoteError> {
let mut input = quote;
let quote = Quote::decode(&mut input)?;
Ok(quote)
}
/// Get the report data
pub fn get_report_data(&self) -> &[u8] {
match &self.report {
Report::SgxEnclave(r) => r.report_data.as_slice(),
Report::TD10(r) => r.report_data.as_slice(),
Report::TD15(r) => r.base.report_data.as_slice(),
}
}
}
/// TEE type
#[non_exhaustive]
pub enum TEEType {
/// Intel SGX
SGX,
/// Intel TDX
TDX,
/// AMD SEV-SNP
SNP,
}
impl Display for TEEType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
TEEType::SGX => "sgx",
TEEType::TDX => "tdx",
TEEType::SNP => "snp",
};
write!(f, "{}", str)
}
}
impl FromStr for TEEType {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"sgx" => Ok(TEEType::SGX),
"tdx" => Ok(TEEType::TDX),
"snp" => Ok(TEEType::SNP),
_ => Err("Invalid TEE type".to_string()),
}
}
}
/// Get the attestation quote from a TEE
pub fn get_quote(report_data: &[u8]) -> Result<Box<[u8]>, GetQuoteError> {
pub fn get_quote(report_data: &[u8]) -> Result<(TEEType, Box<[u8]>), QuoteError> {
// 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"),
});
return Err(QuoteError::ReportDataSize);
}
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)
Ok((TEEType::SGX, sgx_gramine_get_quote(&report_data_fixed)?))
} else if std::fs::metadata("/dev/tdx_guest").is_ok() {
if report_data.len() > 64 {
return Err(QuoteError::ReportDataSize);
}
let mut report_data_fixed = [0u8; 64];
report_data_fixed[..report_data.len()].copy_from_slice(report_data);
Ok((TEEType::TDX, tgx_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"),
})
Err(QuoteError::UnknownTee)
}
}
/// The result of the quote verification
pub struct QuoteVerificationResult {
/// 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: Quote,
}
/// Verifies a quote with optional collateral material
pub fn verify_quote_with_collateral(
quote: &[u8],
collateral: Option<&Collateral>,
current_time: i64,
) -> Result<QuoteVerificationResult, QuoteError> {
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| QuoteError::Quote3Error {
msg: "tee_get_supplemental_data_version_and_size".into(),
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)
.context("tee_verify_quote")?;
trace!("tee_verify_quote end");
// 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)
};
trace!("Quote::parse");
let quote = Quote::parse(quote)?;
let res = QuoteVerificationResult {
collateral_expired: collateral_expiration_status != 0,
earliest_expiration_date,
tcb_level_date_tag,
result,
quote,
advisories,
};
Ok(res)
}

View file

@ -3,17 +3,22 @@
//! 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 crate::{
client::AttestationArgs,
json::http::AttestationResponse,
quote::{
error::QuoteContext, get_quote, verify_quote_with_collateral, QuoteVerificationResult,
},
sgx::{parse_tcb_levels, Collateral, EnumSet, TcbLevel},
};
use anyhow::{bail, Context, Result};
use clap::Args;
use intel_tee_quote_verification_rs::tee_qv_get_collateral;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, RwLock};
use std::time::{Duration, UNIX_EPOCH};
use std::{
sync::{Arc, RwLock},
time::{Duration, UNIX_EPOCH},
};
use tracing::{debug, error, info, trace, warn};
struct Attestation {
@ -53,7 +58,7 @@ pub fn get_quote_and_collateral(
}
}
let myquote = sgx_gramine_get_quote(report_data).context("Failed to get own quote")?;
let (_tee_type, myquote) = 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 {
@ -89,8 +94,8 @@ pub fn get_quote_and_collateral(
"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));
info!("{:#}", quote.report);
let quote: Arc<[u8]> = Arc::from(myquote);
let collateral = Arc::from(collateral);

View file

@ -3,39 +3,38 @@
//! 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 crate::quote::{error::QuoteContext, get_quote};
pub use crate::sgx::{parse_tcb_levels, sgx_ql_qv_result_t, EnumSet, TcbLevel};
use anyhow::{Context, Result};
use const_oid::db::rfc5280::{ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH};
use const_oid::AssociatedOid;
use const_oid::{
db::rfc5280::{ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH},
AssociatedOid,
};
use getrandom::getrandom;
use p256::ecdsa::DerSignature;
use p256::pkcs8::EncodePrivateKey;
use intel_tee_quote_verification_rs::tee_qv_get_collateral;
use p256::{ecdsa::DerSignature, pkcs8::EncodePrivateKey};
use pkcs8::der;
use rustls::pki_types::PrivatePkcs8KeyDer;
use sha2::{Digest, Sha256};
use signature::Signer;
use std::str::FromStr;
use std::time::Duration;
use std::{str::FromStr, time::Duration};
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::{
builder::{Builder, CertificateBuilder, Profile},
der::{asn1::OctetString, pem::LineEnding, Encode as _, EncodePem as _, Length},
ext::{
pkix::{name::GeneralNames, ExtendedKeyUsage, SubjectAltName},
AsExtension, Extension,
},
name::{Name, RdnSequence},
serial_number::SerialNumber,
spki::{
DynSignatureAlgorithmIdentifier, EncodePublicKey, ObjectIdentifier,
SignatureBitStringEncoding, SubjectPublicKeyInfoOwned,
},
time::Validity,
Certificate,
};
use x509_cert::time::Validity;
use x509_cert::Certificate;
use zeroize::Zeroizing;
/// The OID for the `gramine-ra-tls` quote extension
@ -148,7 +147,7 @@ pub fn make_self_signed_cert(
let hash = Sha256::digest(verifying_key_der.as_bytes());
key_hash[..32].copy_from_slice(&hash);
let quote = get_quote(&key_hash)?;
let (_tee_type, quote) = get_quote(&key_hash)?;
debug!("quote.len: {:?}", quote.len());
// Create a relative distinguished name.
let rdns = RdnSequence::from_str(dn)?;
@ -185,6 +184,7 @@ pub fn make_self_signed_cert(
.context("failed to add SubjectAltName")?;
}
// FIXME: OID for tee_type
builder
.add_extension(&RaTlsQuoteExtension {
quote: quote.to_vec(),
@ -234,7 +234,7 @@ where
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")?;
let (_tee_type, quote) = get_quote(&key_hash).context("Failed to get own quote")?;
// Create a relative distinguished name.
let subject = Name::from_str(dn)?;
@ -269,6 +269,7 @@ where
.context("failed to add SubjectAltName")?;
}
// FIXME: oid according to tee_type
builder
.add_extension(&RaTlsQuoteExtension {
quote: quote.to_vec(),

View file

@ -1,50 +0,0 @@
// 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,
}

View file

@ -5,24 +5,18 @@
//! 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};
use crate::quote::error::QuoteContext;
pub use crate::quote::error::QuoteError;
use bytemuck::{try_from_bytes, AnyBitPattern, PodCastError};
pub use intel_tee_quote_verification_rs::{sgx_ql_qv_result_t, Collateral};
use std::{
fs::OpenOptions,
io::{Read, Write},
mem,
};
pub use tcblevel::{parse_tcb_levels, EnumSet, TcbLevel};
/// Structure of a quote
@ -43,13 +37,13 @@ pub struct Quote {
impl Quote {
/// Creates a quote from a byte slice
pub fn try_from_bytes(bytes: &[u8]) -> Result<&Self, QuoteFromError> {
pub fn try_from_bytes(bytes: &[u8]) -> Result<&Self, QuoteError> {
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);
return Err(QuoteError::InvalidVersion);
}
Ok(this)
}
@ -98,152 +92,26 @@ pub struct ReportBody {
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 optional 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> {
pub fn sgx_gramine_get_quote(report_data: &[u8; 64]) -> Result<Box<[u8]>, QuoteError> {
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,
})?;
.context("opening `/dev/attestation/user_report_data`")?;
file.write(report_data).map_err(|e| GetQuoteError {
msg: "Failed to write `/dev/attestation/user_report_data`".into(),
source: e,
})?;
file.write(report_data)
.context("writing `/dev/attestation/user_report_data`")?;
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,
})?;
.context("opening `/dev/attestation/quote`")?;
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,
})?;
file.read_to_end(&mut quote)
.context("reading `/dev/attestation/quote`")?;
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)
}

View file

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

File diff suppressed because it is too large Load diff