diff --git a/README.md b/README.md index 0ea9f0c..7cb9bac 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,9 @@ certificate(s) are as follows: ark.{der, pem} ask.{der, pem} vcek.{der, pem} + crl.{der, pem} (optional) ``` -The `fetch-vcek` subcommand can be used to fetch these certificates from AMD's Key Distribution Service (KDS). +The `fetch-vcek` and `fetch-crl` subcommands can be used to fetch these certificates from AMD's Key Distribution Service (KDS). - Intel TDX: Provisioning Certification Key (PCK), PCK Issuer Chain\*. ``` {certs-dir} \ @@ -82,6 +83,15 @@ cvmtool fetch-vcek --certs-dir certs/ cvmtool fetch-pck --certs-dir certs/ ``` +### Fetch Revocation List (SEV-SNP only, optional) + +To fetch the certificate revocation list for verification, use the `fetch-crl` command from within the CVM: + +```bash +# Fetch CRL from AMD KDS +cvmtool fetch-crl --certs-dir certs/ +``` + ## Azure CVM Support When built with the `azure` feature, cvmtool can retrieve attestation reports from Azure Confidential VMs via the vTPM (Virtual TPM). This works on Azure CVMs using either SNP or TDX isolation. diff --git a/src/main.rs b/src/main.rs index 6e5d431..8661389 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,6 +114,12 @@ enum Commands { #[arg(short, long, default_value = ".")] certs_dir: PathBuf, }, + /// Fetch VCEK Certificate Revocation List from AMD KDS (for SEV-SNP) + FetchCrl { + /// Output directory for the CRL + #[arg(short, long, default_value = ".")] + certs_dir: PathBuf, + }, } /// Options for attestation verification beyond cryptographic checks @@ -291,6 +297,32 @@ fn main() -> Result<()> { println!("Saved certificates to {:?}", certs_dir); } } + Commands::FetchCrl { certs_dir } => { + if cli.verbose { + println!("Generating SEV attestation report..."); + } + + let report = report(Some("sev"), [0; 64])?; + let report = sev::parse_report(&report)?; + + let processor = vcek::get_processor_model(&report)?; + if cli.verbose { + println!("Detected processor: {processor}"); + } + + if cli.verbose { + println!("Fetching CRL from AMD KDS..."); + } + let crl_der = vcek::fetch_crl(&processor)?; + if cli.verbose { + println!("CRL retrieved ({} bytes)", crl_der.len()); + } + + vcek::save_crl(&crl_der, &certs_dir, cli.verbose)?; + if !cli.quiet { + println!("Saved CRL to {:?}", certs_dir); + } + } } Ok(()) diff --git a/src/sev.rs b/src/sev.rs index d52294c..6522d01 100644 --- a/src/sev.rs +++ b/src/sev.rs @@ -3,7 +3,11 @@ use crate::VerifyOptions; use anyhow::{Context, Result}; use asn1_rs::{Oid, oid}; -use openssl::{ecdsa::EcdsaSig, sha::Sha384}; +use openssl::{ + ecdsa::EcdsaSig, + sha::Sha384, + x509::{X509, X509Crl}, +}; use sev::certs::snp::{Certificate, Verifiable}; use sev::firmware::guest::AttestationReport; use sev::parser::ByteParser; @@ -71,6 +75,8 @@ pub fn verify_report( println!("VCEK was signed by ASK"); } + verify_revocation_list(certs_dir, &ark, &ask, &vcek, opts)?; + let vcek_pubkey = vcek .public_key() .context("Failed to get public key from VCEK")? @@ -119,6 +125,75 @@ fn find_cert_in_dir(dir: &Path, name: &str) -> Result { )) } +fn load_crl(path: &Path) -> Result { + let data = fs::read(path).context(format!("Failed to read certificate {}", path.display()))?; + let is_pem = path.extension().map(|ext| ext == "pem").unwrap_or(false); + if is_pem { + X509Crl::from_pem(&data).context("Failed to parse PEM certificate") + } else { + X509Crl::from_der(&data).context("Failed to parse DER certificate") + } +} + +/// Checks that the ASK and VCEK have not been revoked by consulting the CRL +fn verify_revocation_list( + certs_dir: &Path, + ark: &Certificate, + ask: &Certificate, + vcek: &Certificate, + opts: &VerifyOptions, +) -> Result<()> { + let Ok(path) = find_cert_in_dir(certs_dir, "crl") else { + eprintln!( + "Warning: no CRL found in {}; ASK revocation status not checked", + certs_dir.display() + ); + return Ok(()); + }; + + let crl = load_crl(&path)?; + + let ark_pubkey = ark.public_key().context("Failed to get ARK public key")?; + if !crl + .verify(&ark_pubkey) + .context("Failed to verify CRL signature")? + { + return Err(anyhow::anyhow!("CRL is not signed by ARK")); + } + + let ask_serial = X509::from(ask) + .serial_number() + .to_bn() + .context("Failed to read ASK serial number")?; + let vcek_serial = X509::from(vcek) + .serial_number() + .to_bn() + .context("Failed to read VCEK serial number")?; + + // AMD uses TCB versioning rather than CRL entries to supersede old VCEKs, so + // VCEK revocation is not expected in practice, but check anyway for completeness. + if let Some(revoked) = crl.get_revoked() { + for entry in revoked { + let entry_serial = entry + .serial_number() + .to_bn() + .context("Failed to read revoked entry serial number")?; + if entry_serial == ask_serial { + return Err(anyhow::anyhow!("ASK certificate has been revoked")); + } + if entry_serial == vcek_serial { + return Err(anyhow::anyhow!("VCEK certificate has been revoked")); + } + } + } + + if !opts.quiet { + println!("ASK and VCEK are not revoked"); + } + + Ok(()) +} + fn load_certificate(path: &Path) -> Result { let data = fs::read(path).context(format!("Failed to read certificate {}", path.display()))?; diff --git a/src/vcek.rs b/src/vcek.rs index 30b49d3..3c90025 100644 --- a/src/vcek.rs +++ b/src/vcek.rs @@ -237,3 +237,39 @@ pub fn save_certificates( Ok(()) } + +pub fn fetch_crl(processor: &ProcType) -> Result> { + let url = format!("{}/vcek/v1/{}/crl", KDS_BASE_URL, processor.to_kds_url()); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(&url) + .send() + .context("Failed to send request to AMD KDS for CRL")?; + + response + .error_for_status_ref() + .context("AMD KDS returned error for CRL request")?; + + Ok(response + .bytes() + .context("Failed to read CRL from response")? + .to_vec()) +} + +pub fn save_crl(crl_der: &[u8], output_dir: &Path, verbose: bool) -> Result<()> { + fs::create_dir_all(output_dir).context(format!( + "Failed to create output directory {}", + output_dir.display() + ))?; + + let crl = openssl::x509::X509Crl::from_der(crl_der).context("Failed to parse CRL DER")?; + let pem = crl.to_pem().context("Failed to convert CRL to PEM")?; + let path = output_dir.join("crl.pem"); + fs::write(&path, &pem).context(format!("Failed to write CRL to {}", path.display()))?; + if verbose { + println!("Saved CRL to {}", path.display()); + } + + Ok(()) +}