diff --git a/attestation-service/docs/amd-offline-certificate-cache.md b/attestation-service/docs/amd-offline-certificate-cache.md new file mode 100644 index 0000000000..38fda9c597 --- /dev/null +++ b/attestation-service/docs/amd-offline-certificate-cache.md @@ -0,0 +1,185 @@ +# Offline AMD VCEK Caching Guide + +This document describes the process to pre-load VCEKs (Versioned Chip +Endorsement Keys) into the trustee environment to allow normal attestation +without connection to the AMD KDS. This guide mainly targets air-gapped +environments. + +> [!Note] +> Currently this guide shows a method that builds on using +> [docker](https://confidentialcontainers.org/docs/attestation/installation/docker/) +> to manage kbs services. Additional deployment methods will be covered in future releases. + +## Enabling Offline VCEK Store in Attestation Service + +### 1. Set VCEK Sources Configuration Option + +Update the attestation configuration file to use a predefined vcek store: + +`kbs/config/as-config.json`: + +```json +{ + // ... other fields ... + "verifier_config": { + "snp_verifier": { + // Configure VCEK sources to try, in order. Defaults to [KDS]. + "vcek_sources": [ + { + "type": "OfflineStore", + }, + // Optionally add a fallback to KDS. Leave out if KDS will not + // be reachable. + //{ + // "type": "KDS", + //} + ] + } + } +} +``` + +With the `OfflineStore` configuration specified, Trustee will inspect the +configured directory for VCEK values following the following format: + +``` +/opt/confidential-containers/attestation-service/kds-store/vcek/{hwid}/vcek.der +``` + +Where `{hwid}` is the hardware ID of the AMD EPYC server in lowercase hexadecimal. + +### 2. Create and Populate VCEK directory + +Create a `vcek` directory populated with the following structure: + +``` +vcek/ +├── / (ID must be lowercase) +│ └── vcek.der (cert pre-downloaded from kdsintf.amd.com) +├── / +│ └── vcek.der +├── / + └── vcek.der +``` + +Trustee requires one unique certificate per physical AMD EPYC server that the +KBS will be servicing. The server's hardware ID and certificate's URL can be +fetched using the [snphost tool](https://github.com/virtee/snphost). + +On each AMD SNP host, with root/admin access run: +``` +sudo snphost show vcek-url +``` + +You should get a URL of the form: +``` +https://kdsintf.amd.com/vcek/{version}/{machine}/{product_name}/{hwid}?{params} +``` + +You may download it from a browser by pasting that URL, or you can run +the following command on any server with network access to AMD's KDS. +``` +sudo snphost fetch vcek der . +``` + +> [!IMPORTANT] +> - Note that the VCEK URL is specific to the hardware AMD firmware of the +> machine. If the firmware is updated, the VCEK URL will change. See the +> [AMD VCEK documentation](https://docs.amd.com/api/khub/documents/dWGhwYpo1Wv51rJN4d~47g/content) +> for more information about the VCEK URL format. + +### 3. Install `vcek` directory into trustee deployment + +- **Install in running trustee deployment** + +Use docker commands to copy your `vcek` folder into the configured directory: +``` +sudo docker exec -it trustee-as-1 mkdir -p /opt/confidential-containers/attestation-service/kds-store/ +sudo docker cp ./vcek/ trustee-as-1:/opt/confidential-containers/attestation-service/kds-store/vcek/ +``` + +- **Mount shared directory** + +You may also mount a shared directory from the host into the container by +updating `docker-compose.yml` with a specified directory mapping: + +```yaml + as: + ... + volumes: + ... + - ./vcek:/opt/confidential-containers/attestation-service/kds-store/vcek:rw +``` + +### Example: + +For some number of AMD EPYC servers you wish for trustee to service: +``` +ssh privileged-user@epyc-host "sudo snphost show vcek-url" >> urls.txt +``` + +On a system with network access to AMD KDS: +```bash + +# Create the vcek folder that will be copied to trustee +mkdir vcek + +# Fetch certificates using the above URLs file +# There should be one line for each EPYC host that kds will service +cat urls.txt | while read line; do + hwid="$(echo "$line" | cut -d/ -f7 | cut -d'?' -f1 | tr '[:upper:]' '[:lower:]')" + mkdir vcek/$hwid + cd vcek/$hwid + sudo snphost fetch vcek der . + cd ../.. +done + +# Copy the archive to your air-gapped trustee attestation-service deployment +scp -r vcek user@target-host:/path/to/trustee +``` + +On the air-gapped trustee attestation-service host: +```bash +# Update as-config.json to enable Disk Caching +cd /path/to/trustee +vi kbs/config/as-config.json +# Update the verifier_config section to: +# "verifier_config": { +# "snp_verifier": { +# "vcek_sources": [ +# { +# "type": "OfflineStore", +# } +# ] +# } +# } + +# Start trustee +docker compose up -d + +# Copy the vcek store into the running attestation service container +sudo docker exec -it trustee-as-1 mkdir -p /opt/confidential-containers/attestation-service/kds-store/ +sudo docker cp ./vcek/ trustee-as-1:/opt/confidential-containers/attestation-service/kds-store/vcek/ + +# Alternative to docker copy would be to add the following shared mount to +# docker-compose.yml under as.volumes section: +# - ./vcek:/opt/confidential-containers/attestation-service/kds-store/vcek:rw +``` + +## Limitations + +VCEK stores must be updated/rebuilt in the following events: +- AMD EPYC host added to serviced cluster +- AMD EPYC host firmware components updated +- VCEK certificate revoked by CA + +## Troubleshooting + +Enable debug and check logs for `vcek` keyword. Ensure configured sources match +expected values. + +``` +echo "RUST_LOG=debug" > debug.env +docker compose --env-file debug.env up -d +docker logs trustee-as-1 | grep -i vcek +``` diff --git a/deps/verifier/src/lib.rs b/deps/verifier/src/lib.rs index 534dc62f5e..27bd20d432 100644 --- a/deps/verifier/src/lib.rs +++ b/deps/verifier/src/lib.rs @@ -56,6 +56,9 @@ pub struct VerifierConfig { #[cfg(feature = "tpm-verifier")] tpm_verifier: Option, + + #[cfg(feature = "snp-verifier")] + snp_verifier: Option, } pub async fn to_verifier( @@ -95,8 +98,8 @@ pub async fn to_verifier( Tee::Snp => { cfg_if::cfg_if! { if #[cfg(feature = "snp-verifier")] { - let verifier = snp::Snp::default(); - Ok(Box::new(verifier) as Box) + let snp_config = _config.map(|c| c.snp_verifier).unwrap_or(None); + Ok(Box::::new(snp::Snp::new(snp_config).await?) as Box) } else { bail!("feature `snp-verifier` is not enabled for `verifier` crate.") } diff --git a/deps/verifier/src/snp/mod.rs b/deps/verifier/src/snp/mod.rs index 30434aec62..3398ed6c52 100644 --- a/deps/verifier/src/snp/mod.rs +++ b/deps/verifier/src/snp/mod.rs @@ -64,6 +64,9 @@ pub(crate) const FMC_SPL_OID: Oid<'static> = oid!(1.3.6 .1 .4 .1 .3704 .1 .3 .9) const KDS_CERT_SITE: &str = "https://kdsintf.amd.com"; const KDS_VCEK: &str = "/vcek/v1"; +// KDS Offline Store +const KDS_OFFLINE_STORE_PATH: &str = "/opt/confidential-containers/attestation-service/kds-store"; + /// Attestation report versions supported const REPORT_VERSION_MIN: u32 = 3; const REPORT_VERSION_MAX: u32 = 5; @@ -106,24 +109,26 @@ fn init_cache_manager() -> MokaManager { } #[derive(Clone, Debug, Default)] -pub struct Snp {} +pub struct Snp { + verifier_config: SnpVerifierConfig, +} impl Snp { - pub async fn new() -> Result { - Ok(Snp {}) + pub async fn new(config: Option) -> Result { + Ok(Snp { + verifier_config: config.unwrap_or_default(), + }) } - pub fn build_vcek_client(&self) -> reqwest_middleware::ClientWithMiddleware { + fn build_vcek_client(&self) -> reqwest_middleware::ClientWithMiddleware { let client_options = HttpCacheOptions { cache_status_headers: true, ..Default::default() }; - let manager = VCEK_CACHE_MANAGER.get_or_init(init_cache_manager).clone(); - let cache = Cache(HttpCache { mode: CacheMode::Default, - manager, + manager: VCEK_CACHE_MANAGER.get_or_init(init_cache_manager).clone(), options: client_options, }); @@ -132,25 +137,79 @@ impl Snp { .build() } - /// Asynchronously fetches the VCEK from the Key Distribution Service (KDS) using the provided attestation report. - /// Returns the VCEK in DER format. - async fn fetch_vcek_from_kds( + /// Fetches VCEK by trying each configured source in order until one succeeds + async fn fetch_vcek( &self, att_report: AttestationReport, proc_gen: ProcessorGeneration, ) -> Result> { - // Use attestation report to get data for URL - if att_report.chip_id.as_slice() == [0; 64] { - bail!("Hardware ID is 0s on attestation report. Confirm that MASK_CHIP_ID is set to 0 to request VCEK from KDS."); + for source in &self.verifier_config.vcek_sources { + let result = match source { + VCEKSource::OfflineStore { path } => { + self.fetch_vcek_from_offline_store(att_report, &proc_gen, path.clone()) + } + VCEKSource::KDS { base_url } => { + self.fetch_vcek_from_kds(att_report, &proc_gen, base_url.clone()) + .await + } + }; + + if let Ok(vcek_bytes) = result { + debug!("fetched vcek from {:?}", source); + return Ok(vcek_bytes); + } } - let hw_id = match proc_gen { + debug!( + "failed to fetch vcek from all configured sources {:?}", + self.verifier_config.vcek_sources + ); + bail!("Failed to fetch VCEK from any configured source") + } + + fn fetch_vcek_from_offline_store( + &self, + att_report: AttestationReport, + proc_gen: &ProcessorGeneration, + path: Option, + ) -> Result> { + // default dir should contain the /vcek segment + let path = path.unwrap_or(KDS_OFFLINE_STORE_PATH.to_string()); + let hw_id = self.parse_hw_id_from_vcek(att_report, proc_gen.clone()); + let vcek_path = format!("{}/vcek/{}/vcek.der", path, hw_id); + let vcek_bytes = std::fs::read(&vcek_path) + .with_context(|| format!("Failed to read VCEK from offline store at {}", vcek_path))?; + Ok(vcek_bytes) + } + + fn parse_hw_id_from_vcek( + &self, + att_report: AttestationReport, + proc_gen: ProcessorGeneration, + ) -> String { + match proc_gen { ProcessorGeneration::Turin => { let shorter_bytes: &[u8] = &att_report.chip_id[0..8]; hex::encode(shorter_bytes) } _ => hex::encode(att_report.chip_id), - }; + } + } + + /// Asynchronously fetches the VCEK from the Key Distribution Service (KDS) using the provided attestation report. + /// Returns the VCEK in DER format. + async fn fetch_vcek_from_kds( + &self, + att_report: AttestationReport, + proc_gen: &ProcessorGeneration, + kds_url: Option, + ) -> Result> { + // Use attestation report to get data for URL + if att_report.chip_id.as_slice() == [0; 64] { + bail!("Hardware ID is 0s on attestation report. Confirm that MASK_CHIP_ID is set to 0 to request VCEK from KDS."); + } + + let hw_id = self.parse_hw_id_from_vcek(att_report, proc_gen.clone()); // Request VCEK from KDS let vcek_url: String = match proc_gen { @@ -160,8 +219,9 @@ impl Snp { }; format!( - "{KDS_CERT_SITE}{KDS_VCEK}/{}/\ + "{}{KDS_VCEK}/{}/\ {hw_id}?fmcSPL={:02}&blSPL={:02}&teeSPL={:02}&snpSPL={:02}&ucodeSPL={:02}", + kds_url.unwrap_or(KDS_CERT_SITE.to_string()), proc_gen, fmc, att_report.reported_tcb.bootloader, @@ -172,8 +232,9 @@ impl Snp { } _ => { format!( - "{KDS_CERT_SITE}{KDS_VCEK}/{}/\ + "{}{KDS_VCEK}/{}/\ {hw_id}?blSPL={:02}&teeSPL={:02}&snpSPL={:02}&ucodeSPL={:02}", + kds_url.unwrap_or(KDS_CERT_SITE.to_string()), proc_gen, att_report.reported_tcb.bootloader, att_report.reported_tcb.tee, @@ -222,6 +283,31 @@ impl Snp { } } +fn default_vcek_sources() -> Vec { + vec![VCEKSource::KDS { base_url: None }] +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct SnpVerifierConfig { + #[serde(default = "default_vcek_sources")] + pub vcek_sources: Vec, +} + +impl Default for SnpVerifierConfig { + fn default() -> Self { + Self { + vcek_sources: default_vcek_sources(), + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum VCEKSource { + OfflineStore { path: Option }, + KDS { base_url: Option }, +} + #[derive(Clone, Debug)] pub(crate) enum VendorEndorsementKey { Vcek, @@ -349,13 +435,13 @@ impl Verifier for Snp { vek.clone() } - // No certificate chain provided, so we need to request the VCEK from KDS + // No certificate chain provided, so we need to request the VCEK from configured sources _ => { - // Get VCEK from KDS + // Get VCEK from configured sources (tries each in order) let vcek_buf = self - .fetch_vcek_from_kds(report, proc_gen.clone()) + .fetch_vcek(report, proc_gen.clone()) .await - .context("Failed to fetch VCEK from KDS")?; + .context("Failed to fetch VCEK from any configured source")?; let vcek = Certificate::from_bytes(&vcek_buf) .context("Failed to convert KDS VCEK into certificate")?;