From 4034ce6bcae9062990270d5a94acf55f69add459 Mon Sep 17 00:00:00 2001 From: Mohsen Zohrevandi Date: Thu, 12 Dec 2024 16:49:23 -0800 Subject: [PATCH] dcap-artifact-retrieval: Implement provisioning client for PCCS --- intel-sgx/dcap-artifact-retrieval/src/cli.rs | 64 +- intel-sgx/dcap-artifact-retrieval/src/lib.rs | 13 +- intel-sgx/dcap-artifact-retrieval/src/main.rs | 2 +- .../src/provisioning_client/azure.rs | 65 +- .../src/provisioning_client/common.rs | 91 +++ .../src/provisioning_client/intel.rs | 81 +-- .../src/provisioning_client/mod.rs | 40 +- .../src/provisioning_client/pccs.rs | 670 ++++++++++++++++++ 8 files changed, 887 insertions(+), 139 deletions(-) create mode 100644 intel-sgx/dcap-artifact-retrieval/src/provisioning_client/common.rs create mode 100644 intel-sgx/dcap-artifact-retrieval/src/provisioning_client/pccs.rs diff --git a/intel-sgx/dcap-artifact-retrieval/src/cli.rs b/intel-sgx/dcap-artifact-retrieval/src/cli.rs index 6649ba6e..a916479c 100644 --- a/intel-sgx/dcap-artifact-retrieval/src/cli.rs +++ b/intel-sgx/dcap-artifact-retrieval/src/cli.rs @@ -5,18 +5,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use std::convert::TryInto; use std::path::{Path, PathBuf}; use clap::clap_app; use pcs::PckID; +use reqwest::Url; use rustc_serialize::hex::ToHex; use serde::de::{value, IntoDeserializer}; use serde::Deserialize; use crate::{ - AzureProvisioningClientBuilder, Error, IntelProvisioningClientBuilder, PcsVersion, - ProvisioningClient, StatusCode, + AzureProvisioningClientBuilder, Error, IntelProvisioningClientBuilder, + PccsProvisioningClientBuilder, PcsVersion, ProvisioningClient, StatusCode, }; #[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] @@ -24,6 +24,7 @@ use crate::{ enum Origin { Intel, Azure, + Pccs, } fn str_deserialize(s: &str) -> value::StrDeserializer { @@ -63,21 +64,10 @@ fn download_dcap_artifacts( println!(" Storing artifacts:"); } - // Fetch pckcerts, note that Azure does not support this API, instead we mimic it - let pckcerts = match prov_client.pckcerts(&pckid.enc_ppid, pckid.pce_id) { - Ok(pckcerts) => pckcerts, - Err(Error::RequestNotSupported) => prov_client - .pckcert( - None, - &pckid.pce_id, - &pckid.cpu_svn, - pckid.pce_isvsvn, - Some(&pckid.qe_id), - )? - .try_into() - .map_err(|e| Error::PCSDecodeError(format!("{}", e).into()))?, - Err(e) => return Err(e), - }; + // Fetch pckcerts, note that Azure and PCCS do not support this API, + // instead we mimic it using pckcert API. + let pckcerts = prov_client.pckcerts_with_fallback(&pckid)?; + let pckcerts_file = pckcerts.store(output_dir, pckid.qe_id.as_slice())?; if verbose { @@ -136,13 +126,23 @@ pub fn main() { match value { "3" => Ok(PcsVersion::V3), "4" => Ok(PcsVersion::V4), - _ => Err(format!( - "Expected 3 or 4, found `{}`", - value - )), + _ => Err(format!("Expected 3 or 4, found `{}`", value)), } } + fn is_url(value: String) -> Result<(), String> { + let url = Url::parse(&value) + .map_err(|e| format!("cannot parse `{}` as a valid URL: {}", value, e))?; + + if url.scheme() != "http" && url.scheme() != "https" { + return Err(format!( + "Expected an http or https URL found: `{}`", + url.scheme() + )); + } + Ok(()) + } + let matches = clap::clap_app!(("DCAP Artifact Retrieval Tool") => (author: "Fortanix") (about: "Fortanix ecdsa artifact retrieval tool for DCAP attestation") @@ -171,6 +171,15 @@ pub fn main() { @arg API_KEY: --("api-key") +takes_value "API key for authenticating with Intel provisioning service" ) + ( + @arg PCCS_URL: --("pccs-url") +takes_value required_if("ORIGIN", "pccs") + validator(is_url) + "PCCS base URL. This is relevant only when using `--origin pccs`." + ) + ( + @arg INSECURE: -k --insecure + "Do not verify that server's hostname matches their TLS certificate and accept self-signed certificates. This is insecure." + ) ( @arg VERBOSE: -v --verbose "Print information of which files are fetched" @@ -190,7 +199,11 @@ pub fn main() { let origin = parse_origin(matches.value_of("ORIGIN").unwrap_or("intel")).expect("validated"); - let fetcher = crate::reqwest_client(); + let fetcher = match matches.is_present("INSECURE") { + false => crate::reqwest_client(), + true => crate::reqwest_client_insecure_tls(), + }; + let client: Box = match origin { Origin::Intel => { let mut client_builder = IntelProvisioningClientBuilder::new(api_version); @@ -203,6 +216,11 @@ pub fn main() { let client_builder = AzureProvisioningClientBuilder::new(api_version); Box::new(client_builder.build(fetcher)) } + Origin::Pccs => { + let pccs_url = matches.value_of("PCCS_URL").expect("validated").to_owned(); + let client_builder = PccsProvisioningClientBuilder::new(api_version, pccs_url); + Box::new(client_builder.build(fetcher)) + } }; download_dcap_artifacts(&*client, pckid_file, output_dir, 0 < verboseness) } diff --git a/intel-sgx/dcap-artifact-retrieval/src/lib.rs b/intel-sgx/dcap-artifact-retrieval/src/lib.rs index 82a21642..34b84362 100644 --- a/intel-sgx/dcap-artifact-retrieval/src/lib.rs +++ b/intel-sgx/dcap-artifact-retrieval/src/lib.rs @@ -8,7 +8,7 @@ //! DCAP attestations require access to Intel-signed artifacts. This library provides clients to //! access these artifacts both from Intel directly, and from Microsoft Azure. -#[cfg(not(target_env = "sgx"))] +#[cfg(all(not(target_env = "sgx"), feature = "reqwest"))] pub mod cli; pub mod provisioning_client; @@ -120,3 +120,14 @@ pub fn reqwest_client() -> ReqwestClient { .build() .expect("Failed to build reqwest client") } + +#[cfg(feature = "reqwest")] +#[doc(hidden)] +pub fn reqwest_client_insecure_tls() -> ReqwestClient { + ReqwestClient::builder() + .use_native_tls() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .expect("Failed to build reqwest client") +} diff --git a/intel-sgx/dcap-artifact-retrieval/src/main.rs b/intel-sgx/dcap-artifact-retrieval/src/main.rs index c63d56aa..3d21c15c 100644 --- a/intel-sgx/dcap-artifact-retrieval/src/main.rs +++ b/intel-sgx/dcap-artifact-retrieval/src/main.rs @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#[cfg(not(target_env = "sgx"))] +#[cfg(all(not(target_env = "sgx"), feature = "reqwest"))] fn main() { dcap_artifact_retrieval::cli::main() } diff --git a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/azure.rs b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/azure.rs index 8f9abb63..deeb179a 100644 --- a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/azure.rs +++ b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/azure.rs @@ -5,15 +5,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use pcs::{CpuSvn, EncPpid, PceId, PceIsvsvn, PckCert, PckCerts, QeId, Unverified}; +use pcs::{CpuSvn, EncPpid, PceId, PceIsvsvn, PckCert, QeId, Unverified}; use rustc_serialize::hex::ToHex; use serde::Deserialize; use std::time::Duration; +use super::common::PckCertsApiNotSupported; use super::intel::{PckCrlApi, QeIdApi, TcbInfoApi}; -use super::{Fetcher, ProvisioningServiceApi, StatusCode}; -use crate::provisioning_client::{ - Client, ClientBuilder, PckCertIn, PckCertService, PckCertsIn, PckCertsService, PcsVersion, +use super::{ + Client, ClientBuilder, Fetcher, PckCertIn, PckCertService, PcsVersion, ProvisioningServiceApi, + StatusCode, }; use crate::Error; @@ -41,7 +42,7 @@ impl AzureProvisioningClientBuilder { } pub fn build Fetcher<'a>>(self, fetcher: F) -> Client { - let pck_certs = PckCertsApi::new(None); + let pck_certs = PckCertsApiNotSupported; let pck_cert = PckCertApi::new(self.api_version.clone()); let pck_crl = PckCrlApi::new(self.api_version.clone()); let qeid = QeIdApi::new(self.api_version.clone()); @@ -51,58 +52,6 @@ impl AzureProvisioningClientBuilder { } } -pub struct PckCertsApi { - api_key: Option, -} - -impl PckCertsApi { - pub(crate) fn new(api_key: Option) -> PckCertsApi { - PckCertsApi { api_key } - } -} - -impl<'inp> PckCertsService<'inp> for PckCertsApi { - fn build_input( - &'inp self, - enc_ppid: &'inp EncPpid, - pce_id: PceId, - ) -> >::Input { - PckCertsIn { - enc_ppid, - pce_id, - api_key: &self.api_key, - api_version: PcsVersion::V3, - } - } -} - -/// Implementation of pckcerts -/// -impl<'inp> ProvisioningServiceApi<'inp> for PckCertsApi { - type Input = PckCertsIn<'inp>; - type Output = PckCerts; - - fn build_request( - &self, - _input: &Self::Input, - ) -> Result<(String, Vec<(String, String)>), Error> { - Err(Error::RequestNotSupported) - } - - fn validate_response(&self, _status_code: StatusCode) -> Result<(), Error> { - Err(Error::RequestNotSupported) - } - - fn parse_response( - &self, - _response_body: String, - _response_headers: Vec<(String, String)>, - _api_version: PcsVersion, - ) -> Result { - Err(Error::RequestNotSupported) - } -} - pub struct PckCertApi { api_version: PcsVersion, } @@ -200,7 +149,7 @@ impl<'inp> ProvisioningServiceApi<'inp> for PckCertApi { status_code, "PCS is temporarily unavailable", )), - __ => Err(Error::PCSError( + _ => Err(Error::PCSError( status_code.clone(), "Unexpected response from PCS server", )), diff --git a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/common.rs b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/common.rs new file mode 100644 index 00000000..6fca0a48 --- /dev/null +++ b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/common.rs @@ -0,0 +1,91 @@ +/* Copyright (c) Fortanix, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +use pcs::{EncPpid, PceId, PckCerts}; +use percent_encoding::percent_decode; +use pkix::pem::PemBlock; + +use super::{PckCertsIn, PckCertsService, PcsVersion, ProvisioningServiceApi, StatusCode}; +use crate::Error; + +pub const PCK_CERTIFICATE_ISSUER_CHAIN_HEADER: &'static str = "SGX-PCK-Certificate-Issuer-Chain"; +pub const PCK_CRL_ISSUER_CHAIN_HEADER: &'static str = "SGX-PCK-CRL-Issuer-Chain"; +pub const TCB_INFO_ISSUER_CHAIN_HEADER_V3: &'static str = "SGX-TCB-Info-Issuer-Chain"; +pub const TCB_INFO_ISSUER_CHAIN_HEADER_V4: &'static str = "TCB-Info-Issuer-Chain"; +pub const ENCLAVE_ID_ISSUER_CHAIN_HEADER: &'static str = "SGX-Enclave-Identity-Issuer-Chain"; + +pub struct PckCertsApiNotSupported; + +impl<'inp> PckCertsService<'inp> for PckCertsApiNotSupported { + fn build_input( + &'inp self, + enc_ppid: &'inp EncPpid, + pce_id: PceId, + ) -> >::Input { + PckCertsIn { + enc_ppid, + pce_id, + api_key: &None, + api_version: PcsVersion::V3, // does not matter, this API is not supported! + } + } +} + +impl<'inp> ProvisioningServiceApi<'inp> for PckCertsApiNotSupported { + type Input = PckCertsIn<'inp>; + type Output = PckCerts; + + fn build_request( + &self, + _input: &Self::Input, + ) -> Result<(String, Vec<(String, String)>), Error> { + Err(Error::RequestNotSupported) + } + + fn validate_response(&self, _status_code: StatusCode) -> Result<(), Error> { + Err(Error::RequestNotSupported) + } + + fn parse_response( + &self, + _response_body: String, + _response_headers: Vec<(String, String)>, + _api_version: PcsVersion, + ) -> Result { + Err(Error::RequestNotSupported) + } +} + +/// Returns the certificate chain starting from the leaf CA. +pub fn parse_issuer_header( + headers: &Vec<(String, String)>, + header: &'static str, +) -> Result, Error> { + let cert_chain = headers + .iter() + .find_map(|(key, value)| { + if key.to_lowercase() == header.to_lowercase() { + Some(value) + } else { + None + } + }) + .ok_or(Error::HeaderMissing(header))?; + + let cert_chain = percent_decode(cert_chain.as_bytes()) + .decode_utf8() + .map_err(|e| Error::HeaderDecodeError(e))?; + + let mut chain: Vec = vec![]; + for cert in PemBlock::new(cert_chain.as_bytes()) { + let cert = String::from_utf8(cert.to_vec()) + .map_err(|_| Error::CertificateParseError("Cert could not be decoded into utf8"))?; + + chain.push(cert); + } + Ok(chain) +} diff --git a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/intel.rs b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/intel.rs index 32a0310b..b7ccf306 100644 --- a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/intel.rs +++ b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/intel.rs @@ -5,7 +5,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -//! Interface to the Intel DCAP attestation API +//! Interface to the Intel DCAP attestation API. +//! //! Origins: //! - //! - @@ -14,18 +15,20 @@ use pcs::{ CpuSvn, EncPpid, PceId, PceIsvsvn, PckCert, PckCerts, PckCrl, QeId, QeIdentitySigned, TcbInfo, Unverified, }; -use percent_encoding::percent_decode; -use pkix::pem::PemBlock; use rustc_serialize::hex::ToHex; use std::time::Duration; +use super::common::*; use super::{ - Fetcher, PckCertIn, PckCertService, PckCertsIn, PckCertsService, PckCrlIn, PckCrlService, - ProvisioningServiceApi, QeIdIn, QeIdService, StatusCode, TcbInfoIn, TcbInfoService, + Client, ClientBuilder, Fetcher, PckCertIn, PckCertService, PckCertsIn, PckCertsService, + PckCrlIn, PckCrlService, PcsVersion, ProvisioningServiceApi, QeIdIn, QeIdService, StatusCode, + TcbInfoIn, TcbInfoService, }; -use crate::provisioning_client::{Client, ClientBuilder, PcsVersion}; use crate::Error; +const INTEL_BASE_URL: &'static str = "https://api.trustedservices.intel.com"; +const SUBSCRIPTION_KEY_HEADER: &'static str = "Ocp-Apim-Subscription-Key"; + pub struct IntelProvisioningClientBuilder { api_key: Option, api_version: PcsVersion, @@ -121,12 +124,13 @@ impl<'inp> ProvisioningServiceApi<'inp> for PckCertsApi { fn build_request(&self, input: &Self::Input) -> Result<(String, Vec<(String, String)>), Error> { let api_version = input.api_version as u8; let encrypted_ppid = input.enc_ppid.to_hex(); - let pceid = input.pce_id.to_le_bytes().to_hex(); + let pce_id = input.pce_id.to_le_bytes().to_hex(); let url = format!( - "https://api.trustedservices.intel.com/sgx/certification/v{api_version}/pckcerts?encrypted_ppid={encrypted_ppid}&pceid={pceid}", + "{}/sgx/certification/v{}/pckcerts?encrypted_ppid={}&pceid={}", + INTEL_BASE_URL, api_version, encrypted_ppid, pce_id, ); let headers = if let Some(api_key) = &input.api_key { - vec![("Ocp-Apim-Subscription-Key".to_owned(), api_key.to_string())] + vec![(SUBSCRIPTION_KEY_HEADER.to_owned(), api_key.to_string())] } else { Vec::new() }; @@ -167,7 +171,7 @@ impl<'inp> ProvisioningServiceApi<'inp> for PckCertsApi { response_headers: Vec<(String, String)>, _api_version: PcsVersion, ) -> Result { - let ca_chain = parse_issuer_header(&response_headers, "SGX-PCK-Certificate-Issuer-Chain")?; + let ca_chain = parse_issuer_header(&response_headers, PCK_CERTIFICATE_ISSUER_CHAIN_HEADER)?; PckCerts::parse(&response_body, ca_chain).map_err(|e| Error::OfflineAttestationError(e)) } } @@ -199,13 +203,14 @@ impl<'inp> ProvisioningServiceApi<'inp> for PckCertApi { .ok_or(Error::NoEncPPID) .map(|e_ppid| e_ppid.to_hex())?; let cpusvn = input.cpu_svn.to_hex(); - let pcesvn = input.pce_isvsvn.to_le_bytes().to_hex(); - let pceid = input.pce_id.to_le_bytes().to_hex(); + let pce_isvsvn = input.pce_isvsvn.to_le_bytes().to_hex(); + let pce_id = input.pce_id.to_le_bytes().to_hex(); let url = format!( - "https://api.trustedservices.intel.com/sgx/certification/v{api_version}/pckcert?encrypted_ppid={encrypted_ppid}&cpusvn={cpusvn}&pcesvn={pcesvn}&pceid={pceid}" + "{}/sgx/certification/v{}/pckcert?encrypted_ppid={}&cpusvn={}&pcesvn={}&pceid={}", + INTEL_BASE_URL, api_version, encrypted_ppid, cpusvn, pce_isvsvn, pce_id, ); let headers = if let Some(api_key) = input.api_key { - vec![("Ocp-Apim-Subscription-Key".to_owned(), api_key.to_string())] + vec![(SUBSCRIPTION_KEY_HEADER.to_owned(), api_key.to_string())] } else { Vec::new() }; @@ -246,7 +251,7 @@ impl<'inp> ProvisioningServiceApi<'inp> for PckCertApi { response_headers: Vec<(String, String)>, _api_version: PcsVersion, ) -> Result { - let ca_chain = parse_issuer_header(&response_headers, "SGX-PCK-Certificate-Issuer-Chain")?; + let ca_chain = parse_issuer_header(&response_headers, PCK_CERTIFICATE_ISSUER_CHAIN_HEADER)?; Ok(PckCert::new(response_body, ca_chain)) } } @@ -277,8 +282,8 @@ impl<'inp> ProvisioningServiceApi<'inp> for PckCrlApi { fn build_request(&self, input: &Self::Input) -> Result<(String, Vec<(String, String)>), Error> { let url = format!( - "https://api.trustedservices.intel.com/sgx/certification/v{}/pckcrl?ca=processor&encoding=pem", - input.api_version as u8 + "{}/sgx/certification/v{}/pckcrl?ca=processor&encoding=pem", + INTEL_BASE_URL, input.api_version as u8, ); Ok((url, Vec::new())) } @@ -312,7 +317,7 @@ impl<'inp> ProvisioningServiceApi<'inp> for PckCrlApi { response_headers: Vec<(String, String)>, _api_version: PcsVersion, ) -> Result { - let ca_chain = parse_issuer_header(&response_headers, "SGX-PCK-CRL-Issuer-Chain")?; + let ca_chain = parse_issuer_header(&response_headers, PCK_CRL_ISSUER_CHAIN_HEADER)?; let crl = PckCrl::new(response_body, ca_chain)?; Ok(crl) } @@ -350,8 +355,8 @@ impl<'inp> ProvisioningServiceApi<'inp> for TcbInfoApi { let api_version = input.api_version as u8; let fmspc = input.fmspc.to_hex(); let url = format!( - "https://api.trustedservices.intel.com/sgx/certification/v{}/tcb?fmspc={fmspc}", - api_version + "{}/sgx/certification/v{}/tcb?fmspc={}", + INTEL_BASE_URL, api_version, fmspc, ); Ok((url, Vec::new())) } @@ -389,8 +394,8 @@ impl<'inp> ProvisioningServiceApi<'inp> for TcbInfoApi { api_version: PcsVersion, ) -> Result { let key = match api_version { - PcsVersion::V3 => "SGX-TCB-Info-Issuer-Chain", - PcsVersion::V4 => "TCB-Info-Issuer-Chain", + PcsVersion::V3 => TCB_INFO_ISSUER_CHAIN_HEADER_V3, + PcsVersion::V4 => TCB_INFO_ISSUER_CHAIN_HEADER_V4, }; let ca_chain = parse_issuer_header(&response_headers, key)?; let tcb_info = TcbInfo::parse(&response_body, ca_chain)?; @@ -425,7 +430,8 @@ impl<'inp> ProvisioningServiceApi<'inp> for QeIdApi { fn build_request(&self, input: &Self::Input) -> Result<(String, Vec<(String, String)>), Error> { let api_version = input.api_version as u8; let url = format!( - "https://api.trustedservices.intel.com/sgx/certification/v{api_version}/qe/identity", + "{}/sgx/certification/v{}/qe/identity", + INTEL_BASE_URL, api_version, ); Ok((url, Vec::new())) } @@ -462,39 +468,12 @@ impl<'inp> ProvisioningServiceApi<'inp> for QeIdApi { response_headers: Vec<(String, String)>, _api_version: PcsVersion, ) -> Result { - let ca_chain = parse_issuer_header(&response_headers, "SGX-Enclave-Identity-Issuer-Chain")?; + let ca_chain = parse_issuer_header(&response_headers, ENCLAVE_ID_ISSUER_CHAIN_HEADER)?; let id = QeIdentitySigned::parse(&response_body, ca_chain)?; Ok(id) } } -/// Returns the certificate chain starting from the leaf CA -fn parse_issuer_header( - headers: &Vec<(String, String)>, - header: &'static str, -) -> Result, Error> { - let certchain = headers - .iter() - .find_map(|(key, value)| { - if key.to_lowercase() == header.to_lowercase() { - Some(value) - } else { - None - } - }) - .ok_or(Error::HeaderMissing(header))?; - let certchain = percent_decode(certchain.as_bytes()) - .decode_utf8() - .map_err(|e| Error::HeaderDecodeError(e))?; - let mut chain: Vec = vec![]; - for cert in PemBlock::new(certchain.as_bytes()) { - let cert = String::from_utf8(cert.to_vec()) - .map_err(|_| Error::CertificateParseError("Cert could not be decoded into utf8"))?; - chain.push(cert); - } - Ok(chain) -} - #[cfg(all(test, feature = "reqwest"))] mod tests { use std::hash::Hash; diff --git a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/mod.rs b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/mod.rs index f8dc90cb..f2343561 100644 --- a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/mod.rs +++ b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/mod.rs @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use std::hash::{DefaultHasher, Hash, Hasher}; use std::io::Read; use std::sync::Mutex; @@ -14,8 +14,8 @@ use std::time::{Duration, SystemTime}; use lru_cache::LruCache; use num_enum::TryFromPrimitive; use pcs::{ - CpuSvn, EncPpid, PceId, PceIsvsvn, PckCert, PckCerts, PckCrl, QeId, QeIdentitySigned, TcbInfo, - Unverified, + CpuSvn, EncPpid, PceId, PceIsvsvn, PckCert, PckCerts, PckCrl, PckID, QeId, QeIdentitySigned, + TcbInfo, Unverified, }; #[cfg(feature = "reqwest")] use reqwest::blocking::{Client as ReqwestClient, Response as ReqwestResponse}; @@ -23,10 +23,13 @@ use reqwest::blocking::{Client as ReqwestClient, Response as ReqwestResponse}; use crate::Error; pub mod azure; +pub(self) mod common; pub mod intel; +pub mod pccs; pub use self::azure::AzureProvisioningClientBuilder; pub use self::intel::IntelProvisioningClientBuilder; +pub use self::pccs::PccsProvisioningClientBuilder; // Taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml #[derive(Clone, Debug, Eq, PartialEq, TryFromPrimitive)] @@ -93,7 +96,10 @@ pub enum StatusCode { RequestHeaderFieldsTooLarge = 431, //432-450 Unassigned, UnavailableForLegalReasons = 451, - //452-499 Unassigned, + //452-460 Unassigned, + NonStandard461 = 461, // used by PCCS + NonStandard462 = 462, // used by PCCS + //463-499 Unassigned, InternalServerError = 500, NotImplemented = 501, BadGateway = 502, @@ -107,7 +113,7 @@ pub enum StatusCode { NotExtended = 510, // OBSOLETED NetworkAuthenticationRequired = 511, //512-599 Unassigned - #[num_enum(alternatives = [104..=199, 209..=225, 227..=299, 309..=399, 419, 420, 427, 430, 432..=450, 452..=499, 509, 512..=598])] + #[num_enum(alternatives = [104..=199, 209..=225, 227..=299, 309..=399, 419, 420, 427, 430, 432..=450, 452..=460, 463..=499, 509, 512..=598])] Unassigned = 599, } @@ -529,6 +535,30 @@ pub trait ProvisioningClient { fn pckcrl(&self) -> Result; fn qe_identity(&self) -> Result; + + /// Retrieve PCK certificates using `pckcerts()` and fallback to `pckcert()` + /// if provisioning client does not support pckcerts. Note that in case of + /// fallback the returned PckCerts will only contain a single certificate + /// associated with the highest TCB level applied to the platform. + fn pckcerts_with_fallback(&self, pck_id: &PckID) -> Result { + match self.pckcerts(&pck_id.enc_ppid, pck_id.pce_id) { + Ok(pck_certs) => return Ok(pck_certs), + Err(Error::RequestNotSupported) => {} // fallback below + Err(e) => return Err(e), + } + // fallback: + let pck_cert = self.pckcert( + Some(&pck_id.enc_ppid), + &pck_id.pce_id, + &pck_id.cpu_svn, + pck_id.pce_isvsvn, + Some(&pck_id.qe_id), + )?; + + pck_cert + .try_into() + .map_err(|e| Error::PCSDecodeError(format!("{}", e).into())) + } } impl Fetcher<'a>> ProvisioningClient for Client { diff --git a/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/pccs.rs b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/pccs.rs new file mode 100644 index 00000000..4f7eb530 --- /dev/null +++ b/intel-sgx/dcap-artifact-retrieval/src/provisioning_client/pccs.rs @@ -0,0 +1,670 @@ +/* Copyright (c) Fortanix, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +//! Interface to the Intel SGX Provisioning Certificate Caching Service (PCCS). +//! +//! Reference: +//! + +use std::borrow::Cow; +use std::time::Duration; + +use pcs::{ + CpuSvn, EncPpid, PceId, PceIsvsvn, PckCert, PckCrl, QeId, QeIdentitySigned, TcbInfo, Unverified, +}; +use rustc_serialize::hex::{FromHex, ToHex}; + +use super::common::*; +use super::{ + Client, ClientBuilder, Fetcher, PckCertIn, PckCertService, PckCrlIn, PckCrlService, PcsVersion, + ProvisioningServiceApi, QeIdIn, QeIdService, StatusCode, TcbInfoIn, TcbInfoService, +}; +use crate::Error; + +pub struct PccsProvisioningClientBuilder { + base_url: Cow<'static, str>, + api_version: PcsVersion, + client_builder: ClientBuilder, +} + +impl PccsProvisioningClientBuilder { + pub fn new>>(api_version: PcsVersion, base_url: T) -> Self { + Self { + base_url: base_url.into(), + api_version, + client_builder: ClientBuilder::new(), + } + } + + pub fn set_retry_timeout(mut self, retry_timeout: Duration) -> Self { + self.client_builder = self.client_builder.set_retry_timeout(retry_timeout); + self + } + + pub fn build Fetcher<'a>>(self, fetcher: F) -> Client { + let pck_certs = PckCertsApiNotSupported; + let pck_cert = PckCertApi::new(self.base_url.clone(), self.api_version); + let pck_crl = PckCrlApi::new(self.base_url.clone(), self.api_version); + let qeid = QeIdApi::new(self.base_url.clone(), self.api_version); + let tcbinfo = TcbInfoApi::new(self.base_url.clone(), self.api_version); + self.client_builder + .build(pck_certs, pck_cert, pck_crl, qeid, tcbinfo, fetcher) + } +} + +pub struct PckCertApi { + base_url: Cow<'static, str>, + api_version: PcsVersion, +} + +impl PckCertApi { + pub(crate) fn new(base_url: Cow<'static, str>, api_version: PcsVersion) -> PckCertApi { + PckCertApi { + base_url, + api_version, + } + } +} + +impl<'inp> PckCertService<'inp> for PckCertApi { + fn build_input( + &'inp self, + encrypted_ppid: Option<&'inp EncPpid>, + pce_id: &'inp PceId, + cpu_svn: &'inp CpuSvn, + pce_isvsvn: PceIsvsvn, + qe_id: Option<&'inp QeId>, + ) -> >::Input { + PckCertIn { + encrypted_ppid, + pce_id, + cpu_svn, + pce_isvsvn, + qe_id, + api_key: &None, + api_version: self.api_version, + } + } +} + +/// Implementation of Get PCK Certificate API (section 3.1 in the [reference]). +/// +/// [reference]: +impl<'inp> ProvisioningServiceApi<'inp> for PckCertApi { + type Input = PckCertIn<'inp>; + type Output = PckCert; + + fn build_request(&self, input: &Self::Input) -> Result<(String, Vec<(String, String)>), Error> { + let api_version = input.api_version as u8; + let encrypted_ppid = input + .encrypted_ppid + .ok_or(Error::NoEncPPID) + .map(|e_ppid| e_ppid.to_hex())?; + + let cpu_svn = input.cpu_svn.to_hex(); + let pce_isvsvn = input.pce_isvsvn.to_le_bytes().to_hex(); + let pce_id = input.pce_id.to_le_bytes().to_hex(); + let qe_id = input + .qe_id + .ok_or(Error::NoQeID) + .map(|qe_id| qe_id.to_hex())?; + + let url = format!( + "{}/sgx/certification/v{}/pckcert?encrypted_ppid={}&cpusvn={}&pcesvn={}&pceid={}&qeid={}", + self.base_url, api_version, encrypted_ppid, cpu_svn, pce_isvsvn, pce_id, qe_id, + ); + let headers = Vec::new(); + Ok((url, headers)) + } + + fn validate_response(&self, status_code: StatusCode) -> Result<(), Error> { + match status_code { + StatusCode::Ok => Ok(()), + StatusCode::BadRequest => { + Err(Error::PCSError(status_code, "Invalid request parameters")) + } + StatusCode::NotFound => Err(Error::PCSError( + status_code, + "No cache data for this platform", + )), + StatusCode::NonStandard461 => Err(Error::PCSError( + status_code, + "The platform was not found in the cache", + )), + StatusCode::NonStandard462 => Err(Error::PCSError( + status_code, + "Certificates are not available for certain TCBs", + )), + StatusCode::InternalServerError => Err(Error::PCSError( + status_code, + "PCCS suffered from an internal server error", + )), + StatusCode::BadGateway => Err(Error::PCSError( + status_code, + "Unable to retrieve the collateral from the Intel SGX PCS", + )), + _ => Err(Error::PCSError( + status_code, + "Unexpected response from PCCS server", + )), + } + } + + fn parse_response( + &self, + response_body: String, + response_headers: Vec<(String, String)>, + _api_version: PcsVersion, + ) -> Result { + let ca_chain = parse_issuer_header(&response_headers, PCK_CERTIFICATE_ISSUER_CHAIN_HEADER)?; + Ok(PckCert::new(response_body, ca_chain)) + } +} + +pub struct PckCrlApi { + base_url: Cow<'static, str>, + api_version: PcsVersion, +} + +impl PckCrlApi { + pub fn new(base_url: Cow<'static, str>, api_version: PcsVersion) -> Self { + PckCrlApi { + base_url, + api_version, + } + } +} + +impl<'inp> PckCrlService<'inp> for PckCrlApi { + fn build_input(&'inp self) -> >::Input { + PckCrlIn { + api_version: self.api_version, + } + } +} + +/// Implementation of Get PCK Cert CRL API (section 3.2 of [reference]). +/// +/// [reference]: +impl<'inp> ProvisioningServiceApi<'inp> for PckCrlApi { + type Input = PckCrlIn; + type Output = PckCrl; + + fn build_request(&self, input: &Self::Input) -> Result<(String, Vec<(String, String)>), Error> { + let url = format!( + "{}/sgx/certification/v{}/pckcrl?ca=processor", + self.base_url, input.api_version as u8 + ); + Ok((url, Vec::new())) + } + + fn validate_response(&self, status_code: StatusCode) -> Result<(), Error> { + match &status_code { + StatusCode::Ok => Ok(()), + StatusCode::BadRequest => { + Err(Error::PCSError(status_code, "Invalid request parameters")) + } + StatusCode::NotFound => Err(Error::PCSError(status_code, "PCK CRL cannot be found")), + StatusCode::InternalServerError => Err(Error::PCSError( + status_code, + "PCCS suffered from an internal server error", + )), + StatusCode::BadGateway => Err(Error::PCSError( + status_code, + "Unable to retrieve the collateral from the Intel SGX PCS", + )), + _ => Err(Error::PCSError( + status_code, + "Unexpected response from PCCS server", + )), + } + } + + fn parse_response( + &self, + response_body: String, + response_headers: Vec<(String, String)>, + _api_version: PcsVersion, + ) -> Result { + let ca_chain = parse_issuer_header(&response_headers, PCK_CRL_ISSUER_CHAIN_HEADER)?; + let pem_crl = pkix::pem::der_to_pem( + &response_body.from_hex().map_err(|e| { + Error::ReadResponseError( + format!("failed to parse response body as hex-encoded DER: {}", e).into(), + ) + })?, + pkix::pem::PEM_CRL, + ); + Ok(PckCrl::new(pem_crl, ca_chain)?) + } +} + +pub struct TcbInfoApi { + base_url: Cow<'static, str>, + api_version: PcsVersion, +} + +impl TcbInfoApi { + pub fn new(base_url: Cow<'static, str>, api_version: PcsVersion) -> Self { + TcbInfoApi { + base_url, + api_version, + } + } +} + +impl<'inp> TcbInfoService<'inp> for TcbInfoApi { + fn build_input( + &'inp self, + fmspc: &'inp Vec, + ) -> >::Input { + TcbInfoIn { + api_version: self.api_version, + fmspc, + } + } +} + +/// Implementation of Get TCB Info API (section 3.3 of [reference]). +/// +/// [reference]: +impl<'inp> ProvisioningServiceApi<'inp> for TcbInfoApi { + type Input = TcbInfoIn<'inp>; + type Output = TcbInfo; + + fn build_request(&self, input: &Self::Input) -> Result<(String, Vec<(String, String)>), Error> { + let api_version = input.api_version as u8; + let fmspc = input.fmspc.to_hex(); + let url = format!( + "{}/sgx/certification/v{}/tcb?fmspc={}", + self.base_url, api_version, fmspc + ); + Ok((url, Vec::new())) + } + + fn validate_response(&self, status_code: StatusCode) -> Result<(), Error> { + match &status_code { + StatusCode::Ok => Ok(()), + StatusCode::BadRequest => { + Err(Error::PCSError(status_code, "Invalid request parameters")) + } + StatusCode::NotFound => Err(Error::PCSError( + status_code, + "TCB information for provided FMSPC cannot be found", + )), + StatusCode::InternalServerError => Err(Error::PCSError( + status_code, + "PCCS suffered from an internal server error", + )), + StatusCode::BadGateway => Err(Error::PCSError( + status_code, + "Unable to retrieve the collateral from the Intel SGX PCS", + )), + _ => Err(Error::PCSError( + status_code, + "Unexpected response from PCCS server", + )), + } + } + + fn parse_response( + &self, + response_body: String, + response_headers: Vec<(String, String)>, + api_version: PcsVersion, + ) -> Result { + let key = match api_version { + PcsVersion::V3 => TCB_INFO_ISSUER_CHAIN_HEADER_V3, + PcsVersion::V4 => TCB_INFO_ISSUER_CHAIN_HEADER_V4, + }; + let ca_chain = parse_issuer_header(&response_headers, key)?; + Ok(TcbInfo::parse(&response_body, ca_chain)?) + } +} + +pub struct QeIdApi { + base_url: Cow<'static, str>, + api_version: PcsVersion, +} + +impl QeIdApi { + pub fn new(base_url: Cow<'static, str>, api_version: PcsVersion) -> Self { + QeIdApi { + base_url, + api_version, + } + } +} + +impl<'inp> QeIdService<'inp> for QeIdApi { + fn build_input(&'inp self) -> >::Input { + QeIdIn { + api_version: self.api_version, + } + } +} + +/// Implementation of Get Intel's QE Identity API (section 3.4 of [reference]). +/// +/// [reference]: +impl<'inp> ProvisioningServiceApi<'inp> for QeIdApi { + type Input = QeIdIn; + type Output = QeIdentitySigned; + + fn build_request(&self, input: &Self::Input) -> Result<(String, Vec<(String, String)>), Error> { + let api_version = input.api_version as u8; + let url = format!( + "{}/sgx/certification/v{}/qe/identity", + self.base_url, api_version, + ); + Ok((url, Vec::new())) + } + + fn validate_response(&self, status_code: StatusCode) -> Result<(), Error> { + match &status_code { + StatusCode::Ok => Ok(()), + StatusCode::NotFound => Err(Error::PCSError( + status_code, + "QE identity information cannot be found", + )), + StatusCode::InternalServerError => Err(Error::PCSError( + status_code, + "PCCS suffered from an internal server error", + )), + StatusCode::BadGateway => Err(Error::PCSError( + status_code, + "Unable to retrieve the collateral from the Intel SGX PCS", + )), + _ => Err(Error::PCSError( + status_code, + "Unexpected response from PCCS server", + )), + } + } + + fn parse_response( + &self, + response_body: String, + response_headers: Vec<(String, String)>, + _api_version: PcsVersion, + ) -> Result { + let ca_chain = parse_issuer_header(&response_headers, ENCLAVE_ID_ISSUER_CHAIN_HEADER)?; + let id = QeIdentitySigned::parse(&response_body, ca_chain)?; + Ok(id) + } +} + +#[cfg(all(test, feature = "reqwest"))] +mod tests { + use std::hash::{DefaultHasher, Hash, Hasher}; + use std::path::PathBuf; + use std::time::Duration; + + use pcs::PckID; + + use super::Client; + use crate::provisioning_client::{ + test_helpers, PccsProvisioningClientBuilder, PcsVersion, ProvisioningClient, + }; + use crate::{reqwest_client_insecure_tls, ReqwestClient}; + + const PCKID_TEST_FILE: &str = "./tests/data/pckid_retrieval.csv"; + const OUTPUT_TEST_DIR: &str = "./tests/data/"; + const TIME_RETRY_TIMEOUT: Duration = Duration::from_secs(180); + const PCCS_URL: &'static str = "https://localhost:8081"; + + fn make_client(api_version: PcsVersion) -> Client { + PccsProvisioningClientBuilder::new(api_version, PCCS_URL) + .set_retry_timeout(TIME_RETRY_TIMEOUT) + .build(reqwest_client_insecure_tls()) + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn pck() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + + for pckid in PckID::parse_file(&PathBuf::from(PCKID_TEST_FILE).as_path()) + .unwrap() + .iter() + { + let pck = client + .pckcert( + Some(&pckid.enc_ppid), + &pckid.pce_id, + &pckid.cpu_svn, + pckid.pce_isvsvn, + Some(&pckid.qe_id), + ) + .unwrap(); + + assert_eq!( + test_helpers::get_cert_subject(pck.ca_chain().last().unwrap()), + "Intel SGX Root CA" + ); + } + } + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn pck_cached() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + + for pckid in PckID::parse_file(&PathBuf::from(PCKID_TEST_FILE).as_path()) + .unwrap() + .iter() + { + let pck = client + .pckcert( + Some(&pckid.enc_ppid), + &pckid.pce_id, + &pckid.cpu_svn, + pckid.pce_isvsvn, + Some(&pckid.qe_id), + ) + .unwrap(); + + // The cache should be populated after initial service call + { + let mut cache = client.pckcert_service.cache.lock().unwrap(); + + assert!(cache.len() > 0); + + let (cached_pck, _) = { + let mut hasher = DefaultHasher::new(); + let input = client.pckcert_service.pcs_service().build_input( + Some(&pckid.enc_ppid), + &pckid.pce_id, + &pckid.cpu_svn, + pckid.pce_isvsvn, + Some(&pckid.qe_id), + ); + input.hash(&mut hasher); + + cache + .get_mut(&hasher.finish()) + .expect("Can't find key in cache") + .to_owned() + }; + + assert_eq!(pck.fmspc().unwrap(), cached_pck.fmspc().unwrap()); + assert_eq!(pck.ca_chain(), cached_pck.ca_chain()); + } + + // Second service call should return value from cache + let pck_from_service = client + .pckcert( + Some(&pckid.enc_ppid), + &pckid.pce_id, + &pckid.cpu_svn, + pckid.pce_isvsvn, + Some(&pckid.qe_id), + ) + .unwrap(); + + assert_eq!(pck.fmspc().unwrap(), pck_from_service.fmspc().unwrap()); + assert_eq!(pck.ca_chain(), pck_from_service.ca_chain()); + } + } + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn tcb_info() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + + for pckid in PckID::parse_file(&PathBuf::from(PCKID_TEST_FILE).as_path()) + .unwrap() + .iter() + { + let pckcerts = client.pckcerts_with_fallback(&pckid).unwrap(); + + assert!(client + .tcbinfo(&pckcerts.fmspc().unwrap()) + .and_then(|tcb| { Ok(tcb.store(OUTPUT_TEST_DIR).unwrap()) }) + .is_ok()); + } + } + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn tcb_info_cached() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + + for pckid in PckID::parse_file(&PathBuf::from(PCKID_TEST_FILE).as_path()) + .unwrap() + .iter() + { + let pckcerts = client.pckcerts_with_fallback(&pckid).unwrap(); + let fmspc = pckcerts.fmspc().unwrap(); + let tcb_info = client.tcbinfo(&fmspc).unwrap(); + + // The cache should be populated after initial service call + { + let mut cache = client.tcbinfo_service.cache.lock().unwrap(); + + assert!(cache.len() > 0); + + let (cached_tcb_info, _) = { + let mut hasher = DefaultHasher::new(); + let input = client.tcbinfo_service.pcs_service().build_input(&fmspc); + input.hash(&mut hasher); + + cache + .get_mut(&hasher.finish()) + .expect("Can't find key in cache") + .to_owned() + }; + + assert_eq!(tcb_info, cached_tcb_info); + } + + // Second service call should return value from cache + let tcb_info_from_service = client.tcbinfo(&fmspc).unwrap(); + + assert_eq!(tcb_info, tcb_info_from_service); + } + } + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn pckcrl() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + assert!(client + .pckcrl() + .and_then(|crl| Ok(crl.write_to_file(OUTPUT_TEST_DIR).unwrap())) + .is_ok()); + } + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn pckcrl_cached() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + let pckcrl = client.pckcrl().unwrap(); + + // The cache should be populated after initial service call + { + let mut cache = client.pckcrl_service.cache.lock().unwrap(); + + assert!(cache.len() > 0); + + let (cached_pckcrl, _) = { + let mut hasher = DefaultHasher::new(); + let input = client.pckcrl_service.pcs_service().build_input(); + input.hash(&mut hasher); + + cache + .get_mut(&hasher.finish()) + .expect("Can't find key in cache") + .to_owned() + }; + + assert_eq!(pckcrl, cached_pckcrl); + } + + // Second service call should return value from cache + let pckcrl_from_service = client.pckcrl().unwrap(); + + assert_eq!(pckcrl, pckcrl_from_service); + } + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn qe_identity() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + let qe_id = client.qe_identity(); + assert!(qe_id.is_ok()); + assert!(qe_id.unwrap().write_to_file(OUTPUT_TEST_DIR).is_ok()); + } + } + + #[test] + #[ignore = "needs a running PCCS service"] // FIXME + pub fn qe_identity_cached() { + for api_version in [PcsVersion::V3, PcsVersion::V4] { + let client = make_client(api_version); + let qe_id = client.qe_identity().unwrap(); + + // The cache should be populated after initial service call + { + let mut cache = client.qeid_service.cache.lock().unwrap(); + + assert!(cache.len() > 0); + + let (cached_qeid, _) = { + let mut hasher = DefaultHasher::new(); + let input = client.qeid_service.pcs_service().build_input(); + input.hash(&mut hasher); + + cache + .get_mut(&hasher.finish()) + .expect("Can't find key in cache") + .to_owned() + }; + + assert_eq!(qe_id, cached_qeid); + } + + // Second service call should return value from cache + let qeid_from_service = client.qe_identity().unwrap(); + + assert_eq!(qe_id, qeid_from_service); + } + } +}