Skip to main content

chio_core/
standards.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5pub const CHIO_PORTABLE_CLAIM_CATALOG_SCHEMA: &str = "chio.portable-claim-catalog.v1";
6pub const CHIO_PORTABLE_IDENTITY_BINDING_SCHEMA: &str = "chio.portable-identity-binding.v1";
7pub const CHIO_GOVERNED_AUTH_BINDING_SCHEMA: &str = "chio.governed-auth-binding.v1";
8pub const CHIO_PORTABLE_SUBJECT_BINDING_DID_CHIO_SUBJECT_KEY_THUMBPRINT: &str =
9    "did:chio-subject-key-thumbprint";
10pub const CHIO_PORTABLE_ISSUER_IDENTITY_HTTPS_JWKS: &str = "https-url+jwks";
11pub const CHIO_PROVENANCE_ANCHOR_DID_CHIO: &str = "did:chio";
12pub const CHIO_GOVERNED_AUTH_AUTHORITATIVE_SOURCE: &str = "metadata.governed_transaction";
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "camelCase")]
16pub struct ChioPortableClaimCatalog {
17    pub schema: String,
18    #[serde(default, skip_serializing_if = "Vec::is_empty")]
19    pub always_disclosed_claims: Vec<String>,
20    #[serde(default, skip_serializing_if = "Vec::is_empty")]
21    pub selectively_disclosable_claims: Vec<String>,
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub optional_claims: Vec<String>,
24    pub status_reference_kind: String,
25    pub unsupported_claims_fail_closed: bool,
26}
27
28impl Default for ChioPortableClaimCatalog {
29    fn default() -> Self {
30        Self {
31            schema: CHIO_PORTABLE_CLAIM_CATALOG_SCHEMA.to_string(),
32            always_disclosed_claims: vec![
33                "iss".to_string(),
34                "sub".to_string(),
35                "vct".to_string(),
36                "cnf".to_string(),
37                "chio_passport_id".to_string(),
38                "chio_subject_did".to_string(),
39                "chio_credential_count".to_string(),
40            ],
41            selectively_disclosable_claims: vec![
42                "chio_issuer_dids".to_string(),
43                "chio_merkle_roots".to_string(),
44                "chio_enterprise_identity_provenance".to_string(),
45            ],
46            optional_claims: vec!["chio_passport_status".to_string()],
47            status_reference_kind: "chio-passport-status-distribution".to_string(),
48            unsupported_claims_fail_closed: true,
49        }
50    }
51}
52
53impl ChioPortableClaimCatalog {
54    pub fn validate(&self) -> Result<(), String> {
55        if self.schema != CHIO_PORTABLE_CLAIM_CATALOG_SCHEMA {
56            return Err(format!(
57                "portable claim catalog schema must be `{CHIO_PORTABLE_CLAIM_CATALOG_SCHEMA}`"
58            ));
59        }
60        ensure_string_list(
61            "portable claim catalog always_disclosed_claims",
62            &self.always_disclosed_claims,
63        )?;
64        ensure_string_list(
65            "portable claim catalog selectively_disclosable_claims",
66            &self.selectively_disclosable_claims,
67        )?;
68        ensure_string_list(
69            "portable claim catalog optional_claims",
70            &self.optional_claims,
71        )?;
72        if self.status_reference_kind.trim().is_empty() {
73            return Err(
74                "portable claim catalog status_reference_kind must not be empty".to_string(),
75            );
76        }
77        Ok(())
78    }
79
80    #[must_use]
81    pub fn supports_selective_disclosure(&self, claim: &str) -> bool {
82        self.selectively_disclosable_claims
83            .iter()
84            .any(|value| value == claim)
85    }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89#[serde(rename_all = "camelCase")]
90pub struct ChioPortableIdentityBinding {
91    pub schema: String,
92    pub subject_binding: String,
93    pub portable_subject_claim: String,
94    pub subject_confirmation_claim: String,
95    pub chio_subject_provenance_claim: String,
96    pub issuer_identity: String,
97    pub portable_issuer_claim: String,
98    pub chio_issuer_provenance_claim: String,
99    pub enterprise_provenance_claim: String,
100    pub chio_provenance_anchor: String,
101    pub unsupported_mappings_fail_closed: bool,
102}
103
104impl Default for ChioPortableIdentityBinding {
105    fn default() -> Self {
106        Self {
107            schema: CHIO_PORTABLE_IDENTITY_BINDING_SCHEMA.to_string(),
108            subject_binding: CHIO_PORTABLE_SUBJECT_BINDING_DID_CHIO_SUBJECT_KEY_THUMBPRINT
109                .to_string(),
110            portable_subject_claim: "sub".to_string(),
111            subject_confirmation_claim: "cnf.jwk".to_string(),
112            chio_subject_provenance_claim: "chio_subject_did".to_string(),
113            issuer_identity: CHIO_PORTABLE_ISSUER_IDENTITY_HTTPS_JWKS.to_string(),
114            portable_issuer_claim: "iss".to_string(),
115            chio_issuer_provenance_claim: "chio_issuer_dids".to_string(),
116            enterprise_provenance_claim: "chio_enterprise_identity_provenance".to_string(),
117            chio_provenance_anchor: CHIO_PROVENANCE_ANCHOR_DID_CHIO.to_string(),
118            unsupported_mappings_fail_closed: true,
119        }
120    }
121}
122
123impl ChioPortableIdentityBinding {
124    pub fn validate(&self) -> Result<(), String> {
125        if self.schema != CHIO_PORTABLE_IDENTITY_BINDING_SCHEMA {
126            return Err(format!(
127                "portable identity binding schema must be `{CHIO_PORTABLE_IDENTITY_BINDING_SCHEMA}`"
128            ));
129        }
130        ensure_non_empty(
131            "portable identity binding subject_binding",
132            &self.subject_binding,
133        )?;
134        ensure_non_empty(
135            "portable identity binding portable_subject_claim",
136            &self.portable_subject_claim,
137        )?;
138        ensure_non_empty(
139            "portable identity binding subject_confirmation_claim",
140            &self.subject_confirmation_claim,
141        )?;
142        ensure_non_empty(
143            "portable identity binding chio_subject_provenance_claim",
144            &self.chio_subject_provenance_claim,
145        )?;
146        ensure_non_empty(
147            "portable identity binding issuer_identity",
148            &self.issuer_identity,
149        )?;
150        ensure_non_empty(
151            "portable identity binding portable_issuer_claim",
152            &self.portable_issuer_claim,
153        )?;
154        ensure_non_empty(
155            "portable identity binding chio_issuer_provenance_claim",
156            &self.chio_issuer_provenance_claim,
157        )?;
158        ensure_non_empty(
159            "portable identity binding enterprise_provenance_claim",
160            &self.enterprise_provenance_claim,
161        )?;
162        ensure_non_empty(
163            "portable identity binding chio_provenance_anchor",
164            &self.chio_provenance_anchor,
165        )?;
166        Ok(())
167    }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171#[serde(rename_all = "camelCase")]
172pub struct ChioGovernedAuthorizationBinding {
173    pub schema: String,
174    pub authoritative_source: String,
175    #[serde(default, skip_serializing_if = "Vec::is_empty")]
176    pub intent_binding_fields: Vec<String>,
177    #[serde(default, skip_serializing_if = "Vec::is_empty")]
178    pub approval_binding_fields: Vec<String>,
179    #[serde(default, skip_serializing_if = "Vec::is_empty")]
180    pub subject_binding_fields: Vec<String>,
181    #[serde(default, skip_serializing_if = "Vec::is_empty")]
182    pub issuer_binding_fields: Vec<String>,
183    #[serde(default, skip_serializing_if = "Vec::is_empty")]
184    pub runtime_assurance_binding_fields: Vec<String>,
185    pub delegated_call_chain_field: String,
186    pub unsupported_mappings_fail_closed: bool,
187}
188
189impl Default for ChioGovernedAuthorizationBinding {
190    fn default() -> Self {
191        Self {
192            schema: CHIO_GOVERNED_AUTH_BINDING_SCHEMA.to_string(),
193            authoritative_source: CHIO_GOVERNED_AUTH_AUTHORITATIVE_SOURCE.to_string(),
194            intent_binding_fields: vec!["intentId".to_string(), "intentHash".to_string()],
195            approval_binding_fields: vec![
196                "approvalTokenId".to_string(),
197                "approvalApproved".to_string(),
198                "approverKey".to_string(),
199            ],
200            subject_binding_fields: vec!["subjectKey".to_string(), "subjectKeySource".to_string()],
201            issuer_binding_fields: vec!["issuerKey".to_string(), "issuerKeySource".to_string()],
202            runtime_assurance_binding_fields: vec![
203                "runtimeAssuranceTier".to_string(),
204                "runtimeAssuranceVerifier".to_string(),
205                "runtimeAssuranceEvidenceSha256".to_string(),
206            ],
207            delegated_call_chain_field: "callChain".to_string(),
208            unsupported_mappings_fail_closed: true,
209        }
210    }
211}
212
213impl ChioGovernedAuthorizationBinding {
214    pub fn validate(&self) -> Result<(), String> {
215        if self.schema != CHIO_GOVERNED_AUTH_BINDING_SCHEMA {
216            return Err(format!(
217                "governed authorization binding schema must be `{CHIO_GOVERNED_AUTH_BINDING_SCHEMA}`"
218            ));
219        }
220        ensure_non_empty(
221            "governed authorization binding authoritative_source",
222            &self.authoritative_source,
223        )?;
224        ensure_string_list(
225            "governed authorization binding intent_binding_fields",
226            &self.intent_binding_fields,
227        )?;
228        ensure_string_list(
229            "governed authorization binding approval_binding_fields",
230            &self.approval_binding_fields,
231        )?;
232        ensure_string_list(
233            "governed authorization binding subject_binding_fields",
234            &self.subject_binding_fields,
235        )?;
236        ensure_string_list(
237            "governed authorization binding issuer_binding_fields",
238            &self.issuer_binding_fields,
239        )?;
240        ensure_string_list(
241            "governed authorization binding runtime_assurance_binding_fields",
242            &self.runtime_assurance_binding_fields,
243        )?;
244        ensure_non_empty(
245            "governed authorization binding delegated_call_chain_field",
246            &self.delegated_call_chain_field,
247        )?;
248        Ok(())
249    }
250}
251
252fn ensure_non_empty(label: &str, value: &str) -> Result<(), String> {
253    if value.trim().is_empty() {
254        return Err(format!("{label} must not be empty"));
255    }
256    Ok(())
257}
258
259fn ensure_string_list(label: &str, values: &[String]) -> Result<(), String> {
260    if values.is_empty() {
261        return Err(format!("{label} must not be empty"));
262    }
263    let mut seen = BTreeSet::new();
264    for value in values {
265        ensure_non_empty(label, value)?;
266        if !seen.insert(value) {
267            return Err(format!("{label} must not repeat `{value}`"));
268        }
269    }
270    Ok(())
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn portable_claim_catalog_defaults_and_validation_guards_hold() {
279        let catalog = ChioPortableClaimCatalog::default();
280        catalog.validate().expect("default portable claim catalog");
281        assert!(catalog.supports_selective_disclosure("chio_issuer_dids"));
282        assert!(!catalog.supports_selective_disclosure("unknown_claim"));
283
284        let mut invalid = catalog.clone();
285        invalid.schema = "chio.portable-claim-catalog.v9".to_string();
286        assert!(invalid.validate().is_err());
287
288        let mut invalid = catalog.clone();
289        invalid.status_reference_kind = " ".to_string();
290        assert!(invalid.validate().is_err());
291
292        let mut invalid = catalog;
293        invalid.always_disclosed_claims = vec!["iss".to_string(), "iss".to_string()];
294        assert!(invalid.validate().is_err());
295    }
296
297    #[test]
298    fn portable_identity_binding_validation_rejects_schema_and_empty_fields() {
299        let binding = ChioPortableIdentityBinding::default();
300        binding
301            .validate()
302            .expect("default portable identity binding");
303
304        let mut invalid = binding.clone();
305        invalid.schema = "chio.portable-identity-binding.v9".to_string();
306        assert!(invalid.validate().is_err());
307
308        let mut invalid = binding.clone();
309        invalid.subject_binding.clear();
310        assert!(invalid.validate().is_err());
311
312        let mut invalid = binding;
313        invalid.chio_provenance_anchor = " ".to_string();
314        assert!(invalid.validate().is_err());
315    }
316
317    #[test]
318    fn governed_authorization_binding_validation_rejects_schema_and_list_errors() {
319        let binding = ChioGovernedAuthorizationBinding::default();
320        binding
321            .validate()
322            .expect("default governed authorization binding");
323
324        let mut invalid = binding.clone();
325        invalid.schema = "chio.governed-auth-binding.v9".to_string();
326        assert!(invalid.validate().is_err());
327
328        let mut invalid = binding.clone();
329        invalid.intent_binding_fields.clear();
330        assert!(invalid.validate().is_err());
331
332        let mut invalid = binding.clone();
333        invalid.approval_binding_fields =
334            vec!["approvalTokenId".to_string(), "approvalTokenId".to_string()];
335        assert!(invalid.validate().is_err());
336
337        let mut invalid = binding;
338        invalid.delegated_call_chain_field = " ".to_string();
339        assert!(invalid.validate().is_err());
340    }
341}