Skip to main content

qssm_api/
lib.rs

1#![forbid(unsafe_code)]
2//! # QSSM Truth Engine — Layer 6 (Façade)
3//!
4//! The single entry point for the entire truth engine.
5//! Developers only import this crate. Everything else is internal.
6//!
7//! ## Five functions, one byte array — that's it.
8//!
9//! | Function    | Role |
10//! |-------------|------|
11//! | [`compile`] | Resolves a built-in template ID or raw template JSON into an opaque byte-array blueprint. |
12//! | [`commit`]  | Locks a secret without revealing it — returns 32 bytes. |
13//! | [`prove`]   | Creates a ZK proof (byte array) that a secret satisfies the blueprint's rules. |
14//! | [`verify`]  | Checks a proof byte array against a blueprint — returns `true` / `false`. |
15//! | [`open`]    | Returns the commitment bytes for a `(secret, salt)` pair; compare with [`commit`] output. |
16//!
17//! ## Quick start
18//!
19//! ```no_run
20//! use qssm_api::{compile, commit, prove, verify, open};
21//!
22//! let blueprint = compile("age-gate-21").unwrap();
23//! let commitment = commit(b"my-secret", &[1u8; 32]);
24//! let claim = br#"{"claim":{"age_years":25}}"#;
25//! let proof = prove(claim, &[1u8; 32], &blueprint).unwrap();
26//! assert!(verify(&proof, &blueprint));
27//! assert_eq!(open(b"my-secret", &[1u8; 32]), commitment);
28//! ```
29
30mod commit_impl;
31
32use qssm_local_prover::ProofContext;
33use qssm_utils::hashing::blake3_hash;
34use serde::{Deserialize, Serialize};
35
36// ── Internal wire-format structs (never public) ──────────────────────
37
38#[derive(Serialize, Deserialize)]
39struct WireBlueprint {
40    seed_hex: String,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    template_id: Option<String>,
43    template_json: String,
44}
45
46#[derive(Serialize, Deserialize)]
47struct WireZkProof {
48    bundle: qssm_local_prover::ProofBundle,
49    claim: serde_json::Value,
50    binding_ctx_hex: String,
51}
52
53// ── The 5 façade functions ───────────────────────────────────────────
54
55/// **The Blueprint.** Resolves a built-in template ID or raw template JSON and
56/// harvests entropy to produce an opaque byte-array blueprint.
57///
58/// # Errors
59///
60/// Returns `Err` if the input is neither a known built-in template ID nor
61/// valid `QssmTemplate` JSON, or if hardware entropy is unavailable.
62pub fn compile(template_id: &str) -> Result<Vec<u8>, String> {
63    let template = resolve_template_input(template_id)?;
64    let seed = qssm_entropy::harvest(&qssm_entropy::HarvestConfig::default())
65        .map_err(|e| format!("entropy unavailable: {e}"))?
66        .to_seed();
67    let wire = WireBlueprint {
68        seed_hex: hex::encode(seed),
69        template_id: Some(template.id().to_string()),
70        template_json: serde_json::to_string(&template)
71            .map_err(|e| format!("template serialization failed: {e}"))?,
72    };
73    serde_json::to_vec(&wire).map_err(|e| format!("serialization failed: {e}"))
74}
75
76/// **The Envelope.** Locks a secret without revealing it.
77///
78/// Returns a 32-byte commitment. Compare with [`open`] output using `==`.
79#[must_use]
80pub fn commit(secret: &[u8], salt: &[u8; 32]) -> Vec<u8> {
81    commit_impl::commit_hash(secret, salt).to_vec()
82}
83
84/// **The Proof Generator.** Creates a ZK proof (byte array) that the secret
85/// satisfies the blueprint's rules.
86///
87/// - `secret`: the claim data as JSON bytes (e.g. `b'{"claim":{"age_years":25}}'`).
88/// - `salt`: 32-byte caller-chosen salt (used to derive binding context).
89/// - `blueprint`: the opaque byte array from [`compile`].
90///
91/// # Errors
92///
93/// Returns `Err` if `secret` is not valid JSON, if the claim fails the
94/// template's predicates, if hardware entropy is unavailable, or if the
95/// internal prove pipeline fails.
96pub fn prove(secret: &[u8], salt: &[u8; 32], blueprint: &[u8]) -> Result<Vec<u8>, String> {
97    let wire_bp: WireBlueprint =
98        serde_json::from_slice(blueprint).map_err(|e| format!("invalid blueprint: {e}"))?;
99    let seed = decode_hex_32(&wire_bp.seed_hex, "blueprint seed")?;
100    let template = template_from_blueprint(&wire_bp)?;
101    let ctx = ProofContext::new(seed);
102
103    let claim: serde_json::Value =
104        serde_json::from_slice(secret).map_err(|e| format!("invalid JSON claim: {e}"))?;
105    let binding_ctx = blake3_hash(salt);
106    let entropy_seed = qssm_entropy::harvest(&qssm_entropy::HarvestConfig::default())
107        .map_err(|e| format!("entropy unavailable: {e}"))?
108        .to_seed();
109    let (value, target) = extract_value_target(&claim, &template);
110
111    let proof = qssm_local_prover::prove(
112        &ctx,
113        &template,
114        &claim,
115        value,
116        target,
117        binding_ctx,
118        entropy_seed,
119    )
120    .map_err(|e| format!("prove failed: {e}"))?;
121
122    let wire = WireZkProof {
123        bundle: qssm_local_prover::ProofBundle::from_proof(&proof),
124        claim,
125        binding_ctx_hex: hex::encode(binding_ctx),
126    };
127    serde_json::to_vec(&wire).map_err(|e| format!("serialization failed: {e}"))
128}
129
130/// **The Truth Checker.** Validates a proof byte array against a blueprint.
131///
132/// Returns `true` if the proof is valid, `false` otherwise. All internal
133/// errors (tampered proofs, wrong bindings, deserialization failures, etc.)
134/// collapse to `false`.
135#[must_use]
136pub fn verify(proof: &[u8], blueprint: &[u8]) -> bool {
137    verify_inner(proof, blueprint).unwrap_or(false)
138}
139
140/// **The Simple Reveal.** Reconstructs the commitment from `(secret, salt)`.
141///
142/// Returns the same 32 bytes that [`commit`] would produce for the same
143/// inputs. Compare with `==`.
144#[must_use]
145pub fn open(secret: &[u8], salt: &[u8; 32]) -> Vec<u8> {
146    commit_impl::commit_hash(secret, salt).to_vec()
147}
148
149// ── Internal helpers ─────────────────────────────────────────────────
150
151fn verify_inner(proof: &[u8], blueprint: &[u8]) -> Result<bool, String> {
152    let wire_bp: WireBlueprint =
153        serde_json::from_slice(blueprint).map_err(|e| format!("invalid blueprint: {e}"))?;
154    let seed = decode_hex_32(&wire_bp.seed_hex, "blueprint seed")?;
155    let template = template_from_blueprint(&wire_bp)?;
156    let ctx = ProofContext::new(seed);
157
158    let wire_proof: WireZkProof =
159        serde_json::from_slice(proof).map_err(|e| format!("invalid proof: {e}"))?;
160    let binding_ctx = decode_hex_32(&wire_proof.binding_ctx_hex, "binding_ctx")?;
161    let inner_proof = wire_proof
162        .bundle
163        .to_proof()
164        .map_err(|e| format!("invalid proof bundle: {e}"))?;
165
166    qssm_local_verifier::verify(
167        &ctx,
168        &template,
169        &wire_proof.claim,
170        &inner_proof,
171        binding_ctx,
172    )
173    .map_err(|e| format!("verification failed: {e}"))
174}
175
176fn decode_hex_32(hex_str: &str, field: &str) -> Result<[u8; 32], String> {
177    let bytes = hex::decode(hex_str).map_err(|e| format!("invalid hex for {field}: {e}"))?;
178    <[u8; 32]>::try_from(bytes.as_slice())
179        .map_err(|_| format!("{field}: expected 32 bytes, got {}", bytes.len()))
180}
181
182fn resolve_template_input(raw: &str) -> Result<qssm_templates::QssmTemplate, String> {
183    if let Some(template) = qssm_templates::resolve(raw.trim()) {
184        return Ok(template);
185    }
186    qssm_templates::QssmTemplate::from_json_slice(raw.as_bytes())
187        .map_err(|_| format!("unknown template or invalid template JSON: {raw}"))
188}
189
190fn template_from_blueprint(
191    wire_bp: &WireBlueprint,
192) -> Result<qssm_templates::QssmTemplate, String> {
193    if !wire_bp.template_json.trim().is_empty() {
194        return qssm_templates::QssmTemplate::from_json_slice(wire_bp.template_json.as_bytes())
195            .map_err(|e| format!("invalid blueprint template: {e}"));
196    }
197
198    if let Some(template_id) = &wire_bp.template_id {
199        return qssm_templates::resolve(template_id)
200            .ok_or_else(|| format!("unknown template: {template_id}"));
201    }
202
203    Err("blueprint is missing template payload".to_string())
204}
205
206// ── Internal helpers ─────────────────────────────────────────────────
207
208/// Extract (value, target) from claim + template predicates.
209fn extract_value_target(
210    claim: &serde_json::Value,
211    template: &qssm_templates::QssmTemplate,
212) -> (u64, u64) {
213    use qssm_templates::{json_at_path, PredicateBlock};
214
215    for pred in template.predicates() {
216        match pred {
217            PredicateBlock::Range { field, min, .. } => {
218                if let Some(val) = json_at_path(claim, field).and_then(|v| v.as_u64()) {
219                    // MS prover checks strict `value > target`, so pass
220                    // min-1 to get `value > (min-1)` ≡ `value >= min`.
221                    return (val, (*min as u64).saturating_sub(1));
222                }
223            }
224            PredicateBlock::AtLeast { field, min } => {
225                if let Some(val) = json_at_path(claim, field).and_then(|v| v.as_u64()) {
226                    return (val, (*min as u64).saturating_sub(1));
227                }
228            }
229            PredicateBlock::Compare {
230                field,
231                op: qssm_templates::CmpOp::Gt,
232                rhs,
233            } => {
234                if let (Some(lhs), Some(rhs_val)) = (
235                    json_at_path(claim, field).and_then(|v| v.as_u64()),
236                    rhs.as_u64(),
237                ) {
238                    return (lhs, rhs_val);
239                }
240            }
241            _ => {}
242        }
243    }
244    (1, 0)
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn commit_open_round_trip() {
253        let secret = b"my-secret-value";
254        let salt = [42u8; 32];
255        let c = commit(secret, &salt);
256        let d = open(secret, &salt);
257        assert_eq!(c, d);
258    }
259
260    #[test]
261    fn open_rejects_wrong_secret() {
262        let salt = [42u8; 32];
263        let c = commit(b"correct", &salt);
264        let d = open(b"wrong", &salt);
265        assert_ne!(c, d);
266    }
267
268    #[test]
269    fn extract_value_target_age_gate() {
270        let template = qssm_templates::QssmTemplate::proof_of_age("age-gate-21");
271        let claim = serde_json::json!({ "claim": { "age_years": 25 } });
272        let (v, t) = extract_value_target(&claim, &template);
273        assert_eq!(v, 25);
274        // target is min-1 (20) so the strict > prover checks 25 > 20
275        assert_eq!(t, 20);
276    }
277
278    #[test]
279    fn compile_rejects_unknown_template() {
280        let result = compile("nonexistent-template-xyz");
281        assert!(result.is_err());
282        assert!(result.unwrap_err().contains("unknown template"));
283    }
284
285    #[test]
286    fn prove_value_equals_min_passes() {
287        let blueprint = compile("age-gate-21").unwrap();
288        let claim = br#"{"claim":{"age_years":21}}"#;
289        let salt = [1u8; 32];
290        let proof = prove(claim, &salt, &blueprint);
291        assert!(
292            proof.is_ok(),
293            "age=21 should pass age-gate-21: {}",
294            proof.unwrap_err()
295        );
296        assert!(verify(&proof.unwrap(), &blueprint));
297    }
298
299    #[test]
300    fn prove_value_above_min_passes() {
301        let blueprint = compile("age-gate-21").unwrap();
302        let claim = br#"{"claim":{"age_years":30}}"#;
303        let salt = [2u8; 32];
304        let proof = prove(claim, &salt, &blueprint);
305        assert!(proof.is_ok(), "age=30 should pass: {}", proof.unwrap_err());
306        assert!(verify(&proof.unwrap(), &blueprint));
307    }
308
309    #[test]
310    fn prove_value_below_min_fails() {
311        let blueprint = compile("age-gate-21").unwrap();
312        let claim = br#"{"claim":{"age_years":20}}"#;
313        let salt = [3u8; 32];
314        let proof = prove(claim, &salt, &blueprint);
315        assert!(proof.is_err(), "age=20 should fail age-gate-21");
316    }
317
318    #[test]
319    fn compile_accepts_raw_template_json() {
320        let template = serde_json::json!({
321            "qssm_template_version": 1,
322            "id": "custom-age-gate",
323            "title": "Custom age gate",
324            "allowed_anchor_kinds": ["anchor_hash", "static_root", "timestamp_unix_secs"],
325            "predicates": [
326                {
327                    "kind": "at_least",
328                    "field": "claim.age_years",
329                    "min": 21
330                }
331            ]
332        });
333        let blueprint =
334            compile(&template.to_string()).expect("custom template JSON should compile");
335        let proof = prove(br#"{"claim":{"age_years":30}}"#, &[7u8; 32], &blueprint)
336            .expect("custom template blueprint should prove");
337        assert!(verify(&proof, &blueprint));
338    }
339}