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}