Skip to main content

dcap_qvl/
collateral.rs

1use alloc::string::{String, ToString};
2use anyhow::{anyhow, bail, Context, Result};
3use core::marker::PhantomData;
4use der::Decode as DerDecode;
5use scale::Decode;
6use serde::Deserialize;
7use x509_cert::{
8    ext::pkix::{
9        name::{DistributionPointName, GeneralName},
10        CrlDistributionPoints,
11    },
12    Certificate,
13};
14
15use crate::config::{Config, ParsedCert, X509Codec};
16use crate::configs::DefaultConfig;
17use crate::constants::{
18    PCK_ID_ENCRYPTED_PPID_2048, PCK_ID_ENCRYPTED_PPID_3072, PCK_ID_PCK_CERT_CHAIN,
19};
20use crate::quote::{EncryptedPpidParams, Quote};
21use crate::QuoteCollateralV3;
22
23#[derive(Deserialize)]
24struct TcbInfoResponse {
25    #[serde(rename = "tcbInfo")]
26    tcb_info: serde_json::Value,
27    signature: String,
28}
29
30#[derive(Deserialize)]
31struct QeIdentityResponse {
32    #[serde(rename = "enclaveIdentity")]
33    enclave_identity: serde_json::Value,
34    signature: String,
35}
36
37#[cfg(not(feature = "js"))]
38use core::time::Duration;
39
40/// Default PCCS URL (Phala Network's PCCS server).
41/// This is the recommended default for most users as it provides better availability
42/// and lower rate limits compared to Intel's PCS.
43pub const PHALA_PCCS_URL: &str = "https://pccs.phala.network";
44
45/// Intel's official PCS (Provisioning Certification Service) URL.
46/// Pass this to [`CollateralClient::with_default_http`] to fetch directly from Intel.
47pub const INTEL_PCS_URL: &str = "https://api.trustedservices.intel.com";
48
49struct PcsEndpoints {
50    base_url: String,
51    tee: &'static str,
52    fmspc: String,
53    ca: String,
54}
55
56impl PcsEndpoints {
57    fn new(base_url: &str, for_sgx: bool, fmspc: String, ca: &str) -> Self {
58        let tee = if for_sgx { "sgx" } else { "tdx" };
59        let base_url = base_url
60            .trim_end_matches('/')
61            .trim_end_matches("/sgx/certification/v4")
62            .trim_end_matches("/tdx/certification/v4")
63            .to_owned();
64        Self {
65            base_url,
66            tee,
67            fmspc,
68            ca: ca.to_owned(),
69        }
70    }
71
72    fn is_pcs(&self) -> bool {
73        self.base_url.starts_with(INTEL_PCS_URL)
74    }
75
76    fn url_pckcrl(&self) -> String {
77        self.mk_url("sgx", &format!("pckcrl?ca={}&encoding=der", self.ca))
78    }
79
80    fn url_rootcacrl(&self) -> String {
81        self.mk_url("sgx", "rootcacrl")
82    }
83
84    fn url_tcb(&self) -> String {
85        self.mk_url(self.tee, &format!("tcb?fmspc={}", self.fmspc))
86    }
87
88    fn url_qe_identity(&self) -> String {
89        self.mk_url(self.tee, "qe/identity?update=standard")
90    }
91
92    fn mk_url(&self, tee: &str, path: &str) -> String {
93        format!("{}/{}/certification/v4/{}", self.base_url, tee, path)
94    }
95}
96
97fn get_header(response: &reqwest::Response, name: &str) -> Result<String> {
98    let value = response
99        .headers()
100        .get(name)
101        .ok_or_else(|| anyhow!("Missing {name}"))?
102        .to_str()?;
103    let value = urlencoding::decode(value)?;
104    Ok(value.into_owned())
105}
106
107/// Extracts the CRL Distribution Point URL from a certificate.
108///
109/// This function parses the certificate and looks for the CRL Distribution Points extension (OID 2.5.29.31).
110/// It then extracts the first URL found in the extension's FullName field.
111///
112/// # Arguments
113/// * `cert_der` - The DER-encoded certificate bytes
114///
115/// # Returns
116/// * `Ok(Some(String))` - The CRL distribution point URL if found
117/// * `Ok(None)` - If no CRL distribution point was found in the certificate
118/// * `Err(_)` - If there was an error parsing the certificate or the extension
119fn extract_crl_url(cert_der: &[u8]) -> Result<Option<String>> {
120    let cert: Certificate = DerDecode::from_der(cert_der).context("Failed to parse certificate")?;
121
122    let Some(extensions) = &cert.tbs_certificate.extensions else {
123        return Ok(None);
124    };
125    for ext in extensions.iter() {
126        if ext.extn_id.to_string() != "2.5.29.31" {
127            continue;
128        }
129        let crl_dist_points: CrlDistributionPoints = DerDecode::from_der(ext.extn_value.as_bytes())
130            .context("Failed to parse CRL Distribution Points")?;
131
132        for dist_point in crl_dist_points.0.iter() {
133            let Some(dist_point_name) = &dist_point.distribution_point else {
134                continue;
135            };
136            let DistributionPointName::FullName(general_names) = dist_point_name else {
137                continue;
138            };
139            for general_name in general_names.iter() {
140                let GeneralName::UniformResourceIdentifier(uri) = general_name else {
141                    continue;
142                };
143                return Ok(Some(uri.to_string()));
144            }
145        }
146    }
147    Ok(None)
148}
149
150/// Fetch PCK certificate from PCCS using encrypted PPID parameters.
151async fn fetch_pck_certificate(
152    client: &reqwest::Client,
153    pccs_url: &str,
154    qeid: &[u8],
155    params: &EncryptedPpidParams,
156) -> Result<String> {
157    // PCCS normalizes parameters to uppercase, Intel PCS accepts both
158    // Use uppercase for compatibility with both
159    let qeid = hex::encode_upper(qeid);
160    let encrypted_ppid = hex::encode_upper(&params.encrypted_ppid);
161    let cpusvn = hex::encode_upper(params.cpusvn);
162    let pcesvn = hex::encode_upper(params.pcesvn.to_le_bytes());
163    let pceid = hex::encode_upper(params.pceid);
164
165    let base_url = pccs_url
166        .trim_end_matches('/')
167        .trim_end_matches("/sgx/certification/v4")
168        .trim_end_matches("/tdx/certification/v4");
169    let url = format!(
170        "{base_url}/sgx/certification/v4/pckcert?qeid={qeid}&encrypted_ppid={encrypted_ppid}&cpusvn={cpusvn}&pcesvn={pcesvn}&pceid={pceid}"
171    );
172    let response = client.get(&url).send().await?;
173
174    if !response.status().is_success() {
175        bail!(
176            "Failed to fetch PCK certificate from {}: {}",
177            url,
178            response.status()
179        );
180    }
181
182    // Check if Intel returned a certificate for a different TCB level
183    // SGX-TCBm header format: cpusvn (16 bytes) + pcesvn (2 bytes, little-endian)
184    if let Some(tcbm) = response.headers().get("SGX-TCBm") {
185        let tcbm_str = tcbm
186            .to_str()
187            .context("SGX-TCBm header contains invalid characters")?;
188        let tcbm_bytes =
189            hex::decode(tcbm_str).map_err(|e| anyhow!("SGX-TCBm header is not valid hex: {e}"))?;
190        let (matched_cpusvn, matched_pcesvn) = <([u8; 16], u16)>::decode(&mut &tcbm_bytes[..])
191            .context("SGX-TCBm header too short: expected 18 bytes")?;
192
193        if matched_cpusvn != params.cpusvn || matched_pcesvn != params.pcesvn {
194            bail!(
195                "TCB level mismatch: Platform's current TCB (cpusvn={}, pcesvn={}) \
196                is not registered with Intel PCS. Intel matched to a lower TCB level \
197                (cpusvn={}, pcesvn={}). This typically means the platform had a \
198                microcode/firmware update but MPA registration was not re-run afterward. \
199                Solution: Run 'mpa_manage -c mpa_registration.conf' on the platform \
200                to register the new TCB level with Intel.",
201                hex::encode(params.cpusvn),
202                params.pcesvn,
203                hex::encode(matched_cpusvn),
204                matched_pcesvn
205            );
206        }
207    }
208
209    // The response includes the PCK certificate chain in a header
210    let pck_cert_chain = get_header(&response, "SGX-PCK-Certificate-Issuer-Chain")?;
211
212    // The body is the leaf PCK certificate
213    let pck_cert = response.text().await?;
214
215    // Combine into a full PEM chain (leaf first, then issuer chain)
216    Ok(format!("{pck_cert}\n{pck_cert_chain}"))
217}
218
219/// Extract FMSPC and CA type from a PEM certificate chain.
220///
221/// Generic over [`Config`]; the cert parse and issuer DN extraction go
222/// through the configured [`X509Codec`].
223fn extract_fmspc_and_ca_with<C: Config>(pem_chain: &str) -> Result<(String, &'static str)> {
224    let certs = crate::utils::extract_certs(pem_chain.as_bytes())
225        .context("Failed to extract certificates from PEM chain")?;
226    let cert = certs
227        .first()
228        .ok_or_else(|| anyhow!("Empty certificate chain"))?;
229
230    // Parse cert once; reuse for both extension lookup and issuer DN.
231    let parsed = <C as Config>::X509::from_der(cert).context("Failed to decode certificate")?;
232
233    // Extract FMSPC from Intel extension
234    let extension = parsed
235        .extension(crate::oids::SGX_EXTENSION.as_bytes())
236        .context("Failed to get Intel extension from certificate")?
237        .ok_or_else(|| anyhow!("Intel extension not found"))?;
238    let fmspc = crate::utils::get_fmspc(&extension)?;
239    let fmspc_hex = hex::encode_upper(fmspc);
240
241    // Extract CA type from issuer (reuses parsed cert above)
242    let issuer = parsed
243        .issuer_dn()
244        .context("Failed to extract certificate issuer")?;
245    let ca = if issuer.contains(crate::constants::PROCESSOR_ISSUER) {
246        crate::constants::PROCESSOR_ISSUER_ID
247    } else if issuer.contains(crate::constants::PLATFORM_ISSUER) {
248        crate::constants::PLATFORM_ISSUER_ID
249    } else {
250        crate::constants::PROCESSOR_ISSUER_ID
251    };
252
253    Ok((fmspc_hex, ca))
254}
255
256/// Build a `reqwest::Client` with this crate's default settings (180s
257/// timeout on non-`js` targets). Callers who need custom TLS roots,
258/// proxies, or headers should construct their own `reqwest::Client` and
259/// pass it to [`CollateralClient::new`].
260pub fn default_http_client() -> Result<reqwest::Client> {
261    let builder = reqwest::Client::builder();
262    #[cfg(not(feature = "js"))]
263    let builder = builder.timeout(Duration::from_secs(180));
264    Ok(builder.build()?)
265}
266
267/// Get PCK certificate chain for a quote.
268/// - cert_type 5: extracts from quote
269/// - cert_type 2/3: fetches from PCCS using encrypted PPID
270async fn get_pck_chain(client: &reqwest::Client, pccs_url: &str, quote: &Quote) -> Result<String> {
271    match quote.inner_cert_type() {
272        PCK_ID_PCK_CERT_CHAIN => Ok(String::from_utf8_lossy(quote.inner_cert_data()).to_string()),
273        PCK_ID_ENCRYPTED_PPID_2048 | PCK_ID_ENCRYPTED_PPID_3072 => {
274            let params = quote.encrypted_ppid_params()?;
275            fetch_pck_certificate(client, pccs_url, quote.qeid(), &params).await
276        }
277        other => bail!("Unsupported certification data type: {other}"),
278    }
279}
280
281/// A PCCS / PCS client parameterized by [`Config`] for pluggable X.509 /
282/// crypto backends.
283///
284/// Bundles a `reqwest::Client` and a PCCS base URL, and exposes three
285/// fetch methods:
286///
287/// * [`fetch`](Self::fetch) — from a raw DCAP quote.
288/// * [`fetch_for_fmspc`](Self::fetch_for_fmspc) — when the caller already
289///   has the FMSPC / CA type (skips PCK chain extraction).
290/// * [`fetch_and_verify`](Self::fetch_and_verify) — fetch collateral and
291///   run [`verify_with`](crate::verify::verify_with) in one shot
292///   (feature-gated on `_anycrypto`).
293///
294/// # Examples
295///
296/// ```no_run
297/// use dcap_qvl::collateral::{CollateralClient, PHALA_PCCS_URL};
298/// # async fn run(quote: Vec<u8>) -> anyhow::Result<()> {
299/// let collateral = CollateralClient::with_default_http(PHALA_PCCS_URL)?
300///     .fetch(&quote)
301///     .await?;
302/// # Ok(()) }
303/// ```
304///
305/// With a custom `reqwest::Client` (e.g. for a local PCCS with a
306/// self-signed TLS cert) and the default config:
307///
308/// ```no_run
309/// # use dcap_qvl::collateral::CollateralClient;
310/// # async fn run(http: reqwest::Client, quote: Vec<u8>) -> anyhow::Result<()> {
311/// let client: CollateralClient = CollateralClient::new(http, "https://pccs.local:8081");
312/// let collateral = client.fetch(&quote).await?;
313/// # Ok(()) }
314/// ```
315pub struct CollateralClient<C: Config = DefaultConfig> {
316    http: reqwest::Client,
317    pccs_url: String,
318    _cfg: PhantomData<fn() -> C>,
319}
320
321impl<C: Config> Clone for CollateralClient<C> {
322    fn clone(&self) -> Self {
323        Self {
324            http: self.http.clone(),
325            pccs_url: self.pccs_url.clone(),
326            _cfg: PhantomData,
327        }
328    }
329}
330
331impl<C: Config> CollateralClient<C> {
332    /// Build a client with a caller-provided `reqwest::Client`.
333    ///
334    /// Use this when you need custom TLS trust roots, timeouts, proxies,
335    /// or any other [`reqwest::ClientBuilder`] options. For the common
336    /// "just give me something that works" path see
337    /// [`with_default_http`](CollateralClient::<DefaultConfig>::with_default_http).
338    pub fn new(http: reqwest::Client, pccs_url: impl Into<String>) -> Self {
339        Self {
340            http,
341            pccs_url: pccs_url.into(),
342            _cfg: PhantomData,
343        }
344    }
345
346    /// Rebind the `Config` type without rebuilding the HTTP client / URL.
347    pub fn with_config<D: Config>(self) -> CollateralClient<D> {
348        CollateralClient {
349            http: self.http,
350            pccs_url: self.pccs_url,
351            _cfg: PhantomData,
352        }
353    }
354
355    /// Fetch collateral for the given raw DCAP quote.
356    ///
357    /// Decodes the quote, fetches (or extracts) the PCK certificate
358    /// chain, reads FMSPC + CA type from the leaf cert via the
359    /// configured [`X509Codec`], and then fetches the remaining
360    /// collateral items. The returned [`QuoteCollateralV3`] has the PCK
361    /// chain attached in `pck_certificate_chain` for offline
362    /// verification.
363    pub async fn fetch(&self, quote: &[u8]) -> Result<QuoteCollateralV3> {
364        let mut quote = quote;
365        let parsed = Quote::decode(&mut quote).context("Failed to parse quote")?;
366
367        let pck_chain = get_pck_chain(&self.http, &self.pccs_url, &parsed)
368            .await
369            .context("Failed to get PCK certificate chain")?;
370
371        let (fmspc, ca) = extract_fmspc_and_ca_with::<C>(&pck_chain)?;
372
373        let mut collateral = self
374            .fetch_for_fmspc(&fmspc, ca, parsed.header.is_sgx())
375            .await?;
376
377        collateral.pck_certificate_chain = Some(pck_chain);
378        Ok(collateral)
379    }
380
381    /// Fetch the per-fmspc collateral bundle (PCK CRL, TCB info, QE
382    /// identity, root CA CRL) when FMSPC and CA type are already known.
383    ///
384    /// Internal helper called by [`fetch`](Self::fetch) after it parses
385    /// the PCK leaf. Not a standalone API — it deliberately returns a
386    /// [`QuoteCollateralV3`] with `pck_certificate_chain = None`, so
387    /// the output on its own is insufficient to verify a quote whose
388    /// certification data doesn't embed the PCK chain. Call
389    /// [`fetch`](Self::fetch) instead.
390    pub(crate) async fn fetch_for_fmspc(
391        &self,
392        fmspc: &str,
393        ca: &str,
394        for_sgx: bool,
395    ) -> Result<QuoteCollateralV3> {
396        let endpoints = PcsEndpoints::new(&self.pccs_url, for_sgx, fmspc.to_owned(), ca);
397        let client = &self.http;
398
399        // Send a GET and fail with a useful message on non-2xx, so the
400        // header / body readers below don't surface misleading errors
401        // like "missing issuer-chain header" on an HTTP 500.
402        async fn checked_get(client: &reqwest::Client, url: &str) -> Result<reqwest::Response> {
403            let response = client.get(url).send().await?;
404            if !response.status().is_success() {
405                bail!("Failed to fetch {url}: {}", response.status());
406            }
407            Ok(response)
408        }
409
410        let pck_crl_issuer_chain;
411        let pck_crl;
412        {
413            let response = checked_get(client, &endpoints.url_pckcrl()).await?;
414            pck_crl_issuer_chain = get_header(&response, "SGX-PCK-CRL-Issuer-Chain")?;
415            pck_crl = response.bytes().await?.to_vec();
416        };
417
418        let tcb_info_issuer_chain;
419        let raw_tcb_info;
420        {
421            let response = checked_get(client, &endpoints.url_tcb()).await?;
422            tcb_info_issuer_chain = get_header(&response, "SGX-TCB-Info-Issuer-Chain")
423                .or(get_header(&response, "TCB-Info-Issuer-Chain"))?;
424            raw_tcb_info = response.text().await?;
425        };
426        let qe_identity_issuer_chain;
427        let raw_qe_identity;
428        {
429            let response = checked_get(client, &endpoints.url_qe_identity()).await?;
430            qe_identity_issuer_chain = get_header(&response, "SGX-Enclave-Identity-Issuer-Chain")?;
431            raw_qe_identity = response.text().await?;
432        };
433
434        async fn http_get(client: &reqwest::Client, url: &str) -> Result<Vec<u8>> {
435            Ok(checked_get(client, url).await?.bytes().await?.to_vec())
436        }
437
438        // First try to get root CA CRL directly from the PCCS endpoint
439        let mut root_ca_crl = None;
440        if !endpoints.is_pcs() {
441            root_ca_crl = http_get(client, &endpoints.url_rootcacrl()).await.ok();
442
443            if let Some(ref crl) = root_ca_crl {
444                // PCCS returns hex-encoded CRL instead of binary DER.
445                let hex_str = core::str::from_utf8(crl)
446                    .context("Failed to convert hex-encoded CRL to string")?;
447                let ca_crl = hex::decode(hex_str)
448                    .map_err(|_| anyhow!("Failed to decode hex-encoded root CA CRL"))?;
449                root_ca_crl = Some(ca_crl);
450            }
451        }
452        let root_ca_crl = match root_ca_crl {
453            Some(crl) => crl,
454            None => {
455                let certs = crate::utils::extract_certs(qe_identity_issuer_chain.as_bytes())
456                    .context("Failed to extract certificates from QE identity issuer chain")?;
457                let root_cert_der = certs
458                    .last()
459                    .context("No certificate found in QE identity issuer chain")?;
460                let crl_url = extract_crl_url(root_cert_der)?;
461                let Some(url) = crl_url else {
462                    bail!("Could not find CRL distribution point in root certificate");
463                };
464                http_get(client, &url).await?
465            }
466        };
467
468        let tcb_info_resp: TcbInfoResponse =
469            serde_json::from_str(&raw_tcb_info).context("TCB Info should be valid JSON")?;
470        let tcb_info = tcb_info_resp.tcb_info.to_string();
471        let tcb_info_signature = hex::decode(&tcb_info_resp.signature)
472            .ok()
473            .context("TCB Info signature must be valid hex")?;
474
475        let qe_identity_resp: QeIdentityResponse =
476            serde_json::from_str(&raw_qe_identity).context("QE Identity should be valid JSON")?;
477        let qe_identity = qe_identity_resp.enclave_identity.to_string();
478        let qe_identity_signature = hex::decode(&qe_identity_resp.signature)
479            .ok()
480            .context("QE Identity signature must be valid hex")?;
481
482        Ok(QuoteCollateralV3 {
483            pck_crl_issuer_chain,
484            root_ca_crl,
485            pck_crl,
486            tcb_info_issuer_chain,
487            tcb_info,
488            tcb_info_signature,
489            qe_identity_issuer_chain,
490            qe_identity,
491            qe_identity_signature,
492            pck_certificate_chain: None,
493        })
494    }
495
496    /// Fetch collateral and run verification in one step, using the
497    /// configured `Config`'s crypto and x509 backends.
498    #[cfg(feature = "_anycrypto")]
499    pub async fn fetch_and_verify(&self, quote: &[u8]) -> Result<crate::verify::VerifiedReport> {
500        use std::time::SystemTime;
501
502        let collateral = self.fetch(quote).await?;
503        let now = SystemTime::now()
504            .duration_since(SystemTime::UNIX_EPOCH)
505            .context("Failed to get current time")?
506            .as_secs();
507        crate::verify::verify_with::<C>(quote, &collateral, now)
508    }
509}
510
511impl CollateralClient<DefaultConfig> {
512    /// Convenience constructor: build a default `reqwest::Client`
513    /// (180s timeout on non-`js` targets) and pair it with the given
514    /// PCCS URL. Uses [`DefaultConfig`] (audited `x509-cert` backend).
515    pub fn with_default_http(pccs_url: impl Into<String>) -> Result<Self> {
516        Ok(Self::new(default_http_client()?, pccs_url))
517    }
518
519    /// Zero-arg convenience constructor: default HTTP client + PCCS URL
520    /// from the `PCCS_URL` env var (trimmed; empty is treated as unset),
521    /// falling back to [`PHALA_PCCS_URL`]. Uses [`DefaultConfig`].
522    pub fn from_env() -> Result<Self> {
523        let pccs_url = std::env::var("PCCS_URL")
524            .ok()
525            .map(|s| s.trim().to_owned())
526            .filter(|s| !s.is_empty())
527            .unwrap_or_else(|| PHALA_PCCS_URL.to_owned());
528        Self::with_default_http(pccs_url)
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    #![allow(clippy::unwrap_used)]
535
536    use super::*;
537    use crate::constants::{PLATFORM_ISSUER_ID, PROCESSOR_ISSUER_ID};
538
539    // Sample PCK certificate chain (processor CA) for testing - extracted from sample/sgx_quote
540    const TEST_PCK_CHAIN_PROCESSOR: &str = r#"-----BEGIN CERTIFICATE-----
541MIIEjTCCBDSgAwIBAgIVAIG3dzK3YemOubljpKvR5bm/XdjWMAoGCCqGSM49BAMC
542MHExIzAhBgNVBAMMGkludGVsIFNHWCBQQ0sgUHJvY2Vzc29yIENBMRowGAYDVQQK
543DBFJbnRlbCBDb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNV
544BAgMAkNBMQswCQYDVQQGEwJVUzAeFw0yMzA5MjAyMTUzNDNaFw0zMDA5MjAyMTUz
545NDNaMHAxIjAgBgNVBAMMGUludGVsIFNHWCBQQ0sgQ2VydGlmaWNhdGUxGjAYBgNV
546BAoMEUludGVsIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkG
547A1UECAwCQ0ExCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
548kgmE7N3D+RspyaCZ2YoDTLDCuh5pnvAu4crPn2uAGujq9tOgwU8/y7jttShCB603
549U6r+h9ayOk2nZ9jewk25lqOCAqgwggKkMB8GA1UdIwQYMBaAFNDoqtp11/kuSReY
550PHsUZdDV8llNMGwGA1UdHwRlMGMwYaBfoF2GW2h0dHBzOi8vYXBpLnRydXN0ZWRz
551ZXJ2aWNlcy5pbnRlbC5jb20vc2d4L2NlcnRpZmljYXRpb24vdjQvcGNrY3JsP2Nh
552PXByb2Nlc3NvciZlbmNvZGluZz1kZXIwHQYDVR0OBBYEFIW4KX263PRxYJah2Cfj
553AlrcvAC9MA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMIIB1AYJKoZIhvhN
554AQ0BBIIBxTCCAcEwHgYKKoZIhvhNAQ0BAQQQ0E7AbU5tktyQ0K089e4t3zCCAWQG
555CiqGSIb4TQENAQIwggFUMBAGCyqGSIb4TQENAQIBAgELMBAGCyqGSIb4TQENAQIC
556AgELMBAGCyqGSIb4TQENAQIDAgECMBAGCyqGSIb4TQENAQIEAgECMBEGCyqGSIb4
557TQENAQIFAgIA/zAQBgsqhkiG+E0BDQECBgIBATAQBgsqhkiG+E0BDQECBwIBADAQ
558BgsqhkiG+E0BDQECCAIBADAQBgsqhkiG+E0BDQECCQIBADAQBgsqhkiG+E0BDQEC
559CgIBADAQBgsqhkiG+E0BDQECCwIBADAQBgsqhkiG+E0BDQECDAIBADAQBgsqhkiG
560+E0BDQECDQIBADAQBgsqhkiG+E0BDQECDgIBADAQBgsqhkiG+E0BDQECDwIBADAQ
561BgsqhkiG+E0BDQECEAIBADAQBgsqhkiG+E0BDQECEQIBDTAfBgsqhkiG+E0BDQEC
562EgQQCwsCAv8BAAAAAAAAAAAAADAQBgoqhkiG+E0BDQEDBAIAADAUBgoqhkiG+E0B
563DQEEBAYAoGcRAAAwDwYKKoZIhvhNAQ0BBQoBADAKBggqhkjOPQQDAgNHADBEAiBm
564SMZEtlQEjnZgGa192W3ArnZ3iyY6ckM/sTsXxCRmJgIgLf20tZHNw3a1b31JDSOW
565E6wesxoAmTeqJGRqZl621qI=
566-----END CERTIFICATE-----
567-----BEGIN CERTIFICATE-----
568MIICmDCCAj6gAwIBAgIVANDoqtp11/kuSReYPHsUZdDV8llNMAoGCCqGSM49BAMC
569MGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD
570b3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw
571CQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHExIzAh
572BgNVBAMMGkludGVsIFNHWCBQQ0sgUHJvY2Vzc29yIENBMRowGAYDVQQKDBFJbnRl
573bCBDb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNB
574MQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABL9q+NMp2IOg
575tdl1bk/uWZ5+TGQm8aCi8z78fs+fKCQ3d+uDzXnVTAT2ZhDCifyIuJwvN3wNBp9i
576HBSSMJMJrBOjgbswgbgwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqww
577UgYDVR0fBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNl
578cnZpY2VzLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFNDo
579qtp11/kuSReYPHsUZdDV8llNMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG
580AQH/AgEAMAoGCCqGSM49BAMCA0gAMEUCIQCJgTbtVqOyZ1m3jqiAXM6QYa6r5sWS
5814y/G7y8uIJGxdwIgRqPvBSKzzQagBLQq5s5A70pdoiaRJ8z/0uDz4NgV91k=
582-----END CERTIFICATE-----
583-----BEGIN CERTIFICATE-----
584MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw
585aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv
586cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ
587BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG
588A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0
589aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT
590AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7
5911OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB
592uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ
593MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50
594ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV
595Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI
596KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg
597AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=
598-----END CERTIFICATE-----
599"#;
600
601    #[test]
602    fn test_extract_fmspc_and_ca_processor() {
603        let (fmspc, ca) =
604            extract_fmspc_and_ca_with::<DefaultConfig>(TEST_PCK_CHAIN_PROCESSOR).unwrap();
605        assert_eq!(fmspc, "00A067110000");
606        assert_eq!(ca, PROCESSOR_ISSUER_ID);
607    }
608
609    #[test]
610    fn test_pcs_endpoints_new() {
611        // Test SGX endpoint initialization
612        let sgx_endpoints = PcsEndpoints::new(
613            "https://pccs.example.com",
614            true,
615            "B0C06F000000".to_string(),
616            PROCESSOR_ISSUER_ID,
617        );
618        assert_eq!(sgx_endpoints.base_url, "https://pccs.example.com");
619        assert_eq!(sgx_endpoints.tee, "sgx");
620        assert_eq!(sgx_endpoints.fmspc, "B0C06F000000");
621        assert_eq!(sgx_endpoints.ca, PROCESSOR_ISSUER_ID);
622
623        // Test TDX endpoint initialization
624        let tdx_endpoints = PcsEndpoints::new(
625            "https://pccs.example.com",
626            false,
627            "B0C06F000000".to_string(),
628            PROCESSOR_ISSUER_ID,
629        );
630        assert_eq!(tdx_endpoints.base_url, "https://pccs.example.com");
631        assert_eq!(tdx_endpoints.tee, "tdx");
632        assert_eq!(tdx_endpoints.fmspc, "B0C06F000000");
633        assert_eq!(tdx_endpoints.ca, PROCESSOR_ISSUER_ID);
634
635        // Test URL normalization during initialization
636        let endpoints_with_trailing_slash = PcsEndpoints::new(
637            "https://pccs.example.com/",
638            true,
639            "B0C06F000000".to_string(),
640            PROCESSOR_ISSUER_ID,
641        );
642        assert_eq!(
643            endpoints_with_trailing_slash.base_url,
644            "https://pccs.example.com"
645        );
646
647        // Test URL normalization with SGX certification path
648        let endpoints_with_sgx_path = PcsEndpoints::new(
649            "https://pccs.example.com/sgx/certification/v4",
650            true,
651            "B0C06F000000".to_string(),
652            PROCESSOR_ISSUER_ID,
653        );
654        assert_eq!(endpoints_with_sgx_path.base_url, "https://pccs.example.com");
655
656        // Test URL normalization with TDX certification path
657        let endpoints_with_tdx_path = PcsEndpoints::new(
658            "https://pccs.example.com/tdx/certification/v4",
659            false,
660            "B0C06F000000".to_string(),
661            PROCESSOR_ISSUER_ID,
662        );
663        assert_eq!(endpoints_with_tdx_path.base_url, "https://pccs.example.com");
664    }
665
666    #[test]
667    fn test_pcs_endpoints_url_pckcrl() {
668        // Test with processor CA
669        let processor_endpoints = PcsEndpoints::new(
670            "https://pccs.example.com",
671            true,
672            "B0C06F000000".to_string(),
673            PROCESSOR_ISSUER_ID,
674        );
675        assert_eq!(
676            processor_endpoints.url_pckcrl(),
677            "https://pccs.example.com/sgx/certification/v4/pckcrl?ca=processor&encoding=der"
678        );
679
680        // Test with platform CA
681        let platform_endpoints = PcsEndpoints::new(
682            "https://pccs.example.com",
683            true,
684            "B0C06F000000".to_string(),
685            PLATFORM_ISSUER_ID,
686        );
687        assert_eq!(
688            platform_endpoints.url_pckcrl(),
689            "https://pccs.example.com/sgx/certification/v4/pckcrl?ca=platform&encoding=der"
690        );
691    }
692
693    #[test]
694    fn test_pcs_endpoints_url_rootcacrl() {
695        let endpoints = PcsEndpoints::new(
696            "https://pccs.example.com",
697            true,
698            "B0C06F000000".to_string(),
699            PROCESSOR_ISSUER_ID,
700        );
701        assert_eq!(
702            endpoints.url_rootcacrl(),
703            "https://pccs.example.com/sgx/certification/v4/rootcacrl"
704        );
705    }
706
707    #[test]
708    fn test_pcs_endpoints_url_tcb() {
709        // Test SGX TCB URL
710        let sgx_endpoints = PcsEndpoints::new(
711            "https://pccs.example.com",
712            true,
713            "B0C06F000000".to_string(),
714            PROCESSOR_ISSUER_ID,
715        );
716        assert_eq!(
717            sgx_endpoints.url_tcb(),
718            "https://pccs.example.com/sgx/certification/v4/tcb?fmspc=B0C06F000000"
719        );
720
721        // Test TDX TCB URL
722        let tdx_endpoints = PcsEndpoints::new(
723            "https://pccs.example.com",
724            false,
725            "B0C06F000000".to_string(),
726            PROCESSOR_ISSUER_ID,
727        );
728        assert_eq!(
729            tdx_endpoints.url_tcb(),
730            "https://pccs.example.com/tdx/certification/v4/tcb?fmspc=B0C06F000000"
731        );
732    }
733
734    #[test]
735    fn test_pcs_endpoints_url_qe_identity() {
736        // Test SGX QE identity URL
737        let sgx_endpoints = PcsEndpoints::new(
738            "https://pccs.example.com",
739            true,
740            "B0C06F000000".to_string(),
741            PROCESSOR_ISSUER_ID,
742        );
743        assert_eq!(
744            sgx_endpoints.url_qe_identity(),
745            "https://pccs.example.com/sgx/certification/v4/qe/identity?update=standard"
746        );
747
748        // Test TDX QE identity URL
749        let tdx_endpoints = PcsEndpoints::new(
750            "https://pccs.example.com",
751            false,
752            "B0C06F000000".to_string(),
753            PROCESSOR_ISSUER_ID,
754        );
755        assert_eq!(
756            tdx_endpoints.url_qe_identity(),
757            "https://pccs.example.com/tdx/certification/v4/qe/identity?update=standard"
758        );
759    }
760
761    #[test]
762    fn test_intel_pcs_url() {
763        // Test the Intel PCS URL constant
764        assert_eq!(INTEL_PCS_URL, "https://api.trustedservices.intel.com");
765
766        // Test the Phala PCCS URL constant
767        assert_eq!(PHALA_PCCS_URL, "https://pccs.phala.network");
768
769        // Test with the known FMSPC from memory
770        let fmspc = "B0C06F000000";
771        let intel_endpoints =
772            PcsEndpoints::new(INTEL_PCS_URL, true, fmspc.to_string(), PROCESSOR_ISSUER_ID);
773
774        assert_eq!(
775            intel_endpoints.url_pckcrl(),
776            "https://api.trustedservices.intel.com/sgx/certification/v4/pckcrl?ca=processor&encoding=der"
777        );
778
779        assert_eq!(
780            intel_endpoints.url_rootcacrl(),
781            "https://api.trustedservices.intel.com/sgx/certification/v4/rootcacrl"
782        );
783
784        assert_eq!(
785            intel_endpoints.url_tcb(),
786            "https://api.trustedservices.intel.com/sgx/certification/v4/tcb?fmspc=B0C06F000000"
787        );
788
789        assert_eq!(
790            intel_endpoints.url_qe_identity(),
791            "https://api.trustedservices.intel.com/sgx/certification/v4/qe/identity?update=standard"
792        );
793    }
794}