Skip to main content

qualia_core_db/crypto/
deontic_circuit.rs

1// ── Deontic Access Circuit (arkworks) ──────────────────────────────────────
2/// Groth16 circuit for zero-knowledge proof verification.
3
4#[cfg(feature = "zk-culling")]
5use ark_bls12_381::{Bls12_381, Fr};
6#[cfg(feature = "zk-culling")]
7use ark_groth16::{Groth16, Proof, ProvingKey, VerifyingKey};
8#[cfg(feature = "zk-culling")]
9use ark_relations::gr1cs::{
10    ConstraintSynthesizer, ConstraintSystemRef, LinearCombination, SynthesisError, Variable,
11};
12#[cfg(feature = "zk-culling")]
13use ark_serialize::CanonicalDeserialize;
14#[cfg(feature = "zk-culling")]
15use ark_snark::SNARK;
16
17#[cfg(feature = "zk-culling")]
18#[derive(Clone)]
19pub struct DeonticAccessCircuit {
20    pub user_did_commitment: Option<Fr>,
21    pub role_id: Option<Fr>,
22    pub action_permission: Option<Fr>,
23    pub policy_root: Option<Fr>,
24    pub temporal_constraint: Option<Fr>,
25}
26
27#[cfg(feature = "zk-culling")]
28impl ConstraintSynthesizer<Fr> for DeonticAccessCircuit {
29    fn generate_constraints(self, cs: ConstraintSystemRef<Fr>) -> Result<(), SynthesisError> {
30        let did_var = cs.new_witness_variable(|| {
31            self.user_did_commitment
32                .ok_or(SynthesisError::AssignmentMissing)
33        })?;
34        let role_var =
35            cs.new_witness_variable(|| self.role_id.ok_or(SynthesisError::AssignmentMissing))?;
36        let action_var = cs.new_witness_variable(|| {
37            self.action_permission
38                .ok_or(SynthesisError::AssignmentMissing)
39        })?;
40        let root_var =
41            cs.new_input_variable(|| self.policy_root.ok_or(SynthesisError::AssignmentMissing))?;
42        let time_var = cs.new_input_variable(|| {
43            self.temporal_constraint
44                .ok_or(SynthesisError::AssignmentMissing)
45        })?;
46
47        // Access constraint: the prover's secret (did, role, action) must sum to the
48        // public `policy_root` — i.e. they hold a credential authorised under the
49        // committed policy. (Simplified additive commitment; Merkle-set membership is
50        // a future hardening, not faked here.)
51        cs.enforce_r1cs_constraint(
52            || LinearCombination::from(did_var) + role_var + action_var,
53            || LinearCombination::from(Variable::One),
54            || LinearCombination::from(root_var),
55        )?;
56        // `temporal_constraint` is bound as a public input (the verifier checks the
57        // proof was generated for this exact timestamp), but range enforcement
58        // (notBefore <= t <= notAfter) is a documented PendingImplementation. We do
59        // NOT fake it with a tautological `t * 1 = t` constraint that enforces nothing.
60        let _ = time_var;
61        Ok(())
62    }
63}
64
65#[cfg(feature = "zk-culling")]
66pub struct ZkAccessVerifier {
67    verifying_key: Option<VerifyingKey<Bls12_381>>,
68}
69
70#[cfg(feature = "zk-culling")]
71impl ZkAccessVerifier {
72    pub fn new() -> Self {
73        Self {
74            verifying_key: None,
75        }
76    }
77
78    pub fn verify_access(
79        &self,
80        proof: &[u8],
81        public_inputs: &[Fr],
82    ) -> Result<bool, crate::fiduciary_crypto::MlDsaError> {
83        if let Some(ref vk) = self.verifying_key {
84            let parsed_proof =
85                Proof::<Bls12_381>::deserialize_uncompressed(&mut &*proof).map_err(|e| {
86                    crate::fiduciary_crypto::MlDsaError::SignatureVerificationFailed(e.to_string())
87                })?;
88            Groth16::<Bls12_381>::verify(vk, public_inputs, &parsed_proof).map_err(|e| {
89                crate::fiduciary_crypto::MlDsaError::SignatureVerificationFailed(e.to_string())
90            })
91        } else {
92            Err(
93                crate::fiduciary_crypto::MlDsaError::SignatureVerificationFailed(
94                    "Verifying key not loaded".to_string(),
95                ),
96            )
97        }
98    }
99}
100
101#[cfg(feature = "zk-culling")]
102impl Default for ZkAccessVerifier {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108#[cfg(feature = "zk-culling")]
109pub fn generate_deontic_crs() -> Result<(ProvingKey<Bls12_381>, VerifyingKey<Bls12_381>), String> {
110    let circuit = DeonticAccessCircuit {
111        user_did_commitment: None,
112        role_id: None,
113        action_permission: None,
114        policy_root: None,
115        temporal_constraint: None,
116    };
117    let mut rng = crate::zk_proofs::zk_secure_rng();
118    Groth16::<Bls12_381>::circuit_specific_setup(circuit, &mut rng)
119        .map_err(|e| format!("Failed to generate parameters: {e}"))
120}
121
122#[cfg(test)]
123#[cfg(feature = "zk-culling")]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_circuit_setup() {
129        assert!(generate_deontic_crs().is_ok());
130    }
131
132    #[test]
133    fn test_deontic_proof_roundtrip_and_soundness() {
134        use ark_ff::UniformRand;
135        let mut rng = crate::zk_proofs::zk_secure_rng();
136        let (pk, vk) = generate_deontic_crs().unwrap();
137
138        // A satisfying credential: did + role + action == policy_root.
139        let did = Fr::rand(&mut rng);
140        let role = Fr::rand(&mut rng);
141        let action = Fr::rand(&mut rng);
142        let policy_root = did + role + action;
143        let temporal = Fr::from(1_700_000_000u64);
144
145        let circuit = DeonticAccessCircuit {
146            user_did_commitment: Some(did),
147            role_id: Some(role),
148            action_permission: Some(action),
149            policy_root: Some(policy_root),
150            temporal_constraint: Some(temporal),
151        };
152        let proof = Groth16::<Bls12_381>::prove(&pk, circuit, &mut rng).unwrap();
153
154        // Valid: the public inputs match the proven relation.
155        assert!(
156            Groth16::<Bls12_381>::verify(&vk, &[policy_root, temporal], &proof).unwrap(),
157            "a satisfying deontic access proof must verify"
158        );
159
160        // Soundness: a falsified policy_root must be rejected.
161        assert!(
162            !Groth16::<Bls12_381>::verify(&vk, &[policy_root + Fr::from(1u64), temporal], &proof)
163                .unwrap(),
164            "the proof must NOT verify against a falsified policy_root"
165        );
166    }
167}