Skip to main content

dcap_qvl/policy/
simple.rs

1use core::time::Duration;
2
3use anyhow::{bail, Result};
4use serde::{Deserialize, Serialize};
5
6use {
7    super::{PckCertFlag, Policy, QuoteClaims},
8    crate::tcb_info::TcbStatus,
9    crate::utils::parse_rfc3339_unix_secs,
10    alloc::string::String,
11    alloc::vec::Vec,
12};
13
14/// Built-in verification policy with builder pattern.
15///
16/// Covers common appraisal checks without embedding a policy engine.
17/// without requiring a Rego engine. Strict by default: only `UpToDate`,
18/// no grace period, no advisory blacklist.
19///
20/// # Example
21/// ```no_run
22/// use dcap_qvl::QuotePolicy;
23/// use dcap_qvl::TcbStatus;
24///
25/// let now = 1_700_000_000u64; // unix timestamp
26///
27/// // Strict: only UpToDate, collateral must not be expired
28/// let policy = QuotePolicy::strict(now);
29///
30/// // Allow an out-of-date platform for 90 days after its TCB level date.
31/// use core::time::Duration;
32/// let policy = QuotePolicy::strict(now)
33///     .allow_status(TcbStatus::OutOfDate)
34///     .platform_grace_period(Duration::from_secs(90 * 24 * 3600))
35///     .reject_advisory("INTEL-SA-00334");
36/// ```
37#[derive(Clone, Debug)]
38pub struct QuotePolicy {
39    claims_only: bool,
40    acceptable_statuses: u8,
41
42    // Current time + TCB grace periods (default 0 = no tolerance)
43    now: u64,
44    platform_grace_period: u64,
45    qe_grace_period: u64,
46
47    // TCB evaluation
48    min_tcb_eval_data_number: Option<u32>,
49
50    // Advisory blacklist (quote is rejected if any advisory is in this set)
51    rejected_advisory_ids: Vec<String>,
52
53    // Platform flags (default false = reject if True)
54    allow_dynamic_platform: bool,
55    allow_cached_keys: bool,
56    allow_smt: bool,
57
58    // SGX type whitelist (None = skip check)
59    accepted_sgx_types: Option<Vec<u8>>,
60}
61
62impl QuotePolicy {
63    const UP_TO_DATE: u8 = 1 << 0;
64    const SW_HARDENING_NEEDED: u8 = 1 << 1;
65    const CONFIGURATION_NEEDED: u8 = 1 << 2;
66    const CONFIGURATION_AND_SW_HARDENING_NEEDED: u8 = 1 << 3;
67    const OUT_OF_DATE: u8 = 1 << 4;
68    const OUT_OF_DATE_CONFIGURATION_NEEDED: u8 = 1 << 5;
69
70    fn status_to_flag(status: TcbStatus) -> u8 {
71        match status {
72            TcbStatus::UpToDate => Self::UP_TO_DATE,
73            TcbStatus::SWHardeningNeeded => Self::SW_HARDENING_NEEDED,
74            TcbStatus::ConfigurationNeeded => Self::CONFIGURATION_NEEDED,
75            TcbStatus::ConfigurationAndSWHardeningNeeded => {
76                Self::CONFIGURATION_AND_SW_HARDENING_NEEDED
77            }
78            TcbStatus::OutOfDate => Self::OUT_OF_DATE,
79            TcbStatus::OutOfDateConfigurationNeeded => Self::OUT_OF_DATE_CONFIGURATION_NEEDED,
80            TcbStatus::Revoked => 0,
81        }
82    }
83
84    fn new_with_statuses(now: u64, acceptable_statuses: u8) -> Self {
85        Self {
86            claims_only: false,
87            acceptable_statuses,
88            now,
89            platform_grace_period: 0,
90            qe_grace_period: 0,
91            min_tcb_eval_data_number: None,
92            rejected_advisory_ids: Vec::new(),
93            allow_dynamic_platform: false,
94            allow_cached_keys: false,
95            allow_smt: false,
96            accepted_sgx_types: None,
97        }
98    }
99
100    /// Create a strict policy: only `UpToDate` status is accepted,
101    /// no grace period, no advisory blacklist.
102    pub fn strict(now_secs: u64) -> Self {
103        Self::new_with_statuses(now_secs, Self::UP_TO_DATE)
104    }
105
106    /// Create a policy that only supplies the trusted verification time.
107    ///
108    /// Use this when policy appraisal is performed by a downstream engine.
109    pub fn claims_only(now_secs: u64) -> Self {
110        let mut policy = Self::new_with_statuses(now_secs, 0);
111        policy.claims_only = true;
112        policy
113    }
114
115    /// Allow an additional TCB status.
116    pub fn allow_status(mut self, status: TcbStatus) -> Self {
117        self.acceptable_statuses |= Self::status_to_flag(status);
118        self
119    }
120
121    /// Set platform grace period (default: zero). When TCB status is
122    /// OutOfDate or OutOfDateConfigurationNeeded, accepts quotes where
123    /// `tcb_level_date_tag + grace_period >= now`. Skipped for UpToDate/ConfigNeeded/SWHardening.
124    pub fn platform_grace_period(mut self, duration: Duration) -> Self {
125        self.platform_grace_period = duration.as_secs();
126        self
127    }
128
129    /// Set QE grace period (default: zero). When QE TCB status is `OutOfDate`,
130    /// accepts quotes where `qe_tcb_level.tcb_date + grace_period >= now`.
131    pub fn qe_grace_period(mut self, duration: Duration) -> Self {
132        self.qe_grace_period = duration.as_secs();
133        self
134    }
135
136    /// Set minimum TCB evaluation data number. Rejects quotes with
137    /// `tcb_eval_data_number` below this threshold.
138    pub fn min_tcb_eval_data_number(mut self, min: u32) -> Self {
139        self.min_tcb_eval_data_number = Some(min);
140        self
141    }
142
143    /// Reject a specific advisory ID. Quotes containing any advisory in the
144    /// rejected set fail validation. By default the set is empty, allowing all
145    /// advisory IDs.
146    pub fn reject_advisory(mut self, id: impl Into<String>) -> Self {
147        self.rejected_advisory_ids.push(id.into());
148        self
149    }
150
151    /// Reject multiple advisory IDs at once.
152    pub fn reject_advisories(mut self, ids: &[impl AsRef<str>]) -> Self {
153        self.rejected_advisory_ids
154            .extend(ids.iter().map(|id| id.as_ref().to_string()));
155        self
156    }
157
158    /// Set whether dynamic platforms are allowed. If `false` (default), rejects
159    /// quotes where `dynamic_platform` is `True`.
160    pub fn allow_dynamic_platform(mut self, allow: bool) -> Self {
161        self.allow_dynamic_platform = allow;
162        self
163    }
164
165    /// Set whether cached keys are allowed. If `false` (default), rejects
166    /// quotes where `cached_keys` is `True`.
167    pub fn allow_cached_keys(mut self, allow: bool) -> Self {
168        self.allow_cached_keys = allow;
169        self
170    }
171
172    /// Set whether SMT (simultaneous multithreading / hyperthreading) is allowed.
173    /// If `false` (default), rejects quotes where `smt_enabled` is `True`.
174    pub fn allow_smt(mut self, allow: bool) -> Self {
175        self.allow_smt = allow;
176        self
177    }
178
179    /// Set accepted SGX types (0=Standard, 1=Scalable, 2=ScalableWithIntegrity).
180    /// Rejects quotes with `sgx_type` not in this list. Default: skip check.
181    pub fn accepted_sgx_types(mut self, types: &[u8]) -> Self {
182        self.accepted_sgx_types = Some(types.to_vec());
183        self
184    }
185
186    /// Check if a TCB status is acceptable according to this policy.
187    pub fn is_status_acceptable(&self, status: TcbStatus) -> bool {
188        let flag = Self::status_to_flag(status);
189        (self.acceptable_statuses & flag) != 0
190    }
191}
192
193impl Policy for QuotePolicy {
194    fn validate(&self, data: &QuoteClaims) -> Result<()> {
195        if self.claims_only {
196            return Ok(());
197        }
198        fn within_grace(date_tag: u64, grace_period: u64, now: u64) -> bool {
199            date_tag.saturating_add(grace_period) >= now
200        }
201
202        fn advisory_rejected(rejected_advisory_ids: &[String], id: &str) -> bool {
203            rejected_advisory_ids
204                .iter()
205                .any(|a| a.eq_ignore_ascii_case(id))
206        }
207
208        // 1. TCB status whitelist
209        if !self.is_status_acceptable(data.tcb.status) {
210            bail!(
211                "TCB status {:?} is not acceptable by policy",
212                data.tcb.status
213            );
214        }
215
216        // 3. Collateral expiration. The verification pipeline already enforces
217        // this at the same trusted time; retain the check for delayed appraisal.
218        if data.earliest_expiration_date < self.now {
219            bail!(
220                "Collateral expired: earliest_expiration {} < now {}",
221                data.earliest_expiration_date,
222                self.now
223            );
224        }
225
226        // 4. Platform TCB freshness: platform tcb_date_tag + grace >= now.
227        let platform_is_out_of_date = matches!(
228            data.platform.tcb_level.tcb_status,
229            TcbStatus::OutOfDate | TcbStatus::OutOfDateConfigurationNeeded
230        );
231        let platform_in_grace = platform_is_out_of_date
232            && within_grace(
233                data.platform.tcb_date_tag,
234                self.platform_grace_period,
235                self.now,
236            );
237        if platform_is_out_of_date && !platform_in_grace {
238            bail!(
239                "Platform TCB too old: tcb_date_tag {} + grace {} < now {}",
240                data.platform.tcb_date_tag,
241                self.platform_grace_period,
242                self.now
243            );
244        }
245
246        // 4b. QE TCB freshness: QE tcb_date + grace >= now.
247        let qe_tcb_date_tag = parse_rfc3339_unix_secs(&data.qe.tcb_level.tcb_date)
248            .map_err(|e| anyhow::anyhow!("Failed to parse QE TCB date: {e}"))?;
249        let qe_is_out_of_date = data.qe.tcb_level.tcb_status == TcbStatus::OutOfDate;
250        let qe_in_grace =
251            qe_is_out_of_date && within_grace(qe_tcb_date_tag, self.qe_grace_period, self.now);
252        if qe_is_out_of_date && !qe_in_grace {
253            bail!(
254                "QE TCB too old: tcb_date_tag {} + grace {} < now {}",
255                qe_tcb_date_tag,
256                self.qe_grace_period,
257                self.now
258            );
259        }
260
261        // 2. Advisory ID blacklist.
262        for id in &data.platform.tcb_level.advisory_ids {
263            if advisory_rejected(&self.rejected_advisory_ids, id) {
264                bail!("Advisory ID {id} is rejected by policy");
265            }
266        }
267        for id in &data.qe.tcb_level.advisory_ids {
268            if advisory_rejected(&self.rejected_advisory_ids, id) {
269                bail!("Advisory ID {id} is rejected by policy");
270            }
271        }
272
273        // 5. Minimum TCB evaluation data number
274        if let Some(min) = self.min_tcb_eval_data_number {
275            if data.tcb.eval_data_number < min {
276                bail!(
277                    "TCB eval data number {} is below minimum {}",
278                    data.tcb.eval_data_number,
279                    min
280                );
281            }
282        }
283
284        // 6. Dynamic platform flag
285        if !self.allow_dynamic_platform && data.platform.pck.dynamic_platform == PckCertFlag::True {
286            bail!("Dynamic platform is not allowed by policy");
287        }
288
289        // 7. Cached keys flag
290        if !self.allow_cached_keys && data.platform.pck.cached_keys == PckCertFlag::True {
291            bail!("Cached keys are not allowed by policy");
292        }
293
294        // 8. SMT flag
295        if !self.allow_smt && data.platform.pck.smt_enabled == PckCertFlag::True {
296            bail!("SMT (hyperthreading) is not allowed by policy");
297        }
298
299        // 9. SGX type whitelist
300        if let Some(ref types) = self.accepted_sgx_types {
301            if !types.contains(&data.platform.pck.sgx_type) {
302                bail!(
303                    "SGX type {} is not in accepted types {:?}",
304                    data.platform.pck.sgx_type,
305                    types
306                );
307            }
308        }
309
310        Ok(())
311    }
312}
313
314/// JSON-serializable configuration for [`QuotePolicy`].
315///
316/// All fields default to the strict values (zero / empty / false).
317/// Pass as JSON from FFI (Go, Python) to configure verification policy.
318///
319/// Unknown fields are rejected so that a stale or misspelled option in a
320/// security policy fails loudly instead of being silently ignored.
321///
322/// ```json
323/// {
324///   "allowed_statuses": ["UpToDate", "SWHardeningNeeded"],
325///   "rejected_advisory_ids": ["INTEL-SA-00334"],
326///   "platform_grace_period_secs": 2592000,
327///   "allow_smt": true
328/// }
329/// ```
330#[derive(Clone, Debug, Default, Serialize, Deserialize)]
331#[serde(deny_unknown_fields)]
332pub struct QuotePolicyConfig {
333    #[serde(default)]
334    pub allowed_statuses: Vec<TcbStatus>,
335    #[serde(default)]
336    pub rejected_advisory_ids: Vec<String>,
337    #[serde(default)]
338    pub platform_grace_period_secs: u64,
339    #[serde(default)]
340    pub qe_grace_period_secs: u64,
341    #[serde(default)]
342    pub min_tcb_eval_data_number: u32,
343    #[serde(default)]
344    pub allow_dynamic_platform: bool,
345    #[serde(default)]
346    pub allow_cached_keys: bool,
347    #[serde(default)]
348    pub allow_smt: bool,
349    #[serde(default)]
350    pub accepted_sgx_types: Option<Vec<u8>>,
351}
352
353impl QuotePolicyConfig {
354    /// Build a [`QuotePolicy`] from this config + current timestamp.
355    ///
356    /// Default config (all fields zero/empty) produces `QuotePolicy::strict(now)`.
357    pub fn into_policy(self, now_secs: u64) -> QuotePolicy {
358        let mut policy = if self.allowed_statuses.is_empty() {
359            QuotePolicy::strict(now_secs)
360        } else {
361            let mut p = QuotePolicy::new_with_statuses(now_secs, 0);
362            for status in self.allowed_statuses {
363                p = p.allow_status(status);
364            }
365            p
366        };
367        for id in self.rejected_advisory_ids {
368            policy = policy.reject_advisory(id);
369        }
370        if self.platform_grace_period_secs > 0 {
371            policy =
372                policy.platform_grace_period(Duration::from_secs(self.platform_grace_period_secs));
373        }
374        if self.qe_grace_period_secs > 0 {
375            policy = policy.qe_grace_period(Duration::from_secs(self.qe_grace_period_secs));
376        }
377        if self.min_tcb_eval_data_number > 0 {
378            policy = policy.min_tcb_eval_data_number(self.min_tcb_eval_data_number);
379        }
380        policy = policy.allow_dynamic_platform(self.allow_dynamic_platform);
381        policy = policy.allow_cached_keys(self.allow_cached_keys);
382        policy = policy.allow_smt(self.allow_smt);
383        if let Some(types) = self.accepted_sgx_types {
384            policy = policy.accepted_sgx_types(&types);
385        }
386        policy
387    }
388}
389
390#[cfg(test)]
391#[allow(clippy::unwrap_used)]
392mod tests {
393    use super::*;
394    use crate::policy::{PckIdentity, PlatformInfo, QeInfo, TcbVerdict};
395    use crate::tcb_info::TcbStatus::*;
396
397    fn make_test_claims(tcb_status: TcbStatus) -> QuoteClaims {
398        use crate::qe_identity::{QeTcb, QeTcbLevel};
399        use crate::tcb_info::{Tcb, TcbComponents, TcbLevel};
400
401        QuoteClaims {
402            claims_version: 1,
403            header: crate::quote::Header {
404                version: 3,
405                attestation_key_type: 2,
406                tee_type: 0,
407                qe_svn: 0,
408                pce_svn: 0,
409                qe_vendor_id: [0; 16],
410                user_data: [0; 20],
411            },
412            tee_type: 0,
413            tcb: TcbVerdict {
414                status: tcb_status,
415                advisory_ids: vec![],
416                eval_data_number: 17,
417            },
418            platform: PlatformInfo {
419                tcb_level: TcbLevel {
420                    tcb: Tcb {
421                        sgx_components: vec![TcbComponents { svn: 0 }; 16],
422                        tdx_components: vec![],
423                        pce_svn: 13,
424                    },
425                    tcb_date: "2023-07-22T00:00:00Z".to_string(),
426                    tcb_status,
427                    advisory_ids: vec![],
428                },
429                tcb_date_tag: 1_690_000_000, // ~2023-07-22
430                pck: PckIdentity {
431                    ppid: vec![0u8; 16],
432                    cpu_svn: [0u8; 16],
433                    pce_svn: 13,
434                    pce_id: vec![0u8; 2],
435                    fmspc: [0u8; 6],
436                    sgx_type: 0,
437                    platform_instance_id: None,
438                    dynamic_platform: PckCertFlag::Undefined,
439                    cached_keys: PckCertFlag::Undefined,
440                    smt_enabled: PckCertFlag::Undefined,
441                    platform_provider_id: None,
442                },
443                root_key_id: vec![0u8; 48],
444                pck_crl_num: 1,
445                root_ca_crl_num: 1,
446            },
447            qe: QeInfo {
448                tcb_level: QeTcbLevel {
449                    tcb: QeTcb { isvsvn: 8 },
450                    tcb_date: "2024-03-13T00:00:00Z".to_string(),
451                    tcb_status: UpToDate,
452                    advisory_ids: vec![],
453                },
454                report: crate::quote::EnclaveReport {
455                    cpu_svn: [0u8; 16],
456                    misc_select: 0,
457                    reserved1: [0u8; 28],
458                    attributes: [0u8; 16],
459                    mr_enclave: [0u8; 32],
460                    reserved2: [0u8; 32],
461                    mr_signer: [0u8; 32],
462                    reserved3: [0u8; 96],
463                    isv_prod_id: 1,
464                    isv_svn: 8,
465                    reserved4: [0u8; 60],
466                    report_data: [0u8; 64],
467                },
468                tcb_eval_data_number: 17,
469            },
470            report: crate::quote::Report::SgxEnclave(crate::quote::EnclaveReport {
471                cpu_svn: [0u8; 16],
472                misc_select: 0,
473                reserved1: [0u8; 28],
474                attributes: [0u8; 16],
475                mr_enclave: [0u8; 32],
476                reserved2: [0u8; 32],
477                mr_signer: [0u8; 32],
478                reserved3: [0u8; 96],
479                isv_prod_id: 0,
480                isv_svn: 0,
481                reserved4: [0u8; 60],
482                report_data: [0u8; 64],
483            }),
484            earliest_issue_date: 1_690_000_000,
485            latest_issue_date: 1_690_100_000,
486            earliest_expiration_date: 1_703_000_000, // ~2023-12-19
487            qe_iden_earliest_issue_date: 1_690_000_000,
488            qe_iden_latest_issue_date: 1_690_100_000,
489            qe_iden_earliest_expiration_date: 1_703_000_000,
490        }
491    }
492
493    // -- TCB status checks --
494
495    #[test]
496    fn policy_strict_accepts_up_to_date() {
497        let data = make_test_claims(UpToDate);
498        let policy = QuotePolicy::strict(1_702_000_000); // within collateral window
499        assert!(policy.validate(&data).is_ok());
500    }
501
502    #[test]
503    fn policy_strict_rejects_sw_hardening() {
504        let data = make_test_claims(SWHardeningNeeded);
505        let policy = QuotePolicy::strict(1_702_000_000);
506        let err = policy.validate(&data).unwrap_err().to_string();
507        assert!(err.contains("TCB status"), "{err}");
508    }
509
510    #[test]
511    fn policy_out_of_date_with_fresh_tcb_date_accepts() {
512        let mut data = make_test_claims(OutOfDate);
513        data.platform.tcb_date_tag = 1_702_000_000;
514        let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDate);
515        assert!(policy.validate(&data).is_ok());
516    }
517
518    #[test]
519    fn policy_allow_status_builder() {
520        let data = make_test_claims(SWHardeningNeeded);
521        let policy = QuotePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded);
522        assert!(policy.validate(&data).is_ok());
523    }
524
525    // -- Advisory ID blacklist --
526
527    #[test]
528    fn policy_allows_advisory_when_not_blacklisted() {
529        let mut data = make_test_claims(UpToDate);
530        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
531        data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
532        let policy = QuotePolicy::strict(1_702_000_000);
533        assert!(policy.validate(&data).is_ok());
534    }
535
536    #[test]
537    fn policy_rejects_blacklisted_advisory() {
538        let mut data = make_test_claims(UpToDate);
539        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
540        data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
541        let policy = QuotePolicy::strict(1_702_000_000).reject_advisory("INTEL-SA-00615");
542        let err = policy.validate(&data).unwrap_err().to_string();
543        assert!(err.contains("INTEL-SA-00615"), "{err}");
544    }
545
546    #[test]
547    fn policy_advisory_blacklist_case_insensitive() {
548        let mut data = make_test_claims(UpToDate);
549        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
550        data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
551        let policy = QuotePolicy::strict(1_702_000_000).reject_advisory("intel-sa-00615");
552        let err = policy.validate(&data).unwrap_err().to_string();
553        assert!(err.contains("INTEL-SA-00615"), "{err}");
554    }
555
556    #[test]
557    fn policy_reject_advisories_batch() {
558        let mut data = make_test_claims(UpToDate);
559        data.tcb.advisory_ids = vec!["INTEL-SA-00820".to_string()];
560        data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00820".to_string()];
561        let policy = QuotePolicy::strict(1_702_000_000)
562            .reject_advisories(&["INTEL-SA-00615", "INTEL-SA-00820"]);
563        let err = policy.validate(&data).unwrap_err().to_string();
564        assert!(err.contains("INTEL-SA-00820"), "{err}");
565    }
566
567    #[test]
568    fn policy_empty_advisories_passes() {
569        let data = make_test_claims(UpToDate);
570        assert!(data.tcb.advisory_ids.is_empty());
571        let policy = QuotePolicy::strict(1_702_000_000);
572        assert!(policy.validate(&data).is_ok());
573    }
574
575    // -- Collateral expiration --
576
577    #[test]
578    fn policy_collateral_expired_rejects() {
579        let data = make_test_claims(UpToDate);
580        let policy = QuotePolicy::strict(1_704_000_000);
581        let err = policy.validate(&data).unwrap_err().to_string();
582        assert!(err.contains("Collateral expired"), "{err}");
583    }
584
585    #[test]
586    fn policy_collateral_not_expired_passes() {
587        let data = make_test_claims(UpToDate);
588        let policy = QuotePolicy::strict(1_702_000_000);
589        assert!(policy.validate(&data).is_ok());
590    }
591
592    // -- Platform grace period --
593
594    #[test]
595    fn policy_platform_grace_skipped_for_up_to_date() {
596        let data = make_test_claims(UpToDate);
597        let policy = QuotePolicy::strict(1_702_000_000);
598        assert!(policy.validate(&data).is_ok());
599    }
600
601    #[test]
602    fn policy_platform_grace_skipped_for_sw_hardening() {
603        let data = make_test_claims(SWHardeningNeeded);
604        let policy = QuotePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded);
605        assert!(policy.validate(&data).is_ok());
606    }
607
608    #[test]
609    fn policy_platform_grace_skipped_for_config_needed() {
610        let data = make_test_claims(ConfigurationNeeded);
611        let policy = QuotePolicy::strict(1_702_000_000).allow_status(ConfigurationNeeded);
612        assert!(policy.validate(&data).is_ok());
613    }
614
615    #[test]
616    fn policy_platform_grace_checked_for_out_of_date_rejects() {
617        let data = make_test_claims(OutOfDate);
618        let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDate);
619        let err = policy.validate(&data).unwrap_err().to_string();
620        assert!(err.contains("Platform TCB too old"), "{err}");
621    }
622
623    #[test]
624    fn policy_platform_grace_checked_for_out_of_date_accepts_with_grace() {
625        let data = make_test_claims(OutOfDate);
626        let policy = QuotePolicy::strict(1_702_000_000)
627            .allow_status(OutOfDate)
628            .platform_grace_period(Duration::from_secs(13_000_000));
629        assert!(policy.validate(&data).is_ok());
630    }
631
632    #[test]
633    fn policy_platform_grace_too_short_rejects() {
634        let data = make_test_claims(OutOfDate);
635        let policy = QuotePolicy::strict(1_702_000_000)
636            .allow_status(OutOfDate)
637            .platform_grace_period(Duration::from_secs(11_000_000));
638        let err = policy.validate(&data).unwrap_err().to_string();
639        assert!(err.contains("Platform TCB too old"), "{err}");
640    }
641
642    #[test]
643    fn policy_platform_grace_checked_for_out_of_date_config_needed() {
644        let data = make_test_claims(OutOfDateConfigurationNeeded);
645        let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDateConfigurationNeeded);
646        let err = policy.validate(&data).unwrap_err().to_string();
647        assert!(err.contains("Platform TCB too old"), "{err}");
648    }
649
650    // -- min_tcb_eval_data_number --
651
652    #[test]
653    fn policy_min_eval_num_rejects_below() {
654        let data = make_test_claims(UpToDate);
655        assert_eq!(data.tcb.eval_data_number, 17);
656        let policy = QuotePolicy::strict(1_702_000_000).min_tcb_eval_data_number(20);
657        let err = policy.validate(&data).unwrap_err().to_string();
658        assert!(err.contains("below minimum"), "{err}");
659    }
660
661    #[test]
662    fn policy_min_eval_num_accepts_equal() {
663        let data = make_test_claims(UpToDate);
664        let policy = QuotePolicy::strict(1_702_000_000).min_tcb_eval_data_number(17);
665        assert!(policy.validate(&data).is_ok());
666    }
667
668    // -- Platform flags --
669
670    #[test]
671    fn policy_rejects_dynamic_platform_true() {
672        let mut data = make_test_claims(UpToDate);
673        data.platform.pck.dynamic_platform = PckCertFlag::True;
674        let policy = QuotePolicy::strict(1_702_000_000);
675        let err = policy.validate(&data).unwrap_err().to_string();
676        assert!(err.contains("Dynamic platform"), "{err}");
677    }
678
679    #[test]
680    fn policy_allows_dynamic_platform_when_configured() {
681        let mut data = make_test_claims(UpToDate);
682        data.platform.pck.dynamic_platform = PckCertFlag::True;
683        let policy = QuotePolicy::strict(1_702_000_000).allow_dynamic_platform(true);
684        assert!(policy.validate(&data).is_ok());
685    }
686
687    #[test]
688    fn policy_undefined_platform_flags_pass() {
689        let data = make_test_claims(UpToDate);
690        assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined);
691        assert_eq!(data.platform.pck.cached_keys, PckCertFlag::Undefined);
692        assert_eq!(data.platform.pck.smt_enabled, PckCertFlag::Undefined);
693        let policy = QuotePolicy::strict(1_702_000_000);
694        assert!(policy.validate(&data).is_ok());
695    }
696
697    #[test]
698    fn policy_rejects_smt_true() {
699        let mut data = make_test_claims(UpToDate);
700        data.platform.pck.smt_enabled = PckCertFlag::True;
701        let policy = QuotePolicy::strict(1_702_000_000);
702        let err = policy.validate(&data).unwrap_err().to_string();
703        assert!(err.contains("SMT"), "{err}");
704    }
705
706    #[test]
707    fn policy_rejects_cached_keys_true() {
708        let mut data = make_test_claims(UpToDate);
709        data.platform.pck.cached_keys = PckCertFlag::True;
710        let policy = QuotePolicy::strict(1_702_000_000);
711        let err = policy.validate(&data).unwrap_err().to_string();
712        assert!(err.contains("Cached keys"), "{err}");
713    }
714
715    // -- SGX type whitelist --
716
717    #[test]
718    fn policy_sgx_type_not_configured_passes() {
719        let data = make_test_claims(UpToDate);
720        let policy = QuotePolicy::strict(1_702_000_000);
721        assert!(policy.validate(&data).is_ok());
722    }
723
724    #[test]
725    fn policy_sgx_type_whitelist_rejects() {
726        let mut data = make_test_claims(UpToDate);
727        data.platform.pck.sgx_type = 1;
728        let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0]);
729        let err = policy.validate(&data).unwrap_err().to_string();
730        assert!(err.contains("SGX type"), "{err}");
731    }
732
733    #[test]
734    fn policy_sgx_type_whitelist_accepts() {
735        let mut data = make_test_claims(UpToDate);
736        data.platform.pck.sgx_type = 1;
737        let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0, 1, 2]);
738        assert!(policy.validate(&data).is_ok());
739    }
740
741    // -- Advisory blacklist during grace --
742
743    #[test]
744    fn policy_blacklist_checked_during_platform_grace() {
745        let mut data = make_test_claims(OutOfDate);
746        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
747        data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
748        let policy = QuotePolicy::strict(1_702_000_000)
749            .allow_status(OutOfDate)
750            .platform_grace_period(Duration::from_secs(13_000_000))
751            .reject_advisory("INTEL-SA-00615");
752        let err = policy.validate(&data).unwrap_err().to_string();
753        assert!(err.contains("INTEL-SA-00615"), "{err}");
754    }
755
756    #[test]
757    fn policy_blacklist_checked_for_out_of_date_config_needed() {
758        let mut data = make_test_claims(OutOfDateConfigurationNeeded);
759        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
760        data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
761        let policy = QuotePolicy::strict(1_702_000_000)
762            .allow_status(OutOfDateConfigurationNeeded)
763            .platform_grace_period(Duration::from_secs(13_000_000))
764            .reject_advisory("INTEL-SA-00615");
765        let err = policy.validate(&data).unwrap_err().to_string();
766        assert!(err.contains("INTEL-SA-00615"), "{err}");
767    }
768
769    #[test]
770    fn policy_blacklist_checked_without_grace() {
771        let mut data = make_test_claims(UpToDate);
772        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
773        data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
774        let policy = QuotePolicy::strict(1_702_000_000).reject_advisory("INTEL-SA-00615");
775        let err = policy.validate(&data).unwrap_err().to_string();
776        assert!(err.contains("INTEL-SA-00615"), "{err}");
777    }
778
779    #[test]
780    fn policy_platform_grace_does_not_cover_qe_out_of_date() {
781        let mut data = make_test_claims(OutOfDate);
782        data.platform.tcb_level.tcb_status = UpToDate;
783        data.platform.tcb_level.advisory_ids = vec![];
784        data.platform.tcb_date_tag = 1_702_000_000;
785        data.qe.tcb_level.tcb_status = OutOfDate;
786        data.qe.tcb_level.tcb_date = "2023-07-22T00:00:00Z".to_string();
787        data.qe.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
788        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
789
790        let policy = QuotePolicy::strict(1_702_000_000)
791            .allow_status(OutOfDate)
792            .platform_grace_period(Duration::from_secs(13_000_000));
793        let err = policy.validate(&data).unwrap_err().to_string();
794        assert!(err.contains("QE TCB too old"), "{err}");
795    }
796
797    #[test]
798    fn policy_qe_grace_accepts_qe_out_of_date() {
799        let mut data = make_test_claims(OutOfDate);
800        data.platform.tcb_level.tcb_status = UpToDate;
801        data.platform.tcb_level.advisory_ids = vec![];
802        data.qe.tcb_level.tcb_status = OutOfDate;
803        data.qe.tcb_level.tcb_date = "2023-07-22T00:00:00Z".to_string();
804        data.qe.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()];
805        data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()];
806
807        let policy = QuotePolicy::strict(1_702_000_000)
808            .allow_status(OutOfDate)
809            .qe_grace_period(Duration::from_secs(13_000_000));
810        assert!(policy.validate(&data).is_ok());
811    }
812
813    // -- QuotePolicyConfig --
814
815    #[test]
816    fn policy_config_default_is_strict() {
817        let config: QuotePolicyConfig = serde_json::from_str("{}").unwrap();
818        let policy = config.into_policy(1_702_000_000);
819        assert!(policy.validate(&make_test_claims(UpToDate)).is_ok());
820        let err = policy
821            .validate(&make_test_claims(SWHardeningNeeded))
822            .unwrap_err()
823            .to_string();
824        assert!(err.contains("not acceptable"), "{err}");
825    }
826
827    #[test]
828    fn policy_config_into_policy_applies_fields() {
829        let config: QuotePolicyConfig = serde_json::from_str(
830            r#"{
831                "allowed_statuses": ["UpToDate", "SWHardeningNeeded"],
832                "rejected_advisory_ids": ["INTEL-SA-00334"],
833                "allow_smt": true
834            }"#,
835        )
836        .unwrap();
837        let policy = config.into_policy(1_702_000_000);
838        assert!(policy
839            .validate(&make_test_claims(SWHardeningNeeded))
840            .is_ok());
841
842        let mut data = make_test_claims(UpToDate);
843        data.platform.tcb_level.advisory_ids = vec!["intel-sa-00334".to_string()];
844        let err = policy.validate(&data).unwrap_err().to_string();
845        assert!(err.contains("rejected by policy"), "{err}");
846    }
847
848    #[test]
849    fn policy_config_rejects_unknown_fields() {
850        // A stale or misspelled option in a security policy must fail loudly.
851        let result: Result<QuotePolicyConfig, _> =
852            serde_json::from_str(r#"{"collateral_grace_period_secs": 2592000}"#);
853        assert!(result.is_err());
854    }
855}