Skip to content
Open
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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} \
Expand All @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(())
Expand Down
77 changes: 76 additions & 1 deletion src/sev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")?
Expand Down Expand Up @@ -119,6 +125,75 @@ fn find_cert_in_dir(dir: &Path, name: &str) -> Result<PathBuf> {
))
}

fn load_crl(path: &Path) -> Result<X509Crl> {
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<Certificate> {
let data = fs::read(path).context(format!("Failed to read certificate {}", path.display()))?;

Expand Down
36 changes: 36 additions & 0 deletions src/vcek.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,39 @@ pub fn save_certificates(

Ok(())
}

pub fn fetch_crl(processor: &ProcType) -> Result<Vec<u8>> {
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(())
}