fuse_core/
spec.rs

1//! Compliance specification definitions
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::collections::BTreeMap;
6use chrono::{DateTime, Utc};
7
8/// Compliance specification that defines what needs to be verified
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ComplianceSpec {
11    /// Human-readable claim description
12    pub claim: String,
13
14    /// SHA256 hash of the system binary/config being verified
15    pub system_hash: String,
16
17    /// Constraints and parameters for the compliance check
18    pub constraints: BTreeMap<String, String>,
19
20    /// Jurisdiction or regulatory framework (e.g., "US, SEC", "EU, GDPR")
21    pub jurisdiction: String,
22
23    /// Specification version
24    pub version: String,
25
26    /// Expiry date for this specification
27    pub expiry: DateTime<Utc>,
28
29    /// Optional metadata
30    #[serde(default)]
31    pub metadata: BTreeMap<String, String>,
32
33    /// Optional list of top-level JSON fields to disclose in the proof journal.
34    /// Used for selective disclosure (e.g., in C2PA manifest verification).
35    #[serde(default)]
36    pub disclosed_fields: Option<Vec<String>>,
37}
38
39impl ComplianceSpec {
40    /// Create a new compliance specification
41    #[must_use] 
42    pub fn new(
43        claim: String,
44        system_hash: String,
45        constraints: BTreeMap<String, String>,
46        jurisdiction: String,
47        version: String,
48        expiry: DateTime<Utc>,
49    ) -> Self {
50        Self {
51            claim,
52            system_hash,
53            constraints,
54            jurisdiction,
55            version,
56            expiry,
57            metadata: BTreeMap::new(),
58            disclosed_fields: None,
59        }
60    }
61
62    /// Compute the hash of this specification
63    /// Uses `BTreeMap` for deterministic key ordering
64    #[must_use] 
65    pub fn hash(&self) -> String {
66        let json = serde_json::to_string(self).expect("Failed to serialize spec");
67        let mut hasher = Sha256::new();
68        hasher.update(json.as_bytes());
69        hex::encode(hasher.finalize())
70    }
71
72    /// Check if the specification has expired
73    #[must_use] 
74    pub fn is_expired(&self) -> bool {
75        Utc::now() > self.expiry
76    }
77
78    /// Validate the specification
79    pub fn validate(&self) -> crate::Result<()> {
80        if self.claim.is_empty() {
81            return Err(crate::VceError::InvalidSpec("Claim cannot be empty".to_string()));
82        }
83
84        if self.system_hash.is_empty() {
85            return Err(crate::VceError::InvalidSpec(
86                "System hash cannot be empty".to_string(),
87            ));
88        }
89
90        if self.is_expired() {
91            return Err(crate::VceError::SpecExpired(
92                self.expiry.to_rfc3339(),
93            ));
94        }
95
96        Ok(())
97    }
98
99    /// Load a specification from a JSON file
100    pub fn from_json_file(path: &std::path::Path) -> crate::Result<Self> {
101        let content = std::fs::read_to_string(path)?;
102        let spec: Self = serde_json::from_str(&content)?;
103        spec.validate()?;
104        Ok(spec)
105    }
106
107    /// Load a specification from a YAML file
108    pub fn from_yaml_file(path: &std::path::Path) -> crate::Result<Self> {
109        let content = std::fs::read_to_string(path)?;
110        let spec: Self = serde_yaml::from_str(&content)?;
111        spec.validate()?;
112        Ok(spec)
113    }
114
115    /// Save the specification to a JSON file
116    pub fn to_json_file(&self, path: &std::path::Path) -> crate::Result<()> {
117        let json = serde_json::to_string_pretty(self)?;
118        std::fs::write(path, json)?;
119        Ok(())
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_spec_validation() {
129        let spec = ComplianceSpec::new(
130            "SOC2 control X verified".to_string(),
131            "abc123".to_string(),
132            BTreeMap::new(),
133            "US, SEC".to_string(),
134            "1.0".to_string(),
135            Utc::now() + chrono::Duration::days(365),
136        );
137
138        assert!(spec.validate().is_ok());
139    }
140
141    #[test]
142    fn test_spec_expiry() {
143        let spec = ComplianceSpec::new(
144            "Test claim".to_string(),
145            "abc123".to_string(),
146            BTreeMap::new(),
147            "US".to_string(),
148            "1.0".to_string(),
149            Utc::now() - chrono::Duration::days(1),
150        );
151
152        assert!(spec.is_expired());
153        assert!(spec.validate().is_err());
154    }
155}
156