1use crate::verifiable_credential::Credential;
29use crate::webizen::SlgOpcode;
30
31pub const MAX_IDENTITY_BINDINGS: usize = 32;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum CryptoScheme {
39 Ed25519,
40 MlDsa65,
41 Blake3Commitment,
42 X25519,
43 Unknown,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct IdentifierBinding {
52 pub identifier: u64,
54 pub scheme: CryptoScheme,
55 pub attested: bool,
56 pub confidence: f32,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq)]
61pub enum IdentityValidation {
62 Valid {
65 distinct: u16,
66 attested: u16,
67 aggregate_confidence: f32,
68 },
69 Underdetermined { distinct: u16, attested: u16 },
71 DefinitiveCollapse,
75}
76
77pub fn validate_enumerated_identity(
83 bindings: &[IdentifierBinding],
84 min_distinct: u16,
85 min_attested: u16,
86) -> IdentityValidation {
87 let mut distinct = 0u16;
88 let mut attested = 0u16;
89 let mut not_prob = 1.0f32; for (i, b) in bindings.iter().enumerate() {
92 if b.confidence >= 1.0 {
94 return IdentityValidation::DefinitiveCollapse;
95 }
96 let first_seen = !bindings[..i].iter().any(|p| p.identifier == b.identifier);
98 if first_seen {
99 distinct += 1;
100 }
101 if b.attested && b.scheme != CryptoScheme::Unknown {
103 attested += 1;
104 not_prob *= 1.0 - b.confidence.clamp(0.0, 1.0);
105 }
106 }
107
108 if distinct < min_distinct || attested < min_attested {
109 return IdentityValidation::Underdetermined { distinct, attested };
110 }
111 IdentityValidation::Valid {
112 distinct,
113 attested,
114 aggregate_confidence: 1.0 - not_prob,
115 }
116}
117
118pub fn enumerated_identity_opcodes(
122 min_distinct: u32,
123 min_attested: u32,
124 out: &mut [SlgOpcode],
125) -> usize {
126 let ops = [
127 SlgOpcode::CheckMinCount(min_distinct),
128 SlgOpcode::CheckMinCount(min_attested),
129 ];
130 let n = ops.len().min(out.len());
131 out[..n].copy_from_slice(&ops[..n]);
132 n
133}
134
135pub const MAX_SHAPE_ROUTES: usize = 256;
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct ShapeRoute {
145 pub shape: u64,
146 pub locus: u64,
147}
148
149pub fn shapes_for_locus(routes: &[ShapeRoute], locus: u64, out: &mut [u64]) -> usize {
152 let mut n = 0usize;
153 for r in routes {
154 if r.locus != locus || out[..n].contains(&r.shape) {
155 continue;
156 }
157 if n >= out.len() {
158 break;
159 }
160 out[n] = r.shape;
161 n += 1;
162 }
163 n
164}
165
166pub fn loci_for_shape(routes: &[ShapeRoute], shape: u64, out: &mut [u64]) -> usize {
169 let mut n = 0usize;
170 for r in routes {
171 if r.shape != shape || out[..n].contains(&r.locus) {
172 continue;
173 }
174 if n >= out.len() {
175 break;
176 }
177 out[n] = r.locus;
178 n += 1;
179 }
180 n
181}
182
183pub fn route_is_local(routes: &[ShapeRoute], shape: u64, self_locus: u64) -> bool {
185 routes
186 .iter()
187 .any(|r| r.shape == shape && r.locus == self_locus)
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
194pub enum ShaclSeverity {
195 Info,
196 Warning,
197 Violation,
198 Critical,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum OperationMode {
205 Online,
206 OffGrid,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub struct ShapeViolation {
212 pub shape: u64,
213 pub focus_node: u64,
214 pub severity: ShaclSeverity,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub struct DegradationOutcome {
220 pub blocking: u16,
222 pub degraded: u16,
224 pub subgraph_usable: bool,
226}
227
228pub fn degrade_violations(
235 violations: &[ShapeViolation],
236 mode: OperationMode,
237 out: &mut [ShapeViolation],
238) -> DegradationOutcome {
239 let mut blocking = 0u16;
240 let mut degraded = 0u16;
241 let count = violations.len().min(out.len());
242
243 for (i, v) in violations.iter().take(count).enumerate() {
244 let mut adjusted = *v;
245 let blocks = match mode {
246 OperationMode::Online => v.severity >= ShaclSeverity::Violation,
247 OperationMode::OffGrid => {
248 if v.severity == ShaclSeverity::Critical {
249 true
250 } else {
251 if v.severity >= ShaclSeverity::Violation {
253 adjusted.severity = ShaclSeverity::Warning;
254 degraded += 1;
255 }
256 false
257 }
258 }
259 };
260 if blocks {
261 blocking += 1;
262 }
263 out[i] = adjusted;
264 }
265
266 DegradationOutcome {
267 blocking,
268 degraded,
269 subgraph_usable: blocking == 0,
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub struct CredentialGate {
279 pub shape: u64,
281 pub required_claim_predicate: u64,
283 pub required_claim_object: u64,
285 pub accepted_issuer: u64,
287}
288
289pub fn credential_gates_target(gate: &CredentialGate, focus_node: u64, vc: &Credential) -> bool {
298 if vc.subject != focus_node {
299 return false;
300 }
301 if gate.accepted_issuer != 0 && vc.issuer != gate.accepted_issuer {
302 return false;
303 }
304 vc.claims.iter().any(|q| {
305 q.predicate == gate.required_claim_predicate
306 && (gate.required_claim_object == 0 || q.object == gate.required_claim_object)
307 })
308}
309
310pub fn get_identity_shacl_ttl() -> &'static str {
314 r#"
315@prefix q42: <https://webizen.org/q42#> .
316@prefix sh: <http://www.w3.org/ns/shacl#> .
317@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
318
319# Identity is an ENUMERATED state over multiple cryptographically-attested
320# identifiers — never a single definitive identifier. The out-of-band remainder is
321# preserved: identifier confidence is a RELATION strictly in (0,1), never certainty.
322q42:EnumeratedIdentityShape a sh:NodeShape ;
323 sh:targetClass q42:Principal ;
324 sh:property [
325 sh:path q42:hasIdentifier ;
326 sh:minCount 2 ;
327 sh:message "An identity must enumerate at least two distinct identifiers; a single definitive identifier collapses the out-of-band remainder." ;
328 ] ;
329 sh:property [
330 sh:path q42:identifierAttestation ;
331 sh:minCount 1 ;
332 sh:nodeKind sh:BlankNodeOrIRI ;
333 sh:message "Each identifier must carry a cryptographic attestation (Ed25519 / ML-DSA-65 / BLAKE3 commitment)." ;
334 ] ;
335 sh:property [
336 sh:path q42:identifierConfidence ;
337 sh:datatype xsd:decimal ;
338 sh:minExclusive 0 ;
339 sh:maxExclusive 1 ;
340 sh:message "Identifier confidence is strictly in (0,1); certainty (1.0) is a definitive-collapse and is rejected." ;
341 ] .
342
343# Decentralized routing: a shape is bound to the locus where its targets live, so
344# validation is dispatched locally rather than aggregating personal data centrally.
345q42:ShapeRouteShape a sh:NodeShape ;
346 sh:targetClass q42:ShapeRoute ;
347 sh:property [
348 sh:path q42:routedToLocus ;
349 sh:minCount 1 ;
350 sh:message "Every shape route must name a storage locus; validation goes to the data, not the data to a central index." ;
351 ] .
352
353# A SHACL target gated by a presented, verified Verifiable Credential.
354q42:CredentialGatedTargetShape a sh:NodeShape ;
355 sh:targetSubjectsOf q42:presentsCredential ;
356 sh:property [
357 sh:path q42:presentsCredential ;
358 sh:minCount 1 ;
359 sh:message "This target requires a presented, verified Verifiable Credential." ;
360 ] .
361"#
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::verifiable_credential::Credential;
368 use crate::{q_hash, NQuin};
369
370 fn binding(
371 id: &str,
372 scheme: CryptoScheme,
373 attested: bool,
374 confidence: f32,
375 ) -> IdentifierBinding {
376 IdentifierBinding {
377 identifier: q_hash(id),
378 scheme,
379 attested,
380 confidence,
381 }
382 }
383
384 #[test]
385 fn enumerated_identity_valid_stays_below_certainty() {
386 let bindings = [
387 binding("did:key:a", CryptoScheme::Ed25519, true, 0.6),
388 binding("did:key:b", CryptoScheme::MlDsa65, true, 0.7),
389 binding("handle:c", CryptoScheme::Blake3Commitment, true, 0.5),
390 ];
391 match validate_enumerated_identity(&bindings, 2, 2) {
392 IdentityValidation::Valid {
393 distinct,
394 attested,
395 aggregate_confidence,
396 } => {
397 assert_eq!(distinct, 3);
398 assert_eq!(attested, 3);
399 assert!(aggregate_confidence > 0.9 && aggregate_confidence < 1.0);
401 }
402 other => panic!("expected Valid, got {other:?}"),
403 }
404 }
405
406 #[test]
407 fn enumerated_identity_underdetermined_when_too_few() {
408 let bindings = [
410 binding("did:key:a", CryptoScheme::Ed25519, true, 0.6),
411 binding("guess:b", CryptoScheme::Unknown, true, 0.4),
412 ];
413 assert_eq!(
414 validate_enumerated_identity(&bindings, 2, 2),
415 IdentityValidation::Underdetermined {
416 distinct: 2,
417 attested: 1
418 }
419 );
420 }
421
422 #[test]
423 fn certainty_is_a_definitive_collapse() {
424 let bindings = [
427 binding("did:key:a", CryptoScheme::Ed25519, true, 0.6),
428 binding("gov:id", CryptoScheme::MlDsa65, true, 1.0),
429 ];
430 assert_eq!(
431 validate_enumerated_identity(&bindings, 1, 1),
432 IdentityValidation::DefinitiveCollapse
433 );
434 }
435
436 #[test]
437 fn decentralized_routing_dispatches_to_loci() {
438 let routes = [
439 ShapeRoute {
440 shape: 1,
441 locus: 100,
442 },
443 ShapeRoute {
444 shape: 2,
445 locus: 100,
446 },
447 ShapeRoute {
448 shape: 1,
449 locus: 200,
450 },
451 ShapeRoute {
452 shape: 1,
453 locus: 100,
454 }, ];
456 let mut shapes = [0u64; 8];
457 let n = shapes_for_locus(&routes, 100, &mut shapes);
458 assert_eq!(n, 2);
459 assert!(shapes[..n].contains(&1) && shapes[..n].contains(&2));
460
461 let mut loci = [0u64; 8];
462 let m = loci_for_shape(&routes, 1, &mut loci);
463 assert_eq!(m, 2); assert!(loci[..m].contains(&100) && loci[..m].contains(&200));
465
466 assert!(route_is_local(&routes, 1, 100));
467 assert!(!route_is_local(&routes, 2, 200));
468 }
469
470 #[test]
471 fn severity_degradation_offgrid_keeps_partial_subgraph_usable() {
472 let violations = [
473 ShapeViolation {
474 shape: 1,
475 focus_node: 10,
476 severity: ShaclSeverity::Violation,
477 },
478 ShapeViolation {
479 shape: 2,
480 focus_node: 11,
481 severity: ShaclSeverity::Warning,
482 },
483 ];
484 let mut out = [violations[0]; 2];
485
486 let online = degrade_violations(&violations, OperationMode::Online, &mut out);
488 assert_eq!(online.blocking, 1);
489 assert!(!online.subgraph_usable);
490
491 let offgrid = degrade_violations(&violations, OperationMode::OffGrid, &mut out);
493 assert_eq!(offgrid.blocking, 0);
494 assert_eq!(offgrid.degraded, 1);
495 assert!(offgrid.subgraph_usable);
496 assert_eq!(out[0].severity, ShaclSeverity::Warning); }
498
499 #[test]
500 fn critical_never_degrades_even_offgrid() {
501 let violations = [ShapeViolation {
502 shape: 9,
503 focus_node: 99,
504 severity: ShaclSeverity::Critical,
505 }];
506 let mut out = [violations[0]; 1];
507 let outcome = degrade_violations(&violations, OperationMode::OffGrid, &mut out);
508 assert_eq!(outcome.blocking, 1);
509 assert_eq!(outcome.degraded, 0);
510 assert!(!outcome.subgraph_usable); assert_eq!(out[0].severity, ShaclSeverity::Critical);
512 }
513
514 #[test]
515 fn verifiable_credential_gates_the_target() {
516 let alice = q_hash("did:example:alice");
517 let issuer = q_hash("did:example:gov");
518 let cap_pred = q_hash("https://ns.webcivics.net/capability/heldBy");
519 let cap_obj = q_hash("cap:LicensedPractitioner");
520
521 let claim = NQuin {
522 subject: alice,
523 predicate: cap_pred,
524 object: cap_obj,
525 context: 0,
526 metadata: 0,
527 parity: 0,
528 };
529 let vc = Credential {
530 issuer,
531 subject: alice,
532 issued_at: 1000,
533 valid_until: 0,
534 claims: vec![claim],
535 };
536 let gate = CredentialGate {
537 shape: 1,
538 required_claim_predicate: cap_pred,
539 required_claim_object: cap_obj,
540 accepted_issuer: issuer,
541 };
542
543 assert!(credential_gates_target(&gate, alice, &vc));
545 assert!(!credential_gates_target(
547 &gate,
548 q_hash("did:example:bob"),
549 &vc
550 ));
551 let gate_other_issuer = CredentialGate {
553 accepted_issuer: q_hash("did:example:rogue"),
554 ..gate
555 };
556 assert!(!credential_gates_target(&gate_other_issuer, alice, &vc));
557 let gate_other_claim = CredentialGate {
559 required_claim_object: q_hash("cap:Something_Else"),
560 ..gate
561 };
562 assert!(!credential_gates_target(&gate_other_claim, alice, &vc));
563 let gate_any_issuer = CredentialGate {
565 accepted_issuer: 0,
566 ..gate
567 };
568 assert!(credential_gates_target(&gate_any_issuer, alice, &vc));
569 }
570}