1#[cfg(not(target_arch = "wasm32"))]
13use crate::key_vault::{KeyVault, SubgraphKey, SubgraphLayer};
14use crate::q_hash;
15
16pub mod vc_roles {
20 use crate::q_hash;
21
22 pub const AUTHENTICATED: u64 = q_hash("urn:qualia:role:authenticated");
24 pub const PROFESSIONAL: u64 = q_hash("urn:qualia:role:professional");
26 pub const LEGAL_PRACTITIONER: u64 = q_hash("urn:qualia:role:legal-practitioner");
28 pub const MEDICAL_PROFESSIONAL: u64 = q_hash("urn:qualia:role:medical-professional");
30 pub const FIDUCIARY: u64 = q_hash("urn:qualia:role:fiduciary");
32 pub const NODE_OPERATOR: u64 = q_hash("urn:qualia:role:node-operator");
34}
35
36const P_HAS_ROLE: u64 = q_hash("urn:qualia:vc:hasRole");
38const P_CLEARANCE_LEVEL: u64 = q_hash("urn:qualia:vc:clearanceLevel");
40const P_ISSUED_BY: u64 = q_hash("urn:qualia:vc:issuedBy");
42
43const MAX_ROLES: usize = 8;
45
46#[derive(Debug, Clone, Copy)]
50pub struct VcAttributes {
51 pub did_hash: u64,
52 pub roles: [u64; MAX_ROLES],
53 pub role_count: u8,
54 pub clearance_level: u8,
56 pub credential_issuer: u64,
58}
59
60impl VcAttributes {
61 pub fn unauthenticated(did_hash: u64) -> Self {
63 Self {
64 did_hash,
65 roles: [0; MAX_ROLES],
66 role_count: 0,
67 clearance_level: 0,
68 credential_issuer: 0,
69 }
70 }
71
72 pub fn from_quins(agent_did: u64, quins: &[crate::NQuin]) -> Self {
77 let mut attrs = Self::unauthenticated(agent_did);
78 for q in quins {
79 if q.subject != agent_did {
80 continue;
81 }
82 if q.predicate == P_HAS_ROLE && (attrs.role_count as usize) < MAX_ROLES {
83 attrs.roles[attrs.role_count as usize] = q.object;
84 attrs.role_count += 1;
85 } else if q.predicate == P_CLEARANCE_LEVEL {
86 attrs.clearance_level = (q.object & 0x0F) as u8;
87 } else if q.predicate == P_ISSUED_BY {
88 attrs.credential_issuer = q.object;
89 }
90 }
91 attrs
92 }
93
94 #[inline]
96 pub fn has_role(self, role_hash: u64) -> bool {
97 self.roles[..self.role_count as usize].contains(&role_hash)
98 }
99}
100
101#[cfg(not(target_arch = "wasm32"))]
102#[derive(Debug)]
104pub enum DeonticResult {
105 KeyRelease(SubgraphKey),
107 AccessDenied {
109 layer: SubgraphLayer,
110 reason: &'static str,
111 },
112}
113
114#[cfg(not(target_arch = "wasm32"))]
115impl DeonticResult {
116 pub fn is_permitted(&self) -> bool {
117 matches!(self, Self::KeyRelease(_))
118 }
119}
120
121#[cfg(not(target_arch = "wasm32"))]
140pub fn evaluate_vc_for_subgraph_key_release(
141 vault: &KeyVault,
142 vc: &VcAttributes,
143 layer: SubgraphLayer,
144 trusted_issuers: &[u64],
145) -> DeonticResult {
146 if !trusted_issuers.is_empty() && !trusted_issuers.contains(&vc.credential_issuer) {
148 return DeonticResult::AccessDenied {
149 layer,
150 reason: "credential issuer not in trusted-issuer set",
151 };
152 }
153
154 let policy_passed = match layer {
156 SubgraphLayer::Public => true,
157
158 SubgraphLayer::Professional => {
159 vc.clearance_level >= 1
160 || vc.has_role(vc_roles::PROFESSIONAL)
161 || vc.has_role(vc_roles::NODE_OPERATOR)
162 }
163
164 SubgraphLayer::Legal => {
165 vc.clearance_level >= 2 || vc.has_role(vc_roles::LEGAL_PRACTITIONER)
166 }
167
168 SubgraphLayer::Medical => {
169 vc.clearance_level >= 3 || vc.has_role(vc_roles::MEDICAL_PROFESSIONAL)
170 }
171
172 SubgraphLayer::Fiduciary => vc.clearance_level >= 4 || vc.has_role(vc_roles::FIDUCIARY),
173 };
174
175 if policy_passed {
176 DeonticResult::KeyRelease(vault.generate_layer_key(layer))
177 } else {
178 DeonticResult::AccessDenied {
179 layer,
180 reason: "insufficient VC clearance or role",
181 }
182 }
183}
184
185#[cfg(not(target_arch = "wasm32"))]
193pub fn evaluate_accessible_layers(
194 vault: &KeyVault,
195 vc: &VcAttributes,
196 trusted_issuers: &[u64],
197 out: &mut [Option<(SubgraphLayer, SubgraphKey)>],
198) -> usize {
199 let layers = [
200 SubgraphLayer::Public,
201 SubgraphLayer::Professional,
202 SubgraphLayer::Legal,
203 SubgraphLayer::Medical,
204 SubgraphLayer::Fiduciary,
205 ];
206 let mut n = 0usize;
207 for layer in layers {
208 if let DeonticResult::KeyRelease(key) =
209 evaluate_vc_for_subgraph_key_release(vault, vc, layer, trusted_issuers)
210 {
211 if n >= out.len() {
212 break;
213 }
214 out[n] = Some((layer, key));
215 n += 1;
216 }
217 }
218 n
219}
220
221pub fn write_vc_claim_quins(
227 agent_did: u64,
228 roles: &[u64],
229 clearance: u8,
230 issuer_did: u64,
231 ts: u64,
232) -> [crate::NQuin; 3] {
233 const VC_CONTEXT: u64 = q_hash("urn:qualia:context:vc");
234
235 let role_hash = if roles.is_empty() {
236 vc_roles::AUTHENTICATED
237 } else {
238 roles[0]
239 };
240
241 [
242 crate::NQuin {
243 subject: agent_did,
244 predicate: P_HAS_ROLE,
245 object: role_hash,
246 context: VC_CONTEXT,
247 metadata: ts & 0xFFFF_FFFF,
248 parity: agent_did ^ P_HAS_ROLE ^ role_hash ^ VC_CONTEXT,
249 },
250 crate::NQuin {
251 subject: agent_did,
252 predicate: P_CLEARANCE_LEVEL,
253 object: clearance as u64,
254 context: VC_CONTEXT,
255 metadata: ts & 0xFFFF_FFFF,
256 parity: agent_did ^ P_CLEARANCE_LEVEL ^ (clearance as u64) ^ VC_CONTEXT,
257 },
258 crate::NQuin {
259 subject: agent_did,
260 predicate: P_ISSUED_BY,
261 object: issuer_did,
262 context: VC_CONTEXT,
263 metadata: ts & 0xFFFF_FFFF,
264 parity: agent_did ^ P_ISSUED_BY ^ issuer_did ^ VC_CONTEXT,
265 },
266 ]
267}
268
269use crate::modalities::logic::deontic::{DEFEATER_BIT, OP_FORBID, OP_OBLIGATE, OP_PERMIT};
270use crate::modalities::logic::n3_compiler::CompiledRule;
271use crate::modalities::logic::n3_parser::RuleType;
272
273pub fn compile_n3_rule_to_norm(
284 rule: &CompiledRule,
285 contract_hash: u64,
286 expiry_unix32: u32,
287) -> Option<crate::NQuin> {
288 if rule.premise.len == 0 || rule.conclusion.len == 0 {
289 return None;
290 }
291
292 let premise_triple = &rule.premise.triples[0];
293 let conclusion_triple = &rule.conclusion.triples[0];
294
295 let party_did_hash = premise_triple.subject.as_u64();
296 let property_path_hash = premise_triple.predicate.as_u64();
297 let action_object_hash = premise_triple.object.as_u64();
298
299 let mapped_contract_hash = if contract_hash == 0 {
300 conclusion_triple.subject.as_u64()
301 } else {
302 contract_hash
303 };
304
305 let mut opcode = OP_PERMIT;
306 let mut is_defeater = false;
307
308 let p_hash = property_path_hash;
309
310 let is_obligate = [
311 crate::q_hash("q42:obligate"),
312 crate::q_hash("q42:must"),
313 crate::q_hash("q42:shall"),
314 ]
315 .contains(&p_hash);
316 let is_permit = [
317 crate::q_hash("q42:permit"),
318 crate::q_hash("q42:may"),
319 crate::q_hash("q42:can"),
320 ]
321 .contains(&p_hash);
322 let is_forbid = [
323 crate::q_hash("q42:forbid"),
324 crate::q_hash("q42:not"),
325 crate::q_hash("q42:prohibit"),
326 ]
327 .contains(&p_hash);
328
329 match rule.rule_type {
330 RuleType::Strict => {
331 if is_obligate {
332 opcode = OP_OBLIGATE;
333 }
334 }
335 RuleType::Defeasible => {
336 if is_permit {
337 opcode = OP_PERMIT;
338 } else if is_forbid {
339 opcode = OP_FORBID;
340 }
341 }
342 RuleType::Defeater => {
343 opcode = OP_PERMIT;
344 is_defeater = true;
345 }
346 RuleType::Linear => {
347 if is_obligate {
348 opcode = OP_OBLIGATE;
349 }
350 }
351 }
352
353 let mut quin = crate::NQuin::default();
354 quin.subject = party_did_hash;
355
356 let mut predicate_packed =
358 ((property_path_hash & 0x007F_FFFF_FFFF_FFFF) << 8) | (opcode as u64);
359 if is_defeater {
360 predicate_packed |= DEFEATER_BIT;
361 }
362 quin.predicate = predicate_packed;
363
364 quin.object = action_object_hash;
365 quin.context = mapped_contract_hash;
366 quin.metadata = expiry_unix32 as u64;
367 quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
368
369 Some(quin)
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 fn test_vault() -> KeyVault {
377 let tmp = tempfile::tempdir().expect("tmpdir");
378 KeyVault::load_or_generate(tmp.path().to_str().unwrap()).expect("vault")
379 }
380
381 const ISSUER: u64 = 0x1550_0000_0000_CAFE;
382 const AGENT: u64 = 0xA6E4_7777_0000_0001;
383
384 fn vc_with_role(role: u64, clearance: u8) -> VcAttributes {
385 let mut vc = VcAttributes::unauthenticated(AGENT);
386 vc.roles[0] = role;
387 vc.role_count = 1;
388 vc.clearance_level = clearance;
389 vc.credential_issuer = ISSUER;
390 vc
391 }
392
393 #[test]
394 fn public_layer_always_accessible() {
395 let vault = test_vault();
396 let vc = VcAttributes::unauthenticated(AGENT);
397 let result = evaluate_vc_for_subgraph_key_release(&vault, &vc, SubgraphLayer::Public, &[]);
398 assert!(result.is_permitted());
399 }
400
401 #[test]
402 fn professional_layer_requires_role_or_clearance() {
403 let vault = test_vault();
404
405 let no_vc = VcAttributes::unauthenticated(AGENT);
407 assert!(!evaluate_vc_for_subgraph_key_release(
408 &vault,
409 &no_vc,
410 SubgraphLayer::Professional,
411 &[]
412 )
413 .is_permitted());
414
415 let pro = vc_with_role(vc_roles::PROFESSIONAL, 0);
417 assert!(evaluate_vc_for_subgraph_key_release(
418 &vault,
419 &pro,
420 SubgraphLayer::Professional,
421 &[]
422 )
423 .is_permitted());
424
425 let cleared = vc_with_role(0, 1);
427 assert!(evaluate_vc_for_subgraph_key_release(
428 &vault,
429 &cleared,
430 SubgraphLayer::Professional,
431 &[]
432 )
433 .is_permitted());
434 }
435
436 #[test]
437 fn medical_layer_requires_role_or_clearance_3() {
438 let vault = test_vault();
439
440 let low = vc_with_role(vc_roles::PROFESSIONAL, 0);
441 assert!(
442 !evaluate_vc_for_subgraph_key_release(&vault, &low, SubgraphLayer::Medical, &[])
443 .is_permitted()
444 );
445
446 let med_role = vc_with_role(vc_roles::MEDICAL_PROFESSIONAL, 0);
447 assert!(evaluate_vc_for_subgraph_key_release(
448 &vault,
449 &med_role,
450 SubgraphLayer::Medical,
451 &[]
452 )
453 .is_permitted());
454
455 let cleared = vc_with_role(0, 3);
456 assert!(evaluate_vc_for_subgraph_key_release(
457 &vault,
458 &cleared,
459 SubgraphLayer::Medical,
460 &[]
461 )
462 .is_permitted());
463 }
464
465 #[test]
466 fn fiduciary_layer_strictest() {
467 let vault = test_vault();
468
469 let med = vc_with_role(vc_roles::MEDICAL_PROFESSIONAL, 3);
470 assert!(
471 !evaluate_vc_for_subgraph_key_release(&vault, &med, SubgraphLayer::Fiduciary, &[])
472 .is_permitted()
473 );
474
475 let fid = vc_with_role(vc_roles::FIDUCIARY, 0);
476 assert!(
477 evaluate_vc_for_subgraph_key_release(&vault, &fid, SubgraphLayer::Fiduciary, &[])
478 .is_permitted()
479 );
480 }
481
482 #[test]
483 fn trusted_issuer_check_blocks_unknown_issuer() {
484 let vault = test_vault();
485 let trusted = [ISSUER];
486
487 let mut vc = vc_with_role(vc_roles::FIDUCIARY, 4);
488 vc.credential_issuer = 0xBAD_CAFE; assert!(!evaluate_vc_for_subgraph_key_release(
490 &vault,
491 &vc,
492 SubgraphLayer::Fiduciary,
493 &trusted
494 )
495 .is_permitted());
496
497 vc.credential_issuer = ISSUER; assert!(evaluate_vc_for_subgraph_key_release(
499 &vault,
500 &vc,
501 SubgraphLayer::Fiduciary,
502 &trusted
503 )
504 .is_permitted());
505 }
506
507 #[test]
508 fn evaluate_accessible_layers_returns_permitted_set() {
509 let vault = test_vault();
510 let vc = vc_with_role(0, 2);
512 let mut out: [Option<(SubgraphLayer, SubgraphKey)>; 5] = [None, None, None, None, None];
513 let count = evaluate_accessible_layers(&vault, &vc, &[], &mut out);
514 assert_eq!(count, 3);
515 let layers = [
516 out[0].as_ref().unwrap().0,
517 out[1].as_ref().unwrap().0,
518 out[2].as_ref().unwrap().0,
519 ];
520 assert_eq!(
521 layers,
522 [
523 SubgraphLayer::Public,
524 SubgraphLayer::Professional,
525 SubgraphLayer::Legal
526 ]
527 );
528 }
529
530 #[test]
531 fn vc_attributes_from_quins_extracts_role_and_clearance() {
532 let quins = write_vc_claim_quins(AGENT, &[vc_roles::MEDICAL_PROFESSIONAL], 3, ISSUER, 1000);
533 let attrs = VcAttributes::from_quins(AGENT, &quins);
534 assert_eq!(attrs.role_count, 1);
535 assert!(attrs.has_role(vc_roles::MEDICAL_PROFESSIONAL));
536 assert_eq!(attrs.clearance_level, 3);
537 assert_eq!(attrs.credential_issuer, ISSUER);
538 }
539
540 #[test]
541 fn test_child_medical_data_egress_violation() {
542 use crate::mini_parser::{OP_END, OP_EVAL_PERMIT};
543 use crate::webizen_bytecode::{execute_program, GuardianshipContext, VmError};
544
545 let child_did = crate::q_hash("did:q42:child");
546 let medical_data = crate::q_hash("MedicalData");
547 let share_data = crate::q_hash("q42:shareData");
548
549 let mut intent_quin = crate::NQuin::default();
550 intent_quin.subject = child_did;
551 intent_quin.predicate = share_data;
552 intent_quin.object = medical_data;
553
554 let db = [intent_quin];
555 let mut prog = [0u8; 1024];
556 prog[0] = OP_EVAL_PERMIT;
558 prog[1] = OP_END;
559
560 let mut out = [crate::NQuin::default(); 10];
561
562 let context = GuardianshipContext {
563 principal_did: child_did,
564 guardian_did: None, };
566
567 let result = execute_program(&prog, &db, &mut out, Some(&context));
568 assert_eq!(result, Err(VmError::HaltViolation));
569 }
570
571 #[test]
572 fn test_compile_n3_rule_to_norm() {
573 use crate::modalities::logic::n3_parser::RuleType;
574
575 use crate::modalities::logic::n3_compiler::{
576 CompiledFormula, CompiledTerm, CompiledTriple,
577 };
578 let make_rule = |rt: RuleType, pred: &str| -> CompiledRule {
579 let mut premise_triples = [CompiledTriple {
580 subject: CompiledTerm::Uri(0),
581 predicate: CompiledTerm::Uri(0),
582 object: CompiledTerm::Uri(0),
583 }; 8];
584 premise_triples[0] = CompiledTriple {
585 subject: CompiledTerm::Uri(crate::q_hash("did:party")),
586 predicate: CompiledTerm::Uri(crate::q_hash(pred)),
587 object: CompiledTerm::Uri(crate::q_hash("did:target")),
588 };
589 let mut conclusion_triples = [CompiledTriple {
590 subject: CompiledTerm::Uri(0),
591 predicate: CompiledTerm::Uri(0),
592 object: CompiledTerm::Uri(0),
593 }; 8];
594 conclusion_triples[0] = CompiledTriple {
595 subject: CompiledTerm::Uri(crate::q_hash("urn:contract")),
596 predicate: CompiledTerm::Uri(crate::q_hash("q42:boundBy")),
597 object: CompiledTerm::Uri(crate::q_hash("did:party")),
598 };
599 CompiledRule {
600 id_hash: None,
601 rule_type: rt,
602 weight: None,
603 premise: CompiledFormula {
604 triples: premise_triples,
605 len: 1,
606 },
607 conclusion: CompiledFormula {
608 triples: conclusion_triples,
609 len: 1,
610 },
611 }
612 };
613
614 let contract_hash = 12345;
615
616 let defeater_rule = make_rule(RuleType::Defeater, "q42:permit");
618 let q = compile_n3_rule_to_norm(&defeater_rule, contract_hash, 0).unwrap();
619 assert_eq!(
620 q.predicate & 0xFF,
621 crate::modalities::logic::deontic::OP_PERMIT as u64
622 );
623 assert_ne!(
624 q.predicate & crate::modalities::logic::deontic::DEFEATER_BIT,
625 0
626 );
627
628 let permit_rule = make_rule(RuleType::Defeasible, "q42:permit");
630 let q2 = compile_n3_rule_to_norm(&permit_rule, contract_hash, 0).unwrap();
631 assert_eq!(
632 q2.predicate & 0xFF,
633 crate::modalities::logic::deontic::OP_PERMIT as u64
634 );
635 assert_eq!(
636 q2.predicate & crate::modalities::logic::deontic::DEFEATER_BIT,
637 0
638 );
639
640 let obligate_rule = make_rule(RuleType::Strict, "q42:obligate");
642 let q3 = compile_n3_rule_to_norm(&obligate_rule, contract_hash, 0).unwrap();
643 assert_eq!(
644 q3.predicate & 0xFF,
645 crate::modalities::logic::deontic::OP_OBLIGATE as u64
646 );
647 assert_eq!(
648 q3.predicate & crate::modalities::logic::deontic::DEFEATER_BIT,
649 0
650 );
651
652 let mut malformed = make_rule(RuleType::Strict, "q42:obligate");
654 malformed.premise.len = 0;
655 assert!(compile_n3_rule_to_norm(&malformed, contract_hash, 0).is_none());
656 }
657}