diff --git a/Cargo.lock b/Cargo.lock index 0ac592f..35f1966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2603,6 +2603,21 @@ dependencies = [ "generic-array", ] +[[package]] +name = "intel-dcap-api" +version = "0.3.0" +dependencies = [ + "hex", + "percent-encoding", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.11", + "tokio", + "url", + "x509-cert", +] + [[package]] name = "intel-tee-quote-verification-sys" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 695755a..ae48f3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ enumset = { version = "1.1", features = ["serde"] } getrandom = { version = "0.3.1", features = ["std"] } gpt = "4.0.0" hex = { version = "0.4.3", features = ["std"], default-features = false } +intel-dcap-api = { path = "crates/intel-dcap-api" } num-integer = "0.1.46" num-traits = "0.2.18" opentelemetry = { version = "0.28.0", features = ["default", "logs"] } @@ -65,5 +66,5 @@ tracing-log = "0.2" tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "ansi"] } tracing-test = { version = "0.2.5", features = ["no-env-filter"] } url = "2.5.2" -x509-cert = { version = "0.2", features = ["builder", "signature"] } +x509-cert = { version = "0.2", features = ["builder", "signature", "default"] } zeroize = { version = "1.7.0", features = ["serde"] } diff --git a/crates/intel-dcap-api/Cargo.toml b/crates/intel-dcap-api/Cargo.toml new file mode 100644 index 0000000..df51592 --- /dev/null +++ b/crates/intel-dcap-api/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "intel-dcap-api" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +keywords = ["sgx", "tdx", "intel", "attestation", "confidential"] +categories = ["api-bindings", "cryptography", "authentication"] + +[dependencies] +percent-encoding = "2.3.1" +reqwest = { workspace = true, features = ["json"] } +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio.workspace = true +url.workspace = true + +[dev-dependencies] +hex.workspace = true +x509-cert.workspace = true + +[features] +default = ["reqwest/default-tls"] +rustls = ["reqwest/rustls-tls"] diff --git a/crates/intel-dcap-api/examples/example.rs b/crates/intel-dcap-api/examples/example.rs new file mode 100644 index 0000000..16c3523 --- /dev/null +++ b/crates/intel-dcap-api/examples/example.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +use intel_dcap_api::{ + ApiClient, ApiVersion, CaType, CrlEncoding, EnclaveIdentityResponse, IntelApiError, + PckCrlResponse, PlatformFilter, TcbInfoResponse, +}; + +#[tokio::main] +async fn main() -> Result<(), IntelApiError> { + for api_version in [ApiVersion::V3, ApiVersion::V4] { + println!("Using API version: {}", api_version); + + let client = ApiClient::new_with_version(api_version)?; + + // Example: Get SGX TCB Info + let fmspc_example = "00606A000000"; // Example FMSPC from docs + match client.get_sgx_tcb_info(fmspc_example, None, None).await { + Ok(TcbInfoResponse { + tcb_info_json, + issuer_chain, + }) => println!( + "SGX TCB Info for {}:\n{}\nIssuer Chain: {}", + fmspc_example, tcb_info_json, issuer_chain + ), + Err(e) => eprintln!("Error getting SGX TCB info: {}", e), + } + + // Example: Get FMSPCs + match client.get_fmspcs(Some(PlatformFilter::E3)).await { + // Filter for E3 platform type [cite: 230] + Ok(fmspc_list) => println!("\nE3 FMSPCs:\n{}", fmspc_list), + Err(e) => eprintln!("Error getting FMSPCs: {}", e), + } + + // Example: Get SGX QE Identity + match client.get_sgx_qe_identity(None, None).await { + Ok(EnclaveIdentityResponse { + enclave_identity_json, + issuer_chain, + }) => { + println!( + "\nSGX QE Identity:\n{}\nIssuer Chain: {}", + enclave_identity_json, issuer_chain + ) + } + Err(e) => eprintln!("Error getting SGX QE Identity: {}", e), + } + + // Example: Get PCK CRL (Platform CA, PEM encoding) + match client + .get_pck_crl(CaType::Platform, Some(CrlEncoding::Pem)) + .await + { + // [cite: 118, 119] + Ok(PckCrlResponse { + crl_data, + issuer_chain, + }) => { + // Attempt to decode PEM for display, otherwise show byte count + match String::from_utf8(crl_data.clone()) { + Ok(pem_string) => println!( + "\nPlatform PCK CRL (PEM):\n{}\nIssuer Chain: {}", + pem_string, issuer_chain + ), + Err(_) => println!( + "\nPlatform PCK CRL ({} bytes, likely DER):\nIssuer Chain: {}", + crl_data.len(), + issuer_chain + ), + } + } + Err(e) => eprintln!("Error getting PCK CRL: {}", e), + } + } + + Ok(()) +} diff --git a/crates/intel-dcap-api/examples/get_pck_crl.rs b/crates/intel-dcap-api/examples/get_pck_crl.rs new file mode 100644 index 0000000..72d0820 --- /dev/null +++ b/crates/intel-dcap-api/examples/get_pck_crl.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +use intel_dcap_api::{ApiClient, CaType, CrlEncoding, IntelApiError, PckCrlResponse}; +use x509_cert::{ + der::{oid::AssociatedOid, Decode, SliceReader}, + ext::pkix::{ + crl::dp::DistributionPoint, + name::{DistributionPointName, GeneralName}, + CrlDistributionPoints, + }, +}; + +#[tokio::main] +async fn main() -> Result<(), IntelApiError> { + let client = ApiClient::new()?; + + let PckCrlResponse { + crl_data, + issuer_chain, + } = client + .get_pck_crl(CaType::Platform, Some(CrlEncoding::Der)) + .await?; + + let certs = x509_cert::certificate::CertificateInner::< + x509_cert::certificate::Rfc5280 + >::load_pem_chain(issuer_chain.as_bytes()).map_err( + |_| IntelApiError::InvalidParameter("Could not load a PEM chain") + )?; + + for cert in certs { + println!("Issuer: {}", cert.tbs_certificate.issuer); + println!("Subject: {}", cert.tbs_certificate.subject); + println!("Serial Number: {}", cert.tbs_certificate.serial_number); + println!("Not Before: {}", cert.tbs_certificate.validity.not_before); + println!("Not After: {}", cert.tbs_certificate.validity.not_after); + + // Extract and print CRL distribution points + if let Some(extensions) = &cert.tbs_certificate.extensions { + for ext in extensions.iter() { + if ext.extn_id == CrlDistributionPoints::OID { + // Create a SliceReader from the byte slice + let mut reader = SliceReader::new(ext.extn_value.as_bytes()).map_err(|_| { + IntelApiError::InvalidParameter( + "Could not create reader from extension value", + ) + })?; + + // Now pass the reader to decode_value + if let Ok(dist_points) = Vec::::decode(&mut reader) { + for point in dist_points { + if let Some(DistributionPointName::FullName(names)) = + point.distribution_point + { + for name in names { + if let GeneralName::UniformResourceIdentifier(uri) = name { + let uri = uri.as_str(); + let crl_bytes = reqwest::get(uri).await?.bytes().await?; + println!("CRL bytes (hex): {}", hex::encode(&crl_bytes)); + } + } + } + } + } else { + println!("Could not decode CRL distribution points"); + } + } + } + } + } + + println!("CRL bytes (hex): {}", hex::encode(&crl_data)); + + Ok(()) +} diff --git a/crates/intel-dcap-api/src/client.rs b/crates/intel-dcap-api/src/client.rs new file mode 100644 index 0000000..294b6fb --- /dev/null +++ b/crates/intel-dcap-api/src/client.rs @@ -0,0 +1,1324 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +use crate::responses::AddPackageResponse; +use crate::{ + error::{check_status, extract_api_error_details, IntelApiError}, + requests::{PckCertRequest, PckCertsConfigRequest, PckCertsRequest}, + responses::{ + EnclaveIdentityResponse, PckCertificateResponse, PckCertificatesResponse, PckCrlResponse, + TcbEvaluationDataNumbersResponse, TcbInfoResponse, + }, + types::{ApiVersion, CaType, CrlEncoding, PlatformFilter, UpdateType}, // Import ApiVersion + FmspcJsonResponse, +}; +use percent_encoding::percent_decode_str; +use reqwest::{header, Client, IntoUrl, RequestBuilder, Response, StatusCode}; +use std::num::ParseIntError; +use url::Url; + +// Base URL for the Intel Trusted Services API +const BASE_URL: &str = "https://api.trustedservices.intel.com"; + +/// Client for interacting with Intel Trusted Services API. +/// +/// Provides methods to access both SGX and TDX certification services, +/// supporting API versions V3 and V4. This client offers functionality +/// to register platforms, retrieve PCK certificates and CRLs, fetch TCB +/// information, enclave identities, as well as TCB evaluation data numbers. +/// +/// # Examples +/// +/// ```rust,no_run +/// use intel_dcap_api::ApiClient; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// // Create a client with default settings (V4 API) +/// let client = ApiClient::new()?; +/// +/// // Retrieve TCB info for a specific FMSPC +/// let tcb_info = client.get_sgx_tcb_info("00606A000000", None, None).await?; +/// println!("TCB Info: {}", tcb_info.tcb_info_json); +/// +/// Ok(()) +/// } +/// ``` +#[derive(Clone)] +pub struct ApiClient { + client: Client, + base_url: Url, + api_version: ApiVersion, +} + +impl ApiClient { + /// Creates a new client targeting the latest supported API version (V4). + /// + /// # Returns + /// + /// A result containing the newly created `ApiClient` or an `IntelApiError` if there + /// was an issue building the underlying HTTP client. + /// + /// # Errors + /// + /// This function may fail if the provided TLS version or base URL + /// cannot be used to build a `reqwest` client. + pub fn new() -> Result { + Self::new_with_options(BASE_URL, ApiVersion::V4) // Default to V4 + } + + /// Creates a new client targeting a specific API version. + /// + /// # Arguments + /// + /// * `api_version` - The desired API version to use (V3 or V4). + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the `reqwest` client cannot be built + /// with the specified options. + pub fn new_with_version(api_version: ApiVersion) -> Result { + Self::new_with_options(BASE_URL, api_version) + } + + /// Creates a new client with a custom base URL, targeting the latest supported API version (V4). + /// + /// # Arguments + /// + /// * `base_url` - The custom base URL for the Intel Trusted Services API. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the `reqwest` client cannot be built + /// or if the provided base URL is invalid. + pub fn new_with_base_url(base_url: impl IntoUrl) -> Result { + Self::new_with_options(base_url, ApiVersion::V4) // Default to V4 + } + + /// Creates a new client with a custom base URL and specific API version. + /// + /// # Arguments + /// + /// * `base_url` - The custom base URL for the Intel Trusted Services API. + /// * `api_version` - The desired API version (V3 or V4). + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the `reqwest` client cannot be built + /// or if the provided base URL is invalid. + pub fn new_with_options( + base_url: impl IntoUrl, + api_version: ApiVersion, + ) -> Result { + Ok(ApiClient { + client: Client::builder() + .min_tls_version(reqwest::tls::Version::TLS_1_2) + .build()?, + base_url: base_url.into_url()?, + api_version, // Store the version + }) + } + + /// POST /sgx/registration/v1/platform + /// Registers a multi-package SGX platform with the Intel Trusted Services API. + /// + /// # Arguments + /// + /// * `platform_manifest` - Binary data representing the platform manifest. + /// + /// # Returns + /// + /// Request body is binary Platform Manifest + /// Returns the hex-encoded PPID as a `String` upon success. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails or if the response status + /// is not HTTP `201 CREATED`. + pub async fn register_platform( + &self, + platform_manifest: Vec, + ) -> Result { + // Registration paths are fixed, use the helper with "registration" service + let path = self.build_api_path("sgx", "registration", "platform")?; + let url = self.base_url.join(&path)?; + + let response = self + .client + .post(url) + .header(header::CONTENT_TYPE, "application/octet-stream") + .body(platform_manifest) + .send() + .await?; + + let response = check_status(response, &[StatusCode::CREATED]).await?; + + // Response body is hex-encoded PPID + let ppid_hex = response.text().await?; + Ok(ppid_hex) + } + + /// POST /sgx/registration/v1/package + /// Adds new package(s) to an already registered SGX platform instance. + /// + /// # Arguments + /// + /// * `add_package_request` - Binary data for the "Add Package" request body. + /// * `subscription_key` - The subscription key required by the Intel API. + /// + /// # Returns + /// + /// A [`AddPackageResponse`] containing the Platform Membership Certificates and + /// the count of them extracted from the response header. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails, if the subscription key is invalid, + /// or if the response status is not HTTP `200 OK`. + pub async fn add_package( + &self, + add_package_request: Vec, + subscription_key: &str, + ) -> Result { + if subscription_key.is_empty() { + return Err(IntelApiError::InvalidSubscriptionKey); + } + + // Registration paths are fixed + let path = self.build_api_path("sgx", "registration", "package")?; + let url = self.base_url.join(&path)?; + + let response = self + .client + .post(url) + .header("Ocp-Apim-Subscription-Key", subscription_key) + .header(header::CONTENT_TYPE, "application/octet-stream") + .body(add_package_request) + .send() + .await?; + + let response = check_status(response, &[StatusCode::OK]).await?; + + // Use the generic header helper, assuming header name is stable across reg versions + let cert_count_str = self.get_required_header(&response, "Certificate-Count", None)?; + let pck_cert_count: usize = cert_count_str.parse().map_err(|e: ParseIntError| { + IntelApiError::HeaderValueParse("Certificate-Count", e.to_string()) + })?; + + // Response body is a binary array of certificates + let pck_certs = response.bytes().await?.to_vec(); + Ok(AddPackageResponse { + pck_certs, + pck_cert_count, + }) + } + + // === Provisioning Certification Service === + + /// GET /sgx/certification/{v3,v4}/pckcert + /// Retrieves a single SGX PCK certificate using encrypted PPID and SVNs. + /// + /// Optionally requires a subscription key. The `ppid_encryption_key_type` parameter + /// is only valid for API v4 and allows specifying the PPID encryption key type (e.g. "RSA-3072"). + /// + /// # Arguments + /// + /// * `encrypted_ppid` - Hex-encoded encrypted PPID. + /// * `cpusvn` - Hex-encoded CPUSVN value. + /// * `pcesvn` - Hex-encoded PCESVN value. + /// * `pceid` - Hex-encoded PCEID value. + /// * `subscription_key` - Optional subscription key if the Intel API requires it. + /// * `ppid_encryption_key_type` - Optional PPID encryption key type (V4 only). + /// + /// # Returns + /// + /// A [`PckCertificateResponse`] containing the PEM-encoded certificate, issuer chain, + /// TCBm, and FMSPC. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the API call fails or the response contains an invalid status. + /// Returns PEM Cert, Issuer Chain, TCBm, FMSPC. + pub async fn get_pck_certificate_by_ppid( + &self, + encrypted_ppid: &str, + cpusvn: &str, + pcesvn: &str, + pceid: &str, + subscription_key: Option<&str>, + ppid_encryption_key_type: Option<&str>, + ) -> Result { + // Check V4-only parameter + self.check_v4_only_param(ppid_encryption_key_type, "PPID-Encryption-Key")?; + + let path = self.build_api_path("sgx", "", "pckcert")?; // service is empty + let mut url = self.base_url.join(&path)?; + url.query_pairs_mut() + .append_pair("encrypted_ppid", encrypted_ppid) + .append_pair("cpusvn", cpusvn) + .append_pair("pcesvn", pcesvn) + .append_pair("pceid", pceid); + + let mut request_builder = self.client.get(url); + + request_builder = Self::maybe_add_header( + request_builder, + "Ocp-Apim-Subscription-Key", + subscription_key, + ); + + // Only add for V4 + if self.api_version == ApiVersion::V4 { + request_builder = Self::maybe_add_header( + request_builder, + "PPID-Encryption-Key", + ppid_encryption_key_type, + ); + } + + self.fetch_pck_certificate(request_builder).await + } + + /// POST /sgx/certification/{v3,v4}/pckcert + /// Retrieves a single SGX PCK certificate using a platform manifest and SVNs. + /// + /// Optionally requires a subscription key. + /// + /// # Arguments + /// + /// * `platform_manifest` - Hex-encoded platform manifest. + /// * `cpusvn` - Hex-encoded CPUSVN value. + /// * `pcesvn` - Hex-encoded PCESVN value. + /// * `pceid` - Hex-encoded PCEID value. + /// * `subscription_key` - Optional subscription key if the Intel API requires it. + /// + /// # Returns + /// + /// A [`PckCertificateResponse`] containing the PEM-encoded certificate, issuer chain, + /// TCBm, and FMSPC. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails or if the response is invalid. + /// Returns PEM Cert, Issuer Chain, TCBm, FMSPC. + pub async fn get_pck_certificate_by_manifest( + &self, + platform_manifest: &str, + cpusvn: &str, + pcesvn: &str, + pceid: &str, + subscription_key: Option<&str>, + ) -> Result { + let path = self.build_api_path("sgx", "", "pckcert")?; + let url = self.base_url.join(&path)?; + let request_body = PckCertRequest { + platform_manifest, + cpusvn, + pcesvn, + pceid, + }; + + let mut request_builder = self + .client + .post(url) + .header(header::CONTENT_TYPE, "application/json") + .json(&request_body); + + request_builder = Self::maybe_add_header( + request_builder, + "Ocp-Apim-Subscription-Key", + subscription_key, + ); + + self.fetch_pck_certificate(request_builder).await + } + + /// GET /sgx/certification/{v3,v4}/pckcerts + /// Retrieves all SGX PCK certificates for a platform using encrypted PPID. + /// + /// Optionally requires a subscription key. The `ppid_encryption_key_type` parameter + /// is only valid for API v4. + /// + /// # Arguments + /// + /// * `encrypted_ppid` - Hex-encoded encrypted PPID. + /// * `pceid` - Hex-encoded PCEID value. + /// * `subscription_key` - Optional subscription key if the Intel API requires it. + /// * `ppid_encryption_key_type` - Optional PPID encryption key type (V4 only). + /// + /// # Returns + /// + /// A [`PckCertificatesResponse`] containing JSON with `{tcb, tcbm, cert}` entries, + /// as well as the issuer chain and FMSPC headers. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the API call fails or the response status is invalid. + pub async fn get_pck_certificates_by_ppid( + &self, + encrypted_ppid: &str, + pceid: &str, + subscription_key: Option<&str>, + ppid_encryption_key_type: Option<&str>, + ) -> Result { + // Check V4-only parameter + self.check_v4_only_param(ppid_encryption_key_type, "PPID-Encryption-Key")?; + + let path = self.build_api_path("sgx", "", "pckcerts")?; + let mut url = self.base_url.join(&path)?; + url.query_pairs_mut() + .append_pair("encrypted_ppid", encrypted_ppid) + .append_pair("pceid", pceid); + + let mut request_builder = self.client.get(url); + + request_builder = Self::maybe_add_header( + request_builder, + "Ocp-Apim-Subscription-Key", + subscription_key, + ); + + // Only add for V4 + if self.api_version == ApiVersion::V4 { + request_builder = Self::maybe_add_header( + request_builder, + "PPID-Encryption-Key", + ppid_encryption_key_type, + ); + } + + self.fetch_pck_certificates(request_builder).await + } + + /// POST /sgx/certification/{v3,v4}/pckcerts + /// Retrieves all SGX PCK certificates for a platform using a platform manifest. + /// + /// Optionally requires a subscription key. + /// + /// # Arguments + /// + /// * `platform_manifest` - Hex-encoded platform manifest. + /// * `pceid` - Hex-encoded PCEID value. + /// * `subscription_key` - Optional subscription key if the Intel API requires it. + /// + /// # Returns + /// + /// A [`PckCertificatesResponse`] containing JSON with `{tcb, tcbm, cert}` entries, + /// as well as the issuer chain and FMSPC headers. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the API call fails or the response status is invalid. + pub async fn get_pck_certificates_by_manifest( + &self, + platform_manifest: &str, + pceid: &str, + subscription_key: Option<&str>, + ) -> Result { + let path = self.build_api_path("sgx", "", "pckcerts")?; + let url = self.base_url.join(&path)?; + let request_body = PckCertsRequest { + platform_manifest, + pceid, + }; + + let mut request_builder = self + .client + .post(url) + .header(header::CONTENT_TYPE, "application/json") + .json(&request_body); + + request_builder = Self::maybe_add_header( + request_builder, + "Ocp-Apim-Subscription-Key", + subscription_key, + ); + + self.fetch_pck_certificates(request_builder).await + } + + /// GET /sgx/certification/{v3,v4}/pckcerts/config (using PPID) + /// Retrieves SGX PCK certificates for a specific configuration (CPUSVN) using encrypted PPID. + /// + /// Optionally requires a subscription key. The `ppid_encryption_key_type` parameter + /// is only valid for API v4. Returns JSON with `{tcb, tcbm, cert}` entries, + /// as well as the issuer chain and FMSPC headers. + /// + /// # Arguments + /// + /// * `encrypted_ppid` - Hex-encoded encrypted PPID. + /// * `pceid` - Hex-encoded PCEID value. + /// * `cpusvn` - Hex-encoded CPUSVN value for the requested configuration. + /// * `subscription_key` - Optional subscription key if the Intel API requires it. + /// * `ppid_encryption_key_type` - Optional PPID encryption key type (V4 only). + /// + /// # Returns + /// + /// A [`PckCertificatesResponse`] with the requested config's certificate data. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails or if the response status + /// is not `200 OK`. + pub async fn get_pck_certificates_config_by_ppid( + &self, + encrypted_ppid: &str, + pceid: &str, + cpusvn: &str, + subscription_key: Option<&str>, + ppid_encryption_key_type: Option<&str>, + ) -> Result { + // V3 does not support PPID-Encryption-Key header/type + if self.api_version == ApiVersion::V3 && ppid_encryption_key_type.is_some() { + return Err(IntelApiError::UnsupportedApiVersion( + "PPID-Encryption-Key header is only supported in API v4".to_string(), + )); + } + + let path = self.build_api_path("sgx", "", "pckcerts/config")?; + let mut url = self.base_url.join(&path)?; + url.query_pairs_mut() + .append_pair("encrypted_ppid", encrypted_ppid) + .append_pair("pceid", pceid) + .append_pair("cpusvn", cpusvn); + + let mut request_builder = self.client.get(url); + + request_builder = Self::maybe_add_header( + request_builder, + "Ocp-Apim-Subscription-Key", + subscription_key, + ); + + // Only add for V4 + if self.api_version == ApiVersion::V4 { + request_builder = Self::maybe_add_header( + request_builder, + "PPID-Encryption-Key", + ppid_encryption_key_type, + ); + } + + self.fetch_pck_certificates(request_builder).await + } + + /// POST /sgx/certification/{v3,v4}/pckcerts/config (using Manifest) + /// Retrieves SGX PCK certificates for a specific configuration (CPUSVN) using a platform manifest. + /// + /// Optionally requires a subscription key. Returns JSON with `{tcb, tcbm, cert}` entries, + /// as well as the issuer chain and FMSPC headers. + /// + /// # Arguments + /// + /// * `platform_manifest` - Hex-encoded platform manifest. + /// * `pceid` - Hex-encoded PCEID value. + /// * `cpusvn` - Hex-encoded CPUSVN value for the requested configuration. + /// * `subscription_key` - Optional subscription key if needed by the Intel API. + /// + /// # Returns + /// + /// A [`PckCertificatesResponse`] with the requested config's certificate data. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails or if the response status + /// is not `200 OK`. + pub async fn get_pck_certificates_config_by_manifest( + &self, + platform_manifest: &str, + pceid: &str, + cpusvn: &str, + subscription_key: Option<&str>, + ) -> Result { + let path = self.build_api_path("sgx", "", "pckcerts/config")?; + let url = self.base_url.join(&path)?; + let request_body = PckCertsConfigRequest { + platform_manifest, + pceid, + cpusvn, + }; + + let mut request_builder = self + .client + .post(url) + .header(header::CONTENT_TYPE, "application/json") + .json(&request_body); + + request_builder = Self::maybe_add_header( + request_builder, + "Ocp-Apim-Subscription-Key", + subscription_key, + ); + + self.fetch_pck_certificates(request_builder).await + } + + /// GET /sgx/certification/{v3,v4}/pckcrl + /// Retrieves the PCK Certificate Revocation List (CRL) for a specified CA type. + /// + /// Optionally takes an `encoding` parameter indicating whether the CRL should be + /// returned as PEM or DER. Defaults to PEM if not specified. + /// + /// # Arguments + /// + /// * `ca_type` - The type of CA to retrieve the CRL for (e.g., "processor" or "platform"). + /// * `encoding` - An optional [`CrlEncoding`] (PEM or DER). + /// + /// # Returns + /// + /// A [`PckCrlResponse`] containing the CRL data and the issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails or if the response status + /// is not `200 OK`. + /// Optional 'encoding' parameter ("pem" or "der"). + /// Returns CRL data (PEM or DER) and Issuer Chain header. + pub async fn get_pck_crl( + &self, + ca_type: CaType, + encoding: Option, + ) -> Result { + let path = self.build_api_path("sgx", "", "pckcrl")?; + let mut url = self.base_url.join(&path)?; + url.query_pairs_mut() + .append_pair("ca", &ca_type.to_string()); + + if let Some(enc) = encoding { + url.query_pairs_mut() + .append_pair("encoding", &enc.to_string()); + } + + let request_builder = self.client.get(url); + let response = request_builder.send().await?; + let response = check_status(response, &[StatusCode::OK]).await?; + + let issuer_chain = self.get_required_header( + &response, + "SGX-PCK-CRL-Issuer-Chain", // v4 name + Some("SGX-PCK-CRL-Issuer-Chain"), // v3 name + )?; + + // Response body is PEM or DER CRL + let crl_data = response.bytes().await?.to_vec(); + + Ok(PckCrlResponse { + crl_data, + issuer_chain, + }) + } + + // --- TCB Info --- + + /// GET /sgx/certification/{v3,v4}/tcb + /// Retrieves SGX TCB information for a given FMSPC. + /// + /// Returns TCB Info JSON string (Appendix A) and Issuer Chain header. + /// This function supports both API v3 and v4. The `update` and `tcbEvaluationDataNumber` + /// parameters are only supported by API v4. If both are provided at the same time (for v4), + /// a conflict error is returned. + /// + /// # Arguments + /// + /// * `fmspc` - Hex-encoded FMSPC value. + /// * `update` - Optional [`UpdateType`] for API v4. + /// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only). + /// + /// # Returns + /// + /// A [`TcbInfoResponse`] containing the TCB info JSON and the issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the API request fails, if conflicting parameters are used, + /// or if the requested TCB data is not found. + pub async fn get_sgx_tcb_info( + &self, + fmspc: &str, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result { + // V3 does not support 'update' or 'tcbEvaluationDataNumber' + if self.api_version == ApiVersion::V3 && update.is_some() { + return Err(IntelApiError::UnsupportedApiVersion( + "'update' parameter requires API v4".to_string(), + )); + } + if self.api_version == ApiVersion::V3 && tcb_evaluation_data_number.is_some() { + return Err(IntelApiError::UnsupportedApiVersion( + "'tcbEvaluationDataNumber' parameter requires API v4".to_string(), + )); + } + if self.api_version == ApiVersion::V4 + && update.is_some() + && tcb_evaluation_data_number.is_some() + { + return Err(IntelApiError::ConflictingParameters( + "'update' and 'tcbEvaluationDataNumber'", + )); + } + + let path = self.build_api_path("sgx", "", "tcb")?; + let mut url = self.base_url.join(&path)?; + url.query_pairs_mut().append_pair("fmspc", fmspc); + + // Add V4-specific parameters + if self.api_version == ApiVersion::V4 { + if let Some(upd) = update { + url.query_pairs_mut() + .append_pair("update", &upd.to_string()); + } + if let Some(tedn) = tcb_evaluation_data_number { + url.query_pairs_mut() + .append_pair("tcbEvaluationDataNumber", &tedn.to_string()); + } + } + + let request_builder = self.client.get(url); + + // Special handling for 404/410 when tcbEvaluationDataNumber is specified (V4 only) + if self.api_version == ApiVersion::V4 { + if let Some(tedn_val) = tcb_evaluation_data_number { + // Use the helper function to check status before proceeding + self.check_tcb_evaluation_status(&request_builder, tedn_val, "SGX TCB Info") + .await?; + // If the check passes (doesn't return Err), continue to fetch_json_with_issuer_chain + } + } + + // Fetch JSON and header (header name seems same for v3/v4) + let (tcb_info_json, issuer_chain) = self + .fetch_json_with_issuer_chain( + request_builder, + "TCB-Info-Issuer-Chain", // v4 name + Some("SGX-TCB-Info-Issuer-Chain"), // v3 name + ) + .await?; + + Ok(TcbInfoResponse { + tcb_info_json, + issuer_chain, + }) + } + + /// GET /tdx/certification/v4/tcb + /// Retrieves TDX TCB information for a given FMSPC (API v4 only). + /// + /// # Arguments + /// + /// * `fmspc` - Hex-encoded FMSPC value. + /// * `update` - An optional [`UpdateType`] (v4 only). + /// * `tcb_evaluation_data_number` - An optional TCB Evaluation Data Number (v4 only). + /// + /// # Returns + /// + /// A [`TcbInfoResponse`] containing TDX TCB info JSON and the issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if an unsupported API version is used, + /// if there are conflicting parameters, or if the TDX TCB data is not found. + /// Returns TCB Info JSON string (Appendix A) and Issuer Chain header. + pub async fn get_tdx_tcb_info( + &self, + fmspc: &str, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result { + // Ensure V4 API + self.ensure_v4_api("get_tdx_tcb_info")?; + // Check conflicting parameters (only relevant for V4, checked inside helper) + self.check_conflicting_update_params(update, tcb_evaluation_data_number)?; + + let path = self.build_api_path("tdx", "", "tcb")?; + let mut url = self.base_url.join(&path)?; + url.query_pairs_mut().append_pair("fmspc", fmspc); + + if let Some(upd) = update { + url.query_pairs_mut() + .append_pair("update", &upd.to_string()); + } + if let Some(tedn) = tcb_evaluation_data_number { + url.query_pairs_mut() + .append_pair("tcbEvaluationDataNumber", &tedn.to_string()); + } + + let request_builder = self.client.get(url); + + // Special handling for 404/410 when tcbEvaluationDataNumber is specified + if let Some(tedn_val) = tcb_evaluation_data_number { + // Use the helper function to check status before proceeding + self.check_tcb_evaluation_status(&request_builder, tedn_val, "TDX TCB Info") + .await?; + // If the check passes (doesn't return Err), continue to fetch_json_with_issuer_chain + } + + // Fetch JSON and header (TDX only exists in V4) + let (tcb_info_json, issuer_chain) = self + .fetch_json_with_issuer_chain(request_builder, "TCB-Info-Issuer-Chain", None) + .await?; + + Ok(TcbInfoResponse { + tcb_info_json, + issuer_chain, + }) + } + + // --- Enclave Identity --- + + /// Retrieves the SGX QE Identity from the Intel API. + /// + /// Returns Enclave Identity JSON string (Appendix B) and Issuer Chain header. + /// Supports both v3 and v4. The `update` and `tcb_evaluation_data_number` + /// parameters are only valid in API v4. Returns the enclave identity JSON + /// and an issuer chain header. + /// + /// # Arguments + /// + /// * `update` - Optional [`UpdateType`] (v4 only). + /// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only). + /// + /// # Returns + /// + /// An [`EnclaveIdentityResponse`] containing the JSON identity and issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails, if conflicting v4 parameters are used, + /// or if the desired identity resource is not found. + pub async fn get_sgx_qe_identity( + &self, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result { + self.get_sgx_enclave_identity("qe", update, tcb_evaluation_data_number) + .await + } + + /// Retrieves the TDX QE Identity from the Intel API (API v4 only). + /// + /// # Arguments + /// + /// * `update` - Optional [`UpdateType`] (v4 only). + /// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only). + /// + /// # Returns + /// + /// An [`EnclaveIdentityResponse`] containing the JSON identity and issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if an unsupported API version is used, + /// if conflicting parameters are provided, or if the identity resource is not found. + /// GET /tdx/certification/v4/qe/identity - V4 ONLY + pub async fn get_tdx_qe_identity( + &self, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result { + // Ensure V4 API + self.ensure_v4_api("get_tdx_qe_identity")?; + // Check conflicting parameters (only relevant for V4, checked inside helper) + self.check_conflicting_update_params(update, tcb_evaluation_data_number)?; + + let path = self.build_api_path("tdx", "qe", "identity")?; + let mut url = self.base_url.join(&path)?; + + if let Some(upd) = update { + url.query_pairs_mut() + .append_pair("update", &upd.to_string()); + } + if let Some(tedn) = tcb_evaluation_data_number { + url.query_pairs_mut() + .append_pair("tcbEvaluationDataNumber", &tedn.to_string()); + } + + let request_builder = self.client.get(url); + + // Special handling for 404/410 when tcbEvaluationDataNumber is specified + if let Some(tedn_val) = tcb_evaluation_data_number { + // Use the helper function to check status before proceeding + self.check_tcb_evaluation_status(&request_builder, tedn_val, "TDX QE Identity") + .await?; + // If the check passes (doesn't return Err), continue to fetch_json_with_issuer_chain + } + + // Fetch JSON and header (TDX only exists in V4) + let (enclave_identity_json, issuer_chain) = self + .fetch_json_with_issuer_chain( + request_builder, + "SGX-Enclave-Identity-Issuer-Chain", + None, + ) + .await?; + + Ok(EnclaveIdentityResponse { + enclave_identity_json, + issuer_chain, + }) + } + + /// Retrieves the SGX QVE Identity from the Intel API. + /// + /// Supports API v3 and v4. The `update` and `tcb_evaluation_data_number` parameters + /// are v4 only. Returns the QVE identity JSON and issuer chain. + /// + /// # Arguments + /// + /// * `update` - Optional [`UpdateType`] (v4 only). + /// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only). + /// + /// # Returns + /// + /// An [`EnclaveIdentityResponse`] containing the QVE identity JSON and issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails, if conflicting parameters are used, + /// or if the identity resource is not found. + /// GET /sgx/certification/{v3,v4}/qve/identity + pub async fn get_sgx_qve_identity( + &self, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result { + self.get_sgx_enclave_identity("qve", update, tcb_evaluation_data_number) + .await + } + + /// Retrieves the SGX QAE Identity from the Intel API (API v4 only). + /// + /// # Arguments + /// + /// * `update` - Optional [`UpdateType`] (v4 only). + /// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number (v4 only). + /// + /// # Returns + /// + /// An [`EnclaveIdentityResponse`] containing the QAE identity JSON and issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if an unsupported API version is used, + /// if conflicting parameters are provided, or if the QAE identity is not found. + /// GET /sgx/certification/v4/qae/identity - V4 ONLY + pub async fn get_sgx_qae_identity( + &self, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result { + // QAE endpoint requires V4 + if self.api_version != ApiVersion::V4 { + return Err(IntelApiError::UnsupportedApiVersion( + "QAE Identity endpoint requires API v4".to_string(), + )); + } + // Call the generic helper, it will handle V4 params and 404/410 checks + self.get_sgx_enclave_identity("qae", update, tcb_evaluation_data_number) + .await + } + + // --- FMSPCs & TCB Evaluation Data Numbers --- + + /// GET /sgx/certification/{v3,v4}/fmspcs + /// Retrieves a list of FMSPC values for SGX and TDX platforms (API v4 only). + /// + /// # Arguments + /// + /// * `platform_filter` - An optional filter specifying SGX or TDX platforms. + /// + /// # Returns + /// + /// Optional 'platform' filter. + /// A `String` containing the JSON array of objects, each containing `fmspc` and `platform`. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if an unsupported API version is used or if the request fails. + pub async fn get_fmspcs( + &self, + platform_filter: Option, + ) -> Result { + if self.api_version == ApiVersion::V3 { + return Err(IntelApiError::UnsupportedApiVersion( + "API v4 only function".to_string(), + )); + } + let path = self.build_api_path("sgx", "", "fmspcs")?; + let mut url = self.base_url.join(&path)?; + + if let Some(pf) = platform_filter { + url.query_pairs_mut() + .append_pair("platform", &pf.to_string()); + } + + let request_builder = self.client.get(url); + let response = request_builder.send().await?; + let response = check_status(response, &[StatusCode::OK]).await?; + + let fmspcs_json = response.text().await?; + + Ok(fmspcs_json) + } + + /// GET /sgx/certification/v4/tcbevaluationdatanumbers - V4 ONLY + /// Retrieves the currently supported SGX TCB Evaluation Data Numbers (API v4 only). + /// + /// # Returns + /// + /// A [`TcbEvaluationDataNumbersResponse`] containing the JSON structure of TCB Evaluation + /// Data Numbers and an issuer chain header. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if an unsupported API version is used or if the request fails. + pub async fn get_sgx_tcb_evaluation_data_numbers( + &self, + ) -> Result { + // Endpoint requires V4 + if self.api_version != ApiVersion::V4 { + return Err(IntelApiError::UnsupportedApiVersion( + "SGX TCB Evaluation Data Numbers endpoint requires API v4".to_string(), + )); + } + + let path = self.build_api_path("sgx", "", "tcbevaluationdatanumbers")?; + let url = self.base_url.join(&path)?; + let request_builder = self.client.get(url); + + let (tcb_evaluation_data_numbers_json, issuer_chain) = self + .fetch_json_with_issuer_chain( + request_builder, + "TCB-Evaluation-Data-Numbers-Issuer-Chain", + None, + ) + .await?; + + Ok(TcbEvaluationDataNumbersResponse { + tcb_evaluation_data_numbers_json, + issuer_chain, + }) + } + + /// GET /tdx/certification/v4/tcbevaluationdatanumbers - V4 ONLY + /// Retrieves the currently supported TDX TCB Evaluation Data Numbers (API v4 only). + /// + /// # Returns + /// + /// A [`TcbEvaluationDataNumbersResponse`] containing the JSON structure of TCB Evaluation + /// Data Numbers and an issuer chain header. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if an unsupported API version is used or if the request fails. + pub async fn get_tdx_tcb_evaluation_data_numbers( + &self, + ) -> Result { + // Endpoint requires V4 + if self.api_version != ApiVersion::V4 { + return Err(IntelApiError::UnsupportedApiVersion( + "TDX TCB Evaluation Data Numbers endpoint requires API v4".to_string(), + )); + } + + let path = self.build_api_path("tdx", "", "tcbevaluationdatanumbers")?; + let url = self.base_url.join(&path)?; + let request_builder = self.client.get(url); + + let (tcb_evaluation_data_numbers_json, issuer_chain) = self + .fetch_json_with_issuer_chain( + request_builder, + "TCB-Evaluation-Data-Numbers-Issuer-Chain", + None, + ) + .await?; + + Ok(TcbEvaluationDataNumbersResponse { + tcb_evaluation_data_numbers_json, + issuer_chain, + }) + } + + // ------------------------ + // Internal helper methods + // ------------------------ + + /// Helper to construct API paths dynamically based on version and technology (SGX/TDX). + fn build_api_path( + &self, + technology: &str, + service: &str, + endpoint: &str, + ) -> Result { + let api_segment = self.api_version.path_segment(); + + if technology == "tdx" && self.api_version == ApiVersion::V3 { + return Err(IntelApiError::UnsupportedApiVersion(format!( + "TDX endpoint /{}/{}/{} requires API v4", + service, endpoint, technology + ))); + } + if technology == "sgx" && service == "registration" { + return Ok(format!("/sgx/registration/v1/{}", endpoint).replace("//", "/")); + } + + Ok(format!( + "/{}/certification/{}/{}/{}", + technology, api_segment, service, endpoint + ) + .replace("//", "/")) + } + + /// Helper to add an optional header if the string is non-empty. + fn maybe_add_header( + builder: RequestBuilder, + header_name: &'static str, + header_value: Option<&str>, + ) -> RequestBuilder { + match header_value { + Some(value) if !value.is_empty() => builder.header(header_name, value), + _ => builder, + } + } + + /// Helper to extract a required header string value, handling potential v3/v4 differences. + fn get_required_header( + &self, + response: &Response, + v4_header_name: &'static str, + v3_header_name: Option<&'static str>, + ) -> Result { + let header_name = match self.api_version { + ApiVersion::V4 => v4_header_name, + ApiVersion::V3 => v3_header_name.unwrap_or(v4_header_name), + }; + let value = response + .headers() + .get(header_name) + .ok_or(IntelApiError::MissingOrInvalidHeader(header_name))? + .to_str() + .map_err(|e| IntelApiError::HeaderValueParse(header_name, e.to_string()))?; + + if value.contains('%') { + percent_decode_str(value) + .decode_utf8() + .map_err(|e| IntelApiError::HeaderValueParse(header_name, e.to_string())) + .map(|s| s.to_string()) + } else { + Ok(value.to_string()) + } + } + + /// Helper to execute a request that returns a single PCK certificate and associated headers. + async fn fetch_pck_certificate( + &self, + request_builder: RequestBuilder, + ) -> Result { + let response = request_builder.send().await?; + let response = check_status(response, &[StatusCode::OK]).await?; + + let issuer_chain = self.get_required_header( + &response, + "SGX-PCK-Certificate-Issuer-Chain", + Some("SGX-PCK-Certificate-Issuer-Chain"), + )?; + let tcbm = self.get_required_header(&response, "SGX-TCBm", Some("SGX-TCBm"))?; + let fmspc = self.get_required_header(&response, "SGX-FMSPC", Some("SGX-FMSPC"))?; + let pck_cert_pem = response.text().await?; + + Ok(PckCertificateResponse { + pck_cert_pem, + issuer_chain, + tcbm, + fmspc, + }) + } + + /// Helper to execute a request that returns a PCK certificates JSON array and associated headers. + async fn fetch_pck_certificates( + &self, + request_builder: RequestBuilder, + ) -> Result { + let response = request_builder.send().await?; + let response = check_status(response, &[StatusCode::OK]).await?; + + let issuer_chain = self.get_required_header( + &response, + "SGX-PCK-Certificate-Issuer-Chain", + Some("SGX-PCK-Certificate-Issuer-Chain"), + )?; + let fmspc = self.get_required_header(&response, "SGX-FMSPC", Some("SGX-FMSPC"))?; + let pck_certs_json = response.text().await?; + + Ok(PckCertificatesResponse { + pck_certs_json, + issuer_chain, + fmspc, + }) + } + + /// Helper to execute a request expected to return JSON plus an Issuer-Chain header. + async fn fetch_json_with_issuer_chain( + &self, + request_builder: RequestBuilder, + v4_issuer_chain_header: &'static str, + v3_issuer_chain_header: Option<&'static str>, + ) -> Result<(String, String), IntelApiError> { + let response = request_builder.send().await?; + let response = check_status(response, &[StatusCode::OK]).await?; + + let issuer_chain = + self.get_required_header(&response, v4_issuer_chain_header, v3_issuer_chain_header)?; + let json_body = response.text().await?; + + Ok((json_body, issuer_chain)) + } + + /// Checks for HTTP 404 or 410 status when querying TCB Evaluation Data Number based resources. + async fn check_tcb_evaluation_status( + &self, + request_builder: &RequestBuilder, + tcb_evaluation_data_number_val: u64, + resource_description: &str, + ) -> Result<(), IntelApiError> { + let builder_clone = request_builder.try_clone().ok_or_else(|| { + IntelApiError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to clone request builder for status check", + )) + })?; + + let response = builder_clone.send().await?; + let status = response.status(); + + if status == StatusCode::NOT_FOUND || status == StatusCode::GONE { + let (request_id, _, _) = extract_api_error_details(&response); + return Err(IntelApiError::ApiError { + status, + request_id, + error_code: None, + error_message: Some(format!( + "{} for TCB Evaluation Data Number {} {}", + resource_description, + tcb_evaluation_data_number_val, + if status == StatusCode::NOT_FOUND { + "not found" + } else { + "is no longer available" + } + )), + }); + } + Ok(()) + } + + /// Ensures the client is configured for API v4, otherwise returns an error. + fn ensure_v4_api(&self, function_name: &str) -> Result<(), IntelApiError> { + if self.api_version != ApiVersion::V4 { + Err(IntelApiError::UnsupportedApiVersion(format!( + "{} requires API v4", + function_name + ))) + } else { + Ok(()) + } + } + + /// Checks if a V4-only parameter is provided with a V3 API version. + fn check_v4_only_param( + &self, + param_value: Option, + param_name: &str, + ) -> Result<(), IntelApiError> { + if self.api_version == ApiVersion::V3 && param_value.is_some() { + Err(IntelApiError::UnsupportedApiVersion(format!( + "'{}' parameter requires API v4", + param_name + ))) + } else { + Ok(()) + } + } + + /// Checks for conflicting `update` and `tcb_evaluation_data_number` parameters when using V4. + fn check_conflicting_update_params( + &self, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result<(), IntelApiError> { + if self.api_version == ApiVersion::V4 + && update.is_some() + && tcb_evaluation_data_number.is_some() + { + Err(IntelApiError::ConflictingParameters( + "'update' and 'tcbEvaluationDataNumber'", + )) + } else { + Ok(()) + } + } + + /// Retrieves generic SGX enclave identity (QE, QVE, QAE) data. + /// + /// # Arguments + /// + /// * `identity_path_segment` - String slice representing the identity path segment (e.g., "qe", "qve", "qae"). + /// * `update` - Optional [`UpdateType`] for API v4. + /// * `tcb_evaluation_data_number` - Optional TCB Evaluation Data Number for API v4. + /// + /// # Returns + /// + /// An [`EnclaveIdentityResponse`] containing the JSON identity data and issuer chain. + /// + /// # Errors + /// + /// Returns an `IntelApiError` if the request fails or the specified resource + /// is unavailable. + async fn get_sgx_enclave_identity( + &self, + identity_path_segment: &str, + update: Option, + tcb_evaluation_data_number: Option, + ) -> Result { + self.check_v4_only_param(update, "update")?; + self.check_v4_only_param(tcb_evaluation_data_number, "tcbEvaluationDataNumber")?; + self.check_conflicting_update_params(update, tcb_evaluation_data_number)?; + + let path = self.build_api_path("sgx", identity_path_segment, "identity")?; + let mut url = self.base_url.join(&path)?; + + if self.api_version == ApiVersion::V4 { + if let Some(upd) = update { + url.query_pairs_mut() + .append_pair("update", &upd.to_string()); + } + if let Some(tedn) = tcb_evaluation_data_number { + url.query_pairs_mut() + .append_pair("tcbEvaluationDataNumber", &tedn.to_string()); + } + } + + let request_builder = self.client.get(url); + + if self.api_version == ApiVersion::V4 { + if let Some(tedn_val) = tcb_evaluation_data_number { + let description = format!("SGX {} Identity", identity_path_segment.to_uppercase()); + self.check_tcb_evaluation_status(&request_builder, tedn_val, &description) + .await?; + } + } + + let (enclave_identity_json, issuer_chain) = self + .fetch_json_with_issuer_chain( + request_builder, + "SGX-Enclave-Identity-Issuer-Chain", + Some("SGX-Enclave-Identity-Issuer-Chain"), + ) + .await?; + + Ok(EnclaveIdentityResponse { + enclave_identity_json, + issuer_chain, + }) + } +} diff --git a/crates/intel-dcap-api/src/error.rs b/crates/intel-dcap-api/src/error.rs new file mode 100644 index 0000000..564bbd2 --- /dev/null +++ b/crates/intel-dcap-api/src/error.rs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +use reqwest::{Response, StatusCode}; +use thiserror::Error; + +/// Represents all possible errors that can occur when interacting with Intel's DCAP API. +#[derive(Error, Debug)] +pub enum IntelApiError { + /// Indicates that the requested API version or feature is unsupported. + #[error("Unsupported API version or feature: {0}")] + UnsupportedApiVersion(String), + + /// Wraps an underlying reqwest error. + #[error("Reqwest error: {0}")] + Reqwest(#[from] reqwest::Error), + + /// Wraps a URL parsing error. + #[error("URL parsing error: {0}")] + UrlParse(#[from] url::ParseError), + + /// Wraps a Serde JSON error. + #[error("Serde JSON error: {0}")] + JsonError(#[from] serde_json::Error), + + /// Represents a general API error, capturing the HTTP status and optional error details. + #[error("API Error: Status={status}, Request-ID={request_id}, Code={error_code:?}, Message={error_message:?}")] + ApiError { + /// HTTP status code returned by the API. + status: StatusCode, + /// The unique request identifier for tracing errors. + request_id: String, + /// An optional server-provided error code. + error_code: Option, + /// An optional server-provided error message. + error_message: Option, + }, + + /// Indicates that a header is missing or invalid. + #[error("Header missing or invalid: {0}")] + MissingOrInvalidHeader(&'static str), + + /// Represents an invalid subscription key. + #[error("Invalid Subscription Key format")] + InvalidSubscriptionKey, + + /// Indicates that conflicting parameters were supplied. + #[error("Cannot provide conflicting parameters: {0}")] + ConflictingParameters(&'static str), + + /// Wraps a standard I/O error. + #[error("I/O Error: {0}")] + Io(#[from] std::io::Error), + + /// Represents an error while parsing a header's value. + #[error("Header value parse error for '{0}': {1}")] + HeaderValueParse(&'static str, String), + + /// Indicates an invalid parameter was provided. + #[error("Invalid parameter value: {0}")] + InvalidParameter(&'static str), +} + +/// Extracts common API error details from response headers. +pub(crate) fn extract_api_error_details( + response: &Response, +) -> (String, Option, Option) { + let request_id = response + .headers() + .get("Request-ID") + .and_then(|v| v.to_str().ok()) + .unwrap_or("Unknown") + .to_string(); + let error_code = response + .headers() + .get("Error-Code") + .and_then(|v| v.to_str().ok()) + .map(String::from); + let error_message = response + .headers() + .get("Error-Message") + .and_then(|v| v.to_str().ok()) + .map(String::from); + (request_id, error_code, error_message) +} + +/// Checks the response status and returns an ApiError if it's not one of the expected statuses. +pub(crate) async fn check_status( + response: Response, + expected_statuses: &[StatusCode], +) -> Result { + let status = response.status(); + if expected_statuses.contains(&status) { + Ok(response) + } else { + let (request_id, error_code, error_message) = extract_api_error_details(&response); + Err(IntelApiError::ApiError { + status, + request_id, + error_code, + error_message, + }) + } +} diff --git a/crates/intel-dcap-api/src/lib.rs b/crates/intel-dcap-api/src/lib.rs new file mode 100644 index 0000000..a0d9e20 --- /dev/null +++ b/crates/intel-dcap-api/src/lib.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +//! Intel API Client +//! +//! This module provides an API client for interacting with the Intel API for Trusted Services. +//! The API follows the documentation found at [Intel API Documentation](https://api.portal.trustedservices.intel.com/content/documentation.html). +//! +//! Create an [`ApiClient`] to interface with the Intel API. +//! +//! Example +//! ```rust,no_run +//! use intel_dcap_api::{ApiClient, IntelApiError, TcbInfoResponse}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), IntelApiError> { +//! let client = ApiClient::new()?; +//! +//! // Example: Get SGX TCB Info +//! let fmspc_example = "00606A000000"; // Example FMSPC from docs +//! match client.get_sgx_tcb_info(fmspc_example, None, None).await { +//! Ok(TcbInfoResponse { +//! tcb_info_json, +//! issuer_chain, +//! }) => println!( +//! "SGX TCB Info for {}:\n{}\nIssuer Chain: {}", +//! fmspc_example, tcb_info_json, issuer_chain +//! ), +//! Err(e) => eprintln!("Error getting SGX TCB info: {}", e), +//! } +//! +//! Ok(()) +//! } +//! ``` + +#![deny(missing_docs)] +#![deny(clippy::all)] + +mod client; +mod error; +mod requests; +mod responses; +mod types; + +// Re-export public items +pub use client::ApiClient; +pub use error::IntelApiError; +pub use responses::{ + AddPackageResponse, EnclaveIdentityJson, EnclaveIdentityResponse, FmspcJsonResponse, + PckCertificateResponse, PckCertificatesResponse, PckCrlResponse, TcbEvaluationDataNumbersJson, + TcbEvaluationDataNumbersResponse, TcbInfoJson, TcbInfoResponse, +}; +pub use types::{ApiVersion, CaType, CrlEncoding, PlatformFilter, UpdateType}; diff --git a/crates/intel-dcap-api/src/requests.rs b/crates/intel-dcap-api/src/requests.rs new file mode 100644 index 0000000..3be562c --- /dev/null +++ b/crates/intel-dcap-api/src/requests.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +use serde::Serialize; + +#[derive(Serialize)] +pub(crate) struct PckCertRequest<'a> { + #[serde(rename = "platformManifest")] + pub(crate) platform_manifest: &'a str, + pub(crate) cpusvn: &'a str, + pub(crate) pcesvn: &'a str, + pub(crate) pceid: &'a str, +} + +#[derive(Serialize)] +pub(crate) struct PckCertsRequest<'a> { + #[serde(rename = "platformManifest")] + pub(crate) platform_manifest: &'a str, + pub(crate) pceid: &'a str, +} + +#[derive(Serialize)] +pub(crate) struct PckCertsConfigRequest<'a> { + #[serde(rename = "platformManifest")] + pub(crate) platform_manifest: &'a str, + pub(crate) cpusvn: &'a str, + pub(crate) pceid: &'a str, +} diff --git a/crates/intel-dcap-api/src/responses.rs b/crates/intel-dcap-api/src/responses.rs new file mode 100644 index 0000000..29e0a37 --- /dev/null +++ b/crates/intel-dcap-api/src/responses.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +/// JSON structure as defined in Appendix A of the API spec. +/// Content may vary slightly between API v3 and v4. +pub type TcbInfoJson = String; + +/// JSON structure as defined in Appendix B of the API spec. +/// Content may vary slightly between API v3 and v4. +pub type EnclaveIdentityJson = String; + +/// JSON Array of {tcb, tcbm, cert}. +/// Content structure expected to be consistent between v3 and v4. +pub type PckCertsJsonResponse = String; + +/// JSON Array of {fmspc, platform}. +/// Content structure expected to be consistent between v3 and v4. +pub type FmspcJsonResponse = String; + +/// JSON structure as defined in Appendix C of the API spec (V4 ONLY). +pub type TcbEvaluationDataNumbersJson = String; + +/// Response structure for a PCK (Platform Configuration Key) Certificate. +/// +/// Contains the PCK certificate, its issuer chain, TCB measurement, and FMSPC value. +#[derive(Debug, Clone)] +pub struct PckCertificateResponse { + /// PEM-encoded PCK certificate. + pub pck_cert_pem: String, + /// PEM-encoded certificate chain for the PCK certificate issuer. + /// Header name differs between v3 ("PCS-Certificate-Issuer-Chain") and v4 ("SGX-PCK-Certificate-Issuer-Chain"). + pub issuer_chain: String, + /// TCBm value associated with the certificate (Hex-encoded). + pub tcbm: String, + /// FMSPC value associated with the certificate (Hex-encoded). + pub fmspc: String, +} + +/// Response structure for multiple PCK (Platform Configuration Key) Certificates. +/// +/// Contains a JSON array of PCK certificates, their issuer chain, and the associated FMSPC value. +/// This struct represents the response for retrieving multiple PCK certificates from the Intel SGX API. +#[derive(Debug, Clone)] +pub struct PckCertificatesResponse { + /// JSON array containing PCK certificates and their associated TCB levels. + pub pck_certs_json: PckCertsJsonResponse, // String alias for now + /// PEM-encoded certificate chain for the PCK certificate issuer. + /// Header name differs between v3 ("PCS-Certificate-Issuer-Chain") and v4 ("SGX-PCK-Certificate-Issuer-Chain"). + pub issuer_chain: String, + /// FMSPC value associated with the certificates (Hex-encoded). + pub fmspc: String, +} + +/// Response structure for TCB (Trusted Computing Base) Information. +/// +/// Contains the JSON representation of TCB information for a specific platform, +/// along with the certificate chain of the TCB Info signer. +#[derive(Debug, Clone)] +pub struct TcbInfoResponse { + /// JSON containing TCB information for a specific platform (FMSPC). + pub tcb_info_json: TcbInfoJson, // String alias for now + /// PEM-encoded certificate chain for the TCB Info signer. + /// Header name differs slightly between v3 ("SGX-TCB-Info-Issuer-Chain") and v4 ("TCB-Info-Issuer-Chain" - check spec). + pub issuer_chain: String, +} + +/// Response structure for Enclave Identity Information. +/// +/// Contains the JSON representation of enclave identity details for QE, QvE, or QAE, +/// along with its issuer chain. +#[derive(Debug, Clone)] +pub struct EnclaveIdentityResponse { + /// JSON containing information about the QE, QvE, or QAE. + pub enclave_identity_json: EnclaveIdentityJson, // String alias for now + /// PEM-encoded certificate chain for the Enclave Identity signer. + /// Header name seems consistent ("SGX-Enclave-Identity-Issuer-Chain"). + pub issuer_chain: String, +} + +/// Response structure for TCB Evaluation Data Numbers (V4 ONLY). +/// +/// Contains the JSON representation of supported TCB Evaluation Data Numbers +/// and its corresponding issuer chain. +#[derive(Debug, Clone)] +pub struct TcbEvaluationDataNumbersResponse { + /// JSON containing the list of supported TCB Evaluation Data Numbers (V4 ONLY). + pub tcb_evaluation_data_numbers_json: TcbEvaluationDataNumbersJson, // String alias for now + /// PEM-encoded certificate chain for the TCB Evaluation Data Numbers signer (V4 ONLY). + /// Header: "TCB-Evaluation-Data-Numbers-Issuer-Chain". + pub issuer_chain: String, +} + +/// Response structure for Platform Configuration Key Certificate Revocation List (PCK CRL). +/// +/// Contains the CRL data and its issuer chain for validating platform configuration keys. +#[derive(Debug, Clone)] +pub struct PckCrlResponse { + /// CRL data (PEM or DER encoded). + pub crl_data: Vec, + /// PEM-encoded certificate chain for the CRL issuer. + /// Header name differs between v3 ("PCS-CRL-Issuer-Chain") and v4 ("SGX-PCK-CRL-Issuer-Chain"). + pub issuer_chain: String, +} + +/// Response structure for the request to add a package. +pub struct AddPackageResponse { + /// Platform Membership Certificates + pub pck_certs: Vec, + /// The certificate count extracted from the response header. + pub pck_cert_count: usize, +} diff --git a/crates/intel-dcap-api/src/types.rs b/crates/intel-dcap-api/src/types.rs new file mode 100644 index 0000000..07b73f1 --- /dev/null +++ b/crates/intel-dcap-api/src/types.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +use std::fmt; + +/// Represents the type of Certificate Authority (CA) for Intel Trusted Services. +/// +/// This enum defines the different types of Certificate Authorities used in the Intel DCAP API, +/// specifically distinguishing between processor and platform CAs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CaType { + /// Represents a processor-specific Certificate Authority. + Processor, + /// Represents a platform-wide Certificate Authority. + Platform, +} + +impl fmt::Display for CaType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CaType::Processor => write!(f, "processor"), + CaType::Platform => write!(f, "platform"), + } + } +} + +/// Represents the encoding format for Certificate Revocation Lists (CRLs). +/// +/// This enum defines the supported encoding formats for CRLs in the Intel DCAP API, +/// distinguishing between PEM (Privacy Enhanced Mail) and DER (Distinguished Encoding Rules) formats. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CrlEncoding { + /// Represents the PEM (Privacy Enhanced Mail) encoding format. + Pem, + /// Represents the DER (Distinguished Encoding Rules) encoding format. + Der, +} + +impl fmt::Display for CrlEncoding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CrlEncoding::Pem => write!(f, "pem"), + CrlEncoding::Der => write!(f, "der"), + } + } +} + +/// Represents the type of update for Intel Trusted Services. +/// +/// This enum defines different update types, distinguishing between early and standard updates +/// in the Intel DCAP (Data Center Attestation Primitives) API. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpdateType { + /// Represents early updates, typically used for preview or beta releases. + Early, + /// Represents standard updates, which are the regular release cycle. + Standard, +} + +impl fmt::Display for UpdateType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UpdateType::Early => write!(f, "early"), + UpdateType::Standard => write!(f, "standard"), + } + } +} + +/// Represents the platform filter options for Intel DCAP (Data Center Attestation Primitives) API. +/// +/// This enum allows filtering platforms based on different criteria, +/// such as selecting all platforms, client-specific platforms, or specific Intel processor generations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlatformFilter { + /// Represents a selection of all available platforms. + All, + /// Represents a selection of client-specific platforms. + Client, + /// Represents platforms with Intel E3 processors. + E3, + /// Represents platforms with Intel E5 processors. + E5, +} + +impl fmt::Display for PlatformFilter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PlatformFilter::All => write!(f, "all"), + PlatformFilter::Client => write!(f, "client"), + PlatformFilter::E3 => write!(f, "E3"), + PlatformFilter::E5 => write!(f, "E5"), + } + } +} + +/// Represents the version of the Intel Trusted Services API to target. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApiVersion { + /// Represents version 3 of the Intel Trusted Services API. + V3, + /// Represents version 4 of the Intel Trusted Services API. + V4, +} + +impl ApiVersion { + /// Returns the string representation of the version for URL paths. + pub fn path_segment(&self) -> &'static str { + match self { + ApiVersion::V3 => "v3", + ApiVersion::V4 => "v4", + } + } +} + +impl fmt::Display for ApiVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ApiVersion::V3 => write!(f, "v3"), + ApiVersion::V4 => write!(f, "v4"), + } + } +}