Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

verifier: Fetch VCEK cert from KDS instead of bailing #555

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 29 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions deps/verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ strum.workspace = true
veraison-apiclient = { git = "https://github.com/chendave/rust-apiclient", branch = "token", optional = true }
ear = { git = "https://github.com/veraison/rust-ear", rev = "43f7f480d09ea2ebc03137af8fbcd70fe3df3468", optional = true }
x509-parser = { version = "0.14.0", optional = true }
reqwest.workspace = true
AdithyaKrishnan marked this conversation as resolved.
Show resolved Hide resolved

[build-dependencies]
shadow-rs.workspace = true
Expand Down
64 changes: 62 additions & 2 deletions deps/verifier/src/snp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use openssl::{
sha::sha384,
x509::{self, X509},
};
use reqwest::{get, Response as ReqwestResponse, StatusCode};
use serde_json::json;
use sev::firmware::guest::AttestationReport;
use sev::firmware::host::{CertTableEntry, CertType};
Expand All @@ -32,11 +33,18 @@ const SNP_SPL_OID: Oid<'static> = oid!(1.3.6 .1 .4 .1 .3704 .1 .3 .3);
const TEE_SPL_OID: Oid<'static> = oid!(1.3.6 .1 .4 .1 .3704 .1 .3 .2);
const LOADER_SPL_OID: Oid<'static> = oid!(1.3.6 .1 .4 .1 .3704 .1 .3 .1);

// KDS URL parameters
const KDS_CERT_SITE: &str = "https://kdsintf.amd.com";
const KDS_VCEK: &str = "/vcek/v1";

#[derive(Debug)]
pub struct Snp {
vendor_certs: VendorCertificates,
}

/// Loads the Milan certificate chain and returns a static reference to it.
/// The chain is loaded lazily using `OnceLock` to ensure it's only initialized once.
/// Certificates are loaded from a PEM file and must contain exactly three certificates (ASK, ARK, ASVK).
pub(crate) fn load_milan_cert_chain() -> &'static Result<VendorCertificates> {
static MILAN_CERT_CHAIN: OnceLock<Result<VendorCertificates>> = OnceLock::new();
MILAN_CERT_CHAIN.get_or_init(|| {
Expand All @@ -55,6 +63,8 @@ pub(crate) fn load_milan_cert_chain() -> &'static Result<VendorCertificates> {
}

impl Snp {
/// Creates a new `Snp` instance by loading the Milan certificate chain.
/// Returns an error if the certificate chain can not be loaded.
pub fn new() -> Result<Self> {
let Result::Ok(vendor_certs) = load_milan_cert_chain() else {
bail!("Failed to load Milan cert chain");
Expand All @@ -73,6 +83,9 @@ pub(crate) struct VendorCertificates {

#[async_trait]
impl Verifier for Snp {
/// Evaluates the provided evidence against the expected report data and initialize data hash.
/// Validates the report signature, version, VMPL, and other fields.
/// Returns parsed claims if the verification is successful.
async fn evaluate(
&self,
evidence: &[u8],
Expand All @@ -84,8 +97,9 @@ impl Verifier for Snp {
cert_chain,
} = serde_json::from_slice(evidence).context("Deserialize Quote failed.")?;

let Some(cert_chain) = cert_chain else {
bail!("Cert chain is unset");
let cert_chain = match cert_chain {
Some(chain) if !chain.is_empty() => chain,
_ => fetch_vcek_from_kds(report).await?,
};

verify_report_signature(&report, &cert_chain, &self.vendor_certs)?;
Expand Down Expand Up @@ -128,6 +142,8 @@ impl Verifier for Snp {
}
}

/// Retrieves the octet string value for a given OID from a certificate's extensions.
/// Supports both raw and DER-encoded formats.
fn get_oid_octets<const N: usize>(
vcek: &x509_parser::certificate::TbsCertificate,
oid: Oid,
Expand All @@ -152,6 +168,7 @@ fn get_oid_octets<const N: usize>(
.context("Unexpected data size")
}

/// Retrieves an integer value for a given OID from a certificate's extensions.
fn get_oid_int(cert: &x509_parser::certificate::TbsCertificate, oid: Oid) -> Result<u8> {
let val = cert
.get_extension_unique(&oid)?
Expand All @@ -162,6 +179,7 @@ fn get_oid_int(cert: &x509_parser::certificate::TbsCertificate, oid: Oid) -> Res
val_int.as_u8().context("Unexpected data size")
}

/// Verifies the signature of the attestation report using the provided certificate chain and vendor certificates.
pub(crate) fn verify_report_signature(
report: &AttestationReport,
cert_chain: &[CertTableEntry],
Expand Down Expand Up @@ -223,12 +241,15 @@ pub(crate) fn verify_report_signature(
Ok(())
}

/// Verifies the signature of a certificate against its issuer's public key.
fn verify_signature(cert: &X509, issuer: &X509, name: &str) -> Result<()> {
cert.verify(&(issuer.public_key()? as PKey<Public>))?
.then_some(())
.ok_or_else(|| anyhow!("Invalid {name} signature"))
}

/// Verifies the certificate chain based on the provided VCEK or VLEK.
/// Ensures the chain is valid by verifying signatures and relationships between certificates.
fn verify_cert_chain(
cert_chain: &[CertTableEntry],
ask: &X509,
Expand Down Expand Up @@ -267,6 +288,8 @@ fn verify_cert_chain(
Ok(decoded_key)
}

/// Parses the attestation report and extracts the TEE evidence claims.
/// Returns a JSON-formatted map of parsed claims.
pub(crate) fn parse_tee_evidence(report: &AttestationReport) -> TeeEvidenceParsedClaim {
let claims_map = json!({
// policy fields
Expand Down Expand Up @@ -294,6 +317,7 @@ pub(crate) fn parse_tee_evidence(report: &AttestationReport) -> TeeEvidenceParse
claims_map as TeeEvidenceParsedClaim
}

/// Extracts the common name (CN) from the subject name of a certificate.
fn get_common_name(cert: &x509::X509) -> Result<String> {
let mut entries = cert.subject_name().entries_by_nid(Nid::COMMONNAME);
let Some(e) = entries.next() else {
Expand All @@ -307,6 +331,42 @@ fn get_common_name(cert: &x509::X509) -> Result<String> {
Ok(e.data().as_utf8()?.to_string())
}

/// Asynchronously fetches the VCEK from the Key Distribution Service (KDS) using the provided attestation report.
/// Returns the VCEK in DER format as part of a certificate table entry.
async fn fetch_vcek_from_kds(att_report: AttestationReport) -> Result<Vec<CertTableEntry>> {
// Use attestation report to get data for URL
let hw_id: String = hex::encode(att_report.chip_id);

let vcek_url: String = format!(
"{KDS_CERT_SITE}{KDS_VCEK}/Milan/\
{hw_id}?blSPL={:02}&teeSPL={:02}&snpSPL={:02}&ucodeSPL={:02}",
att_report.reported_tcb.bootloader,
att_report.reported_tcb.tee,
att_report.reported_tcb.snp,
att_report.reported_tcb.microcode
);
// VCEK in DER format
let vcek_rsp: ReqwestResponse = get(vcek_url)
.await
.context("Unable to send request for VCEK")?;

match vcek_rsp.status() {
StatusCode::OK => {
let vcek_rsp_bytes: Vec<u8> = vcek_rsp
.bytes()
.await
.context("Unable to parse VCEK")?
.to_vec();
let key = CertTableEntry {
cert_type: CertType::VCEK,
data: vcek_rsp_bytes,
};
Ok(vec![key])
}
status => Err(anyhow!("Unable to fetch VCEK from URL: {status:?}")),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading