Skip to main content

dcp_ai/
lib.rs

1//! DCP-AI Rust SDK — Digital Citizenship Protocol for AI Agents.
2//!
3//! Provides types, Ed25519 cryptography, SHA-256 hashing, Merkle trees,
4//! and full signed bundle verification. V2 adds composite hybrid signatures,
5//! domain separation, and post-quantum algorithm support.
6
7pub mod types;
8pub mod crypto;
9pub mod verify;
10pub mod v2;
11pub mod providers;
12pub mod observability;
13
14pub use types::*;
15pub use crypto::{
16    canonicalize, hash_object, generate_keypair, sign_object, verify_object,
17    merkle_root_from_hex_leaves,
18};
19pub use verify::verify_signed_bundle;
20
21/// Detect the DCP protocol version from a JSON value.
22pub fn detect_dcp_version(value: &serde_json::Value) -> Option<&str> {
23    if let Some(v) = value.get("dcp_version").and_then(|v| v.as_str()) {
24        match v {
25            "1.0" | "2.0" => return Some(v),
26            _ => {}
27        }
28    }
29    if let Some(v) = value.get("dcp_bundle_version").and_then(|v| v.as_str()) {
30        if v == "2.0" {
31            return Some("2.0");
32        }
33    }
34    if let Some(bundle) = value.get("bundle") {
35        if let Some(v) = bundle.get("dcp_bundle_version").and_then(|v| v.as_str()) {
36            if v == "2.0" {
37                return Some("2.0");
38            }
39        }
40        if let Some(rpr) = bundle.get("responsible_principal_record") {
41            if rpr.get("dcp_version").and_then(|v| v.as_str()) == Some("1.0") {
42                return Some("1.0");
43            }
44        }
45    }
46    None
47}
48
49// ── WASM bindings ──
50
51#[cfg(feature = "wasm")]
52pub mod wasm {
53    use wasm_bindgen::prelude::*;
54    use serde_json::{json, Value};
55    use base64::Engine;
56    use base64::engine::general_purpose::STANDARD as BASE64;
57
58    use crate::crypto;
59    use crate::verify;
60    use crate::providers::ed25519::Ed25519Provider;
61    use crate::providers::ml_dsa_65::MlDsa65Provider;
62    use crate::providers::slh_dsa_192f::SlhDsa192fProvider;
63    use crate::v2::crypto_provider::CryptoProvider;
64    use crate::v2::composite_ops::{
65        CompositeKeyInfo, composite_sign, classical_only_sign, composite_verify,
66    };
67    use crate::v2::composite_sig::{CompositeSignature, SignatureEntry};
68    use crate::v2::dual_hash;
69    use crate::v2::canonicalize::canonicalize_v2;
70    use crate::v2::signed_payload;
71    use crate::v2::proof_of_possession::{
72        PopChallenge, generate_registration_pop, verify_registration_pop,
73    };
74
75    fn json_err(msg: &str) -> String {
76        format!("{{\"error\":\"{}\"}}", msg.replace('"', "'"))
77    }
78
79    fn provider_for_alg(alg: &str) -> Result<Box<dyn CryptoProvider>, String> {
80        match alg {
81            "ed25519" => Ok(Box::new(Ed25519Provider)),
82            "ml-dsa-65" => Ok(Box::new(MlDsa65Provider)),
83            "slh-dsa-192f" => Ok(Box::new(SlhDsa192fProvider)),
84            _ => Err(format!("Unknown algorithm: {}", alg)),
85        }
86    }
87
88    // ── V1 Compatibility ──────────────────────────────────────────────────
89
90    #[wasm_bindgen]
91    pub fn wasm_verify_signed_bundle(signed_bundle_json: &str, public_key_b64: Option<String>) -> String {
92        let sb: Value = match serde_json::from_str(signed_bundle_json) {
93            Ok(v) => v,
94            Err(e) => return json_err(&format!("JSON parse error: {}", e)),
95        };
96        let result = verify::verify_signed_bundle(&sb, public_key_b64.as_deref());
97        serde_json::to_string(&result).unwrap_or_else(|_| "{\"verified\":false}".to_string())
98    }
99
100    #[wasm_bindgen]
101    pub fn wasm_hash_object(json_str: &str) -> String {
102        let obj: Value = match serde_json::from_str(json_str) {
103            Ok(v) => v,
104            Err(e) => return json_err(&format!("JSON parse: {}", e)),
105        };
106        crypto::hash_object(&obj)
107    }
108
109    #[wasm_bindgen]
110    pub fn wasm_detect_version(json_str: &str) -> String {
111        let val: Value = match serde_json::from_str(json_str) {
112            Ok(v) => v,
113            Err(_) => return "null".to_string(),
114        };
115        match crate::detect_dcp_version(&val) {
116            Some(v) => format!("\"{}\"", v),
117            None => "null".to_string(),
118        }
119    }
120
121    // ── Keypair Generation ────────────────────────────────────────────────
122
123    #[wasm_bindgen]
124    pub fn wasm_generate_keypair() -> String {
125        let (pub_key, sec_key) = crypto::generate_keypair();
126        serde_json::to_string(&json!({
127            "alg": "ed25519",
128            "public_key_b64": pub_key,
129            "secret_key_b64": sec_key
130        })).unwrap()
131    }
132
133    #[wasm_bindgen]
134    pub fn wasm_generate_ml_dsa_65_keypair() -> String {
135        let provider = MlDsa65Provider;
136        match provider.generate_keypair() {
137            Ok(kp) => serde_json::to_string(&json!({
138                "alg": "ml-dsa-65",
139                "kid": kp.kid,
140                "public_key_b64": kp.public_key_b64,
141                "secret_key_b64": kp.secret_key_b64
142            })).unwrap(),
143            Err(e) => json_err(&e.to_string()),
144        }
145    }
146
147    #[wasm_bindgen]
148    pub fn wasm_generate_slh_dsa_192f_keypair() -> String {
149        let provider = SlhDsa192fProvider;
150        match provider.generate_keypair() {
151            Ok(kp) => serde_json::to_string(&json!({
152                "alg": "slh-dsa-192f",
153                "kid": kp.kid,
154                "public_key_b64": kp.public_key_b64,
155                "secret_key_b64": kp.secret_key_b64
156            })).unwrap(),
157            Err(e) => json_err(&e.to_string()),
158        }
159    }
160
161    /// Generate an Ed25519 + ML-DSA-65 hybrid keypair in a single call.
162    #[wasm_bindgen]
163    pub fn wasm_generate_hybrid_keypair() -> String {
164        let ed = Ed25519Provider;
165        let pq = MlDsa65Provider;
166        let ed_kp = match ed.generate_keypair() {
167            Ok(kp) => kp,
168            Err(e) => return json_err(&e.to_string()),
169        };
170        let pq_kp = match pq.generate_keypair() {
171            Ok(kp) => kp,
172            Err(e) => return json_err(&e.to_string()),
173        };
174        serde_json::to_string(&json!({
175            "classical": {
176                "alg": "ed25519",
177                "kid": ed_kp.kid,
178                "public_key_b64": ed_kp.public_key_b64,
179                "secret_key_b64": ed_kp.secret_key_b64
180            },
181            "pq": {
182                "alg": "ml-dsa-65",
183                "kid": pq_kp.kid,
184                "public_key_b64": pq_kp.public_key_b64,
185                "secret_key_b64": pq_kp.secret_key_b64
186            }
187        })).unwrap()
188    }
189
190    // ── Composite Signing ─────────────────────────────────────────────────
191
192    /// Composite sign: Ed25519 + ML-DSA-65 with pq_over_classical binding.
193    #[wasm_bindgen]
194    pub fn wasm_composite_sign(
195        context: &str,
196        payload_json: &str,
197        classical_sk_b64: &str,
198        classical_kid: &str,
199        pq_sk_b64: &str,
200        pq_kid: &str,
201    ) -> String {
202        let val: Value = match serde_json::from_str(payload_json) {
203            Ok(v) => v,
204            Err(e) => return json_err(&format!("JSON parse: {}", e)),
205        };
206        let canonical = match canonicalize_v2(&val) {
207            Ok(c) => c,
208            Err(e) => return json_err(&e),
209        };
210
211        let ed = Ed25519Provider;
212        let pq = MlDsa65Provider;
213        let classical_key = CompositeKeyInfo {
214            kid: classical_kid.to_string(),
215            alg: "ed25519".to_string(),
216            secret_key_b64: classical_sk_b64.to_string(),
217            public_key_b64: String::new(),
218        };
219        let pq_key = CompositeKeyInfo {
220            kid: pq_kid.to_string(),
221            alg: "ml-dsa-65".to_string(),
222            secret_key_b64: pq_sk_b64.to_string(),
223            public_key_b64: String::new(),
224        };
225
226        match composite_sign(&ed, &pq, context, canonical.as_bytes(), &classical_key, &pq_key) {
227            Ok(sig) => serde_json::to_string(&sig).unwrap_or_else(|_| json_err("serialize failed")),
228            Err(e) => json_err(&e.to_string()),
229        }
230    }
231
232    /// Classical-only signing (Ed25519 transition mode).
233    #[wasm_bindgen]
234    pub fn wasm_classical_only_sign(
235        context: &str,
236        payload_json: &str,
237        sk_b64: &str,
238        kid: &str,
239    ) -> String {
240        let val: Value = match serde_json::from_str(payload_json) {
241            Ok(v) => v,
242            Err(e) => return json_err(&format!("JSON parse: {}", e)),
243        };
244        let canonical = match canonicalize_v2(&val) {
245            Ok(c) => c,
246            Err(e) => return json_err(&e),
247        };
248
249        let ed = Ed25519Provider;
250        let key = CompositeKeyInfo {
251            kid: kid.to_string(),
252            alg: "ed25519".to_string(),
253            secret_key_b64: sk_b64.to_string(),
254            public_key_b64: String::new(),
255        };
256
257        match classical_only_sign(&ed, context, canonical.as_bytes(), &key) {
258            Ok(sig) => serde_json::to_string(&sig).unwrap_or_else(|_| json_err("serialize failed")),
259            Err(e) => json_err(&e.to_string()),
260        }
261    }
262
263    /// Sign a payload and return a SignedPayload envelope (payload + hash + composite_sig).
264    #[wasm_bindgen]
265    pub fn wasm_sign_payload(
266        context: &str,
267        payload_json: &str,
268        classical_sk_b64: &str,
269        classical_kid: &str,
270        pq_sk_b64: &str,
271        pq_kid: &str,
272    ) -> String {
273        let val: Value = match serde_json::from_str(payload_json) {
274            Ok(v) => v,
275            Err(e) => return json_err(&format!("JSON parse: {}", e)),
276        };
277        let (canonical_bytes, payload_hash) = match signed_payload::prepare_payload(&val) {
278            Ok(r) => r,
279            Err(e) => return json_err(&e),
280        };
281
282        let ed = Ed25519Provider;
283        let pq = MlDsa65Provider;
284        let classical_key = CompositeKeyInfo {
285            kid: classical_kid.to_string(),
286            alg: "ed25519".to_string(),
287            secret_key_b64: classical_sk_b64.to_string(),
288            public_key_b64: String::new(),
289        };
290        let pq_key = CompositeKeyInfo {
291            kid: pq_kid.to_string(),
292            alg: "ml-dsa-65".to_string(),
293            secret_key_b64: pq_sk_b64.to_string(),
294            public_key_b64: String::new(),
295        };
296
297        match composite_sign(&ed, &pq, context, &canonical_bytes, &classical_key, &pq_key) {
298            Ok(sig) => serde_json::to_string(&json!({
299                "payload": val,
300                "payload_hash": payload_hash,
301                "composite_sig": sig
302            })).unwrap_or_else(|_| json_err("serialize failed")),
303            Err(e) => json_err(&e.to_string()),
304        }
305    }
306
307    // ── Composite Verification ────────────────────────────────────────────
308
309    /// Verify a composite signature cryptographically.
310    #[wasm_bindgen]
311    pub fn wasm_composite_verify(
312        context: &str,
313        payload_json: &str,
314        composite_sig_json: &str,
315        classical_pk_b64: &str,
316        pq_pk_b64: Option<String>,
317    ) -> String {
318        let val: Value = match serde_json::from_str(payload_json) {
319            Ok(v) => v,
320            Err(e) => return json_err(&format!("JSON parse: {}", e)),
321        };
322        let sig: CompositeSignature = match serde_json::from_str(composite_sig_json) {
323            Ok(s) => s,
324            Err(e) => return json_err(&format!("Signature parse: {}", e)),
325        };
326        let canonical = match canonicalize_v2(&val) {
327            Ok(c) => c,
328            Err(e) => return json_err(&e),
329        };
330
331        let ed = Ed25519Provider;
332        let pq = MlDsa65Provider;
333        let pq_ref: Option<&dyn CryptoProvider> = if pq_pk_b64.is_some() { Some(&pq) } else { None };
334
335        match composite_verify(
336            &ed, pq_ref, context, canonical.as_bytes(),
337            &sig, classical_pk_b64, pq_pk_b64.as_deref(),
338        ) {
339            Ok(result) => serde_json::to_string(&json!({
340                "valid": result.valid,
341                "classical_valid": result.classical_valid,
342                "pq_valid": result.pq_valid
343            })).unwrap(),
344            Err(e) => json_err(&e.to_string()),
345        }
346    }
347
348    /// Full V2 bundle verification with cryptographic signature checks.
349    #[wasm_bindgen]
350    pub fn wasm_verify_signed_bundle_v2(signed_bundle_json: &str) -> String {
351        let val: Value = match serde_json::from_str(signed_bundle_json) {
352            Ok(v) => v,
353            Err(e) => return serde_json::to_string(&json!({
354                "verified": false, "errors": [format!("JSON parse error: {}", e)]
355            })).unwrap(),
356        };
357
358        let version = crate::detect_dcp_version(&val);
359
360        match version {
361            Some("1.0") => {
362                let result = verify::verify_signed_bundle(&val, None);
363                return serde_json::to_string(&result).unwrap_or_else(|_| "{\"verified\":false}".to_string());
364            },
365            Some("2.0") => {},
366            _ => {
367                return serde_json::to_string(&json!({
368                    "verified": false, "errors": ["Unknown DCP version"]
369                })).unwrap();
370            }
371        }
372
373        let mut errors: Vec<String> = Vec::new();
374        let mut warnings: Vec<String> = Vec::new();
375        let mut classical_valid = false;
376        let mut pq_valid = false;
377
378        let bundle = match val.get("bundle") {
379            Some(b) => b,
380            None => return serde_json::to_string(&json!({
381                "verified": false, "errors": ["Missing bundle field"]
382            })).unwrap(),
383        };
384        let signature = match val.get("signature") {
385            Some(s) => s,
386            None => return serde_json::to_string(&json!({
387                "verified": false, "errors": ["Missing signature field"]
388            })).unwrap(),
389        };
390
391        if bundle.get("dcp_bundle_version").and_then(|v| v.as_str()) != Some("2.0") {
392            errors.push("Invalid dcp_bundle_version".to_string());
393        }
394        if bundle.get("manifest").is_none() {
395            errors.push("Missing manifest in bundle".to_string());
396        }
397        for field in &["responsible_principal_record", "agent_passport", "intent", "policy_decision"] {
398            if bundle.get(*field).is_none() {
399                errors.push(format!("Missing {} in bundle", field));
400            }
401        }
402
403        let manifest_nonce = bundle.get("manifest")
404            .and_then(|m| m.get("session_nonce"))
405            .and_then(|n| n.as_str())
406            .unwrap_or("");
407        if manifest_nonce.is_empty() {
408            errors.push("Missing session_nonce in manifest".to_string());
409        }
410
411        // Verify manifest hashes against actual artifact hashes
412        if let Some(manifest) = bundle.get("manifest") {
413            for (field, hash_key) in &[
414                ("responsible_principal_record", "rpr_hash"),
415                ("agent_passport", "passport_hash"),
416                ("intent", "intent_hash"),
417                ("policy_decision", "policy_hash"),
418            ] {
419                if let (Some(artifact), Some(expected)) = (
420                    bundle.get(*field).and_then(|a| a.get("payload")),
421                    manifest.get(*hash_key).and_then(|h| h.as_str()),
422                ) {
423                    if let Ok(canonical) = canonicalize_v2(artifact) {
424                        let dh = dual_hash::dual_hash_canonical(&canonical);
425                        let computed = format!("sha256:{}", dh.sha256);
426                        if computed != expected {
427                            errors.push(format!("Manifest {} mismatch", hash_key));
428                        }
429                    }
430                }
431            }
432        }
433
434        // Session nonce consistency across artifacts
435        if !manifest_nonce.is_empty() {
436            for field in &["responsible_principal_record", "agent_passport", "intent", "policy_decision"] {
437                if let Some(nonce) = bundle.get(*field)
438                    .and_then(|a| a.get("payload"))
439                    .and_then(|p| p.get("session_nonce"))
440                    .and_then(|n| n.as_str())
441                {
442                    if nonce != manifest_nonce {
443                        errors.push(format!("Session nonce mismatch in {}", field));
444                        break;
445                    }
446                }
447            }
448        }
449
450        // Cryptographic signature verification on the bundle-level composite_sig
451        if let Some(cs_val) = signature.get("composite_sig") {
452            if let Ok(cs) = serde_json::from_value::<CompositeSignature>(cs_val.clone()) {
453                let binding = cs.binding.as_str();
454
455                // We need public keys from the passport
456                let passport_keys = bundle.get("agent_passport")
457                    .and_then(|a| a.get("payload"))
458                    .and_then(|p| p.get("keys"))
459                    .and_then(|k| k.as_array());
460
461                let mut classical_pk: Option<String> = None;
462                let mut pq_pk: Option<String> = None;
463
464                if let Some(keys) = passport_keys {
465                    for key_entry in keys {
466                        let alg = key_entry.get("alg").and_then(|a| a.as_str()).unwrap_or("");
467                        let pk = key_entry.get("public_key_b64").and_then(|p| p.as_str());
468                        match alg {
469                            "ed25519" => classical_pk = pk.map(|s| s.to_string()),
470                            "ml-dsa-65" => pq_pk = pk.map(|s| s.to_string()),
471                            _ => {}
472                        }
473                    }
474                }
475
476                // Verify bundle manifest signature
477                if let Some(manifest) = bundle.get("manifest") {
478                    if let Ok(canonical) = canonicalize_v2(manifest) {
479                        let ed = Ed25519Provider;
480                        let pq_prov = MlDsa65Provider;
481
482                        if let Some(ref cpk) = classical_pk {
483                            let pq_ref: Option<&dyn CryptoProvider> = if pq_pk.is_some() && binding == "pq_over_classical" {
484                                Some(&pq_prov)
485                            } else {
486                                None
487                            };
488                            match composite_verify(
489                                &ed, pq_ref,
490                                crate::v2::domain_separation::CTX_BUNDLE,
491                                canonical.as_bytes(), &cs, cpk,
492                                pq_pk.as_deref(),
493                            ) {
494                                Ok(result) => {
495                                    classical_valid = result.classical_valid;
496                                    pq_valid = result.pq_valid;
497                                    if !result.valid {
498                                        errors.push("Bundle signature verification failed".to_string());
499                                    }
500                                },
501                                Err(e) => errors.push(format!("Signature verify error: {}", e)),
502                            }
503                        } else {
504                            warnings.push("No classical public key found in passport".to_string());
505                        }
506                    }
507                }
508
509                if binding == "classical_only" {
510                    warnings.push("Bundle uses classical_only binding (no PQ protection)".to_string());
511                }
512            } else {
513                errors.push("Invalid composite_sig structure".to_string());
514            }
515        } else {
516            errors.push("Missing composite_sig in signature".to_string());
517        }
518
519        // Verify audit entry hash chain
520        if let Some(entries) = bundle.get("audit_entries").and_then(|e| e.as_array()) {
521            let mut expected_prev = "sha256:".to_string() + &"0".repeat(64);
522            for (i, entry) in entries.iter().enumerate() {
523                if let Some(prev) = entry.get("prev_hash").and_then(|p| p.as_str()) {
524                    if i > 0 && prev != expected_prev {
525                        errors.push(format!("Audit hash chain broken at entry {}", i));
526                        break;
527                    }
528                }
529                if let Ok(canonical) = canonicalize_v2(entry) {
530                    let dh = dual_hash::dual_hash_canonical(&canonical);
531                    expected_prev = format!("sha256:{}", dh.sha256);
532                }
533            }
534        }
535
536        let verified = errors.is_empty();
537        serde_json::to_string(&json!({
538            "verified": verified,
539            "dcp_version": "2.0",
540            "errors": errors,
541            "warnings": warnings,
542            "classical_valid": classical_valid,
543            "pq_valid": pq_valid,
544            "session_binding_valid": !manifest_nonce.is_empty(),
545            "manifest_valid": bundle.get("manifest").is_some()
546        })).unwrap()
547    }
548
549    // ── Canonicalization & Domain Separation ───────────────────────────────
550
551    #[wasm_bindgen]
552    pub fn wasm_derive_kid(alg: &str, public_key_b64: &str) -> String {
553        let pk_bytes = match BASE64.decode(public_key_b64) {
554            Ok(b) => b,
555            Err(e) => return json_err(&format!("base64 decode: {}", e)),
556        };
557        crate::v2::crypto_provider::derive_kid(alg, &pk_bytes)
558    }
559
560    #[wasm_bindgen]
561    pub fn wasm_canonicalize_v2(json_str: &str) -> String {
562        let val: Value = match serde_json::from_str(json_str) {
563            Ok(v) => v,
564            Err(e) => return json_err(&format!("JSON parse: {}", e)),
565        };
566        match canonicalize_v2(&val) {
567            Ok(s) => s,
568            Err(e) => json_err(&e),
569        }
570    }
571
572    #[wasm_bindgen]
573    pub fn wasm_domain_separated_message(context: &str, payload_hex: &str) -> String {
574        let payload = match hex::decode(payload_hex) {
575            Ok(b) => b,
576            Err(e) => return json_err(&format!("hex decode: {}", e)),
577        };
578        match crate::v2::domain_separation::domain_separated_message(context, &payload) {
579            Ok(dsm) => hex::encode(dsm),
580            Err(e) => json_err(&e),
581        }
582    }
583
584    // ── Dual Hash ─────────────────────────────────────────────────────────
585
586    /// Compute SHA-256 + SHA3-256 dual hash of a string.
587    #[wasm_bindgen]
588    pub fn wasm_dual_hash(data: &str) -> String {
589        let dh = dual_hash::dual_hash(data.as_bytes());
590        serde_json::to_string(&dh).unwrap()
591    }
592
593    /// Compute SHA3-256 hash of a string (hex-encoded).
594    #[wasm_bindgen]
595    pub fn wasm_sha3_256(data: &str) -> String {
596        dual_hash::sha3_256_hex(data.as_bytes())
597    }
598
599    /// Compute dual Merkle root from an array of dual-hash leaves.
600    /// Input: JSON array of {"sha256":"...","sha3_256":"..."} objects.
601    #[wasm_bindgen]
602    pub fn wasm_dual_merkle_root(leaves_json: &str) -> String {
603        let leaves: Vec<dual_hash::DualHash> = match serde_json::from_str(leaves_json) {
604            Ok(v) => v,
605            Err(e) => return json_err(&format!("JSON parse: {}", e)),
606        };
607        if leaves.is_empty() {
608            return json_err("Empty leaves array");
609        }
610
611        fn merkle_reduce(hashes: Vec<String>, use_sha3: bool) -> String {
612            if hashes.len() == 1 {
613                return hashes[0].clone();
614            }
615            let mut next = Vec::new();
616            let mut i = 0;
617            while i < hashes.len() {
618                if i + 1 < hashes.len() {
619                    let combined = format!("{}{}", hashes[i], hashes[i + 1]);
620                    if use_sha3 {
621                        next.push(dual_hash::sha3_256_hex(combined.as_bytes()));
622                    } else {
623                        next.push(dual_hash::sha256_hex(combined.as_bytes()));
624                    }
625                    i += 2;
626                } else {
627                    next.push(hashes[i].clone());
628                    i += 1;
629                }
630            }
631            merkle_reduce(next, use_sha3)
632        }
633
634        let sha256_leaves: Vec<String> = leaves.iter().map(|l| l.sha256.clone()).collect();
635        let sha3_leaves: Vec<String> = leaves.iter().map(|l| l.sha3_256.clone()).collect();
636
637        let sha256_root = merkle_reduce(sha256_leaves, false);
638        let sha3_root = merkle_reduce(sha3_leaves, true);
639
640        serde_json::to_string(&json!({
641            "sha256": sha256_root,
642            "sha3_256": sha3_root
643        })).unwrap()
644    }
645
646    // ── Session Nonce ─────────────────────────────────────────────────────
647
648    /// Generate a 256-bit random session nonce (64 hex chars).
649    #[wasm_bindgen]
650    pub fn wasm_generate_session_nonce() -> String {
651        use rand::RngCore;
652        let mut bytes = [0u8; 32];
653        rand::thread_rng().fill_bytes(&mut bytes);
654        hex::encode(bytes)
655    }
656
657    /// Verify session nonce consistency across artifacts.
658    /// Input: JSON array of objects, each optionally containing "session_nonce".
659    #[wasm_bindgen]
660    pub fn wasm_verify_session_binding(artifacts_json: &str) -> String {
661        let artifacts: Vec<Value> = match serde_json::from_str(artifacts_json) {
662            Ok(v) => v,
663            Err(e) => return serde_json::to_string(&json!({
664                "valid": false, "error": format!("JSON parse: {}", e)
665            })).unwrap(),
666        };
667
668        let mut found_nonce: Option<String> = None;
669        for art in &artifacts {
670            if let Some(nonce) = art.get("session_nonce").and_then(|n| n.as_str()) {
671                match &found_nonce {
672                    None => found_nonce = Some(nonce.to_string()),
673                    Some(expected) => {
674                        if nonce != expected {
675                            return serde_json::to_string(&json!({
676                                "valid": false,
677                                "error": "Session nonce mismatch",
678                                "expected": expected,
679                                "got": nonce
680                            })).unwrap();
681                        }
682                    }
683                }
684            }
685        }
686
687        serde_json::to_string(&json!({
688            "valid": true,
689            "nonce": found_nonce
690        })).unwrap()
691    }
692
693    // ── Security Tier ─────────────────────────────────────────────────────
694
695    /// Compute adaptive security tier from an intent's risk profile.
696    /// Input: JSON intent with risk_score, data_classes, action_type.
697    #[wasm_bindgen]
698    pub fn wasm_compute_security_tier(intent_json: &str) -> String {
699        let val: Value = match serde_json::from_str(intent_json) {
700            Ok(v) => v,
701            Err(e) => return json_err(&format!("JSON parse: {}", e)),
702        };
703
704        let risk_score = val.get("risk_score").and_then(|r| r.as_u64()).unwrap_or(0);
705        let data_classes: Vec<&str> = val.get("data_classes")
706            .and_then(|d| d.as_array())
707            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
708            .unwrap_or_default();
709        let action_type = val.get("action_type").and_then(|a| a.as_str()).unwrap_or("");
710
711        let has_high_sensitivity = data_classes.iter().any(|c|
712            *c == "credentials" || *c == "children_data" || *c == "biometric"
713        );
714        let has_medium_sensitivity = data_classes.iter().any(|c|
715            *c == "pii" || *c == "financial" || *c == "health" || *c == "legal"
716        );
717        let is_payment = action_type == "payment" || action_type == "transfer";
718
719        let tier = if risk_score >= 800 || has_high_sensitivity {
720            "maximum"
721        } else if risk_score >= 500 || has_medium_sensitivity || is_payment {
722            "elevated"
723        } else if risk_score >= 200 {
724            "standard"
725        } else {
726            "routine"
727        };
728
729        let (verification_mode, checkpoint_interval) = match tier {
730            "maximum" => ("hybrid_required", 1),
731            "elevated" => ("hybrid_required", 1),
732            "standard" => ("hybrid_preferred", 10),
733            _ => ("classical_only", 50),
734        };
735
736        serde_json::to_string(&json!({
737            "tier": tier,
738            "verification_mode": verification_mode,
739            "checkpoint_interval": checkpoint_interval
740        })).unwrap()
741    }
742
743    // ── Payload Preparation ───────────────────────────────────────────────
744
745    /// Canonicalize a payload and compute its hash.
746    /// Returns { canonical: "...", payload_hash: "sha256:..." }.
747    #[wasm_bindgen]
748    pub fn wasm_prepare_payload(payload_json: &str) -> String {
749        let val: Value = match serde_json::from_str(payload_json) {
750            Ok(v) => v,
751            Err(e) => return json_err(&format!("JSON parse: {}", e)),
752        };
753        match signed_payload::prepare_payload(&val) {
754            Ok((canonical_bytes, hash)) => {
755                let canonical_str = String::from_utf8_lossy(&canonical_bytes);
756                serde_json::to_string(&json!({
757                    "canonical": canonical_str,
758                    "payload_hash": hash
759                })).unwrap()
760            },
761            Err(e) => json_err(&e),
762        }
763    }
764
765    // ── Bundle Building & Signing ─────────────────────────────────────────
766
767    /// Build a complete V2 CitizenshipBundle with manifest.
768    #[wasm_bindgen]
769    pub fn wasm_build_bundle(
770        rpr_json: &str,
771        passport_json: &str,
772        intent_json: &str,
773        policy_json: &str,
774        audit_entries_json: &str,
775        session_nonce: &str,
776    ) -> String {
777        let rpr: Value = match serde_json::from_str(rpr_json) {
778            Ok(v) => v, Err(e) => return json_err(&format!("RPR parse: {}", e)),
779        };
780        let passport: Value = match serde_json::from_str(passport_json) {
781            Ok(v) => v, Err(e) => return json_err(&format!("Passport parse: {}", e)),
782        };
783        let intent: Value = match serde_json::from_str(intent_json) {
784            Ok(v) => v, Err(e) => return json_err(&format!("Intent parse: {}", e)),
785        };
786        let policy: Value = match serde_json::from_str(policy_json) {
787            Ok(v) => v, Err(e) => return json_err(&format!("Policy parse: {}", e)),
788        };
789        let audit_entries: Vec<Value> = match serde_json::from_str(audit_entries_json) {
790            Ok(v) => v, Err(e) => return json_err(&format!("Audit entries parse: {}", e)),
791        };
792
793        let hash_val = |v: &Value| -> String {
794            match canonicalize_v2(v) {
795                Ok(c) => {
796                    let dh = dual_hash::dual_hash_canonical(&c);
797                    format!("sha256:{}", dh.sha256)
798                },
799                Err(_) => "sha256:error".to_string(),
800            }
801        };
802
803        let rpr_hash = hash_val(&rpr);
804        let passport_hash = hash_val(&passport);
805        let intent_hash = hash_val(&intent);
806        let policy_hash = hash_val(&policy);
807
808        // Compute dual Merkle root over audit entries
809        let audit_hashes: Vec<dual_hash::DualHash> = audit_entries.iter()
810            .filter_map(|e| canonicalize_v2(e).ok())
811            .map(|c| dual_hash::dual_hash_canonical(&c))
812            .collect();
813
814        let (audit_merkle_sha256, audit_merkle_sha3) = if audit_hashes.is_empty() {
815            ("sha256:".to_string() + &"0".repeat(64), "sha3-256:".to_string() + &"0".repeat(64))
816        } else {
817            let sha256_leaves: Vec<String> = audit_hashes.iter().map(|h| h.sha256.clone()).collect();
818            let sha3_leaves: Vec<String> = audit_hashes.iter().map(|h| h.sha3_256.clone()).collect();
819            (
820                format!("sha256:{}", crypto::merkle_root_from_hex_leaves(&sha256_leaves).unwrap_or_default()),
821                format!("sha3-256:{}", crypto::merkle_root_from_hex_leaves(&sha3_leaves).unwrap_or_default()),
822            )
823        };
824
825        let manifest = json!({
826            "session_nonce": session_nonce,
827            "rpr_hash": rpr_hash,
828            "passport_hash": passport_hash,
829            "intent_hash": intent_hash,
830            "policy_hash": policy_hash,
831            "audit_merkle_root": audit_merkle_sha256,
832            "audit_merkle_root_secondary": audit_merkle_sha3,
833            "audit_count": audit_entries.len(),
834            "canonicalization_profile": "dcp-jcs-v1"
835        });
836
837        let bundle = json!({
838            "dcp_bundle_version": "2.0",
839            "manifest": manifest,
840            "responsible_principal_record": { "payload": rpr, "payload_hash": rpr_hash },
841            "agent_passport": { "payload": passport, "payload_hash": passport_hash },
842            "intent": { "payload": intent, "payload_hash": intent_hash },
843            "policy_decision": { "payload": policy, "payload_hash": policy_hash },
844            "audit_entries": audit_entries
845        });
846
847        serde_json::to_string(&bundle).unwrap_or_else(|_| json_err("serialize failed"))
848    }
849
850    /// Sign a V2 bundle with composite signature (Ed25519 + ML-DSA-65).
851    #[wasm_bindgen]
852    pub fn wasm_sign_bundle(
853        bundle_json: &str,
854        classical_sk_b64: &str,
855        classical_kid: &str,
856        pq_sk_b64: &str,
857        pq_kid: &str,
858    ) -> String {
859        let bundle: Value = match serde_json::from_str(bundle_json) {
860            Ok(v) => v,
861            Err(e) => return json_err(&format!("Bundle parse: {}", e)),
862        };
863
864        let manifest = match bundle.get("manifest") {
865            Some(m) => m,
866            None => return json_err("Missing manifest in bundle"),
867        };
868
869        let canonical = match canonicalize_v2(manifest) {
870            Ok(c) => c,
871            Err(e) => return json_err(&e),
872        };
873
874        let manifest_hash = {
875            let dh = dual_hash::dual_hash_canonical(&canonical);
876            format!("sha256:{}", dh.sha256)
877        };
878
879        let ed = Ed25519Provider;
880        let pq = MlDsa65Provider;
881        let classical_key = CompositeKeyInfo {
882            kid: classical_kid.to_string(),
883            alg: "ed25519".to_string(),
884            secret_key_b64: classical_sk_b64.to_string(),
885            public_key_b64: String::new(),
886        };
887        let pq_key = CompositeKeyInfo {
888            kid: pq_kid.to_string(),
889            alg: "ml-dsa-65".to_string(),
890            secret_key_b64: pq_sk_b64.to_string(),
891            public_key_b64: String::new(),
892        };
893
894        let sig = match composite_sign(
895            &ed, &pq,
896            crate::v2::domain_separation::CTX_BUNDLE,
897            canonical.as_bytes(), &classical_key, &pq_key,
898        ) {
899            Ok(s) => s,
900            Err(e) => return json_err(&e.to_string()),
901        };
902
903        let signed_bundle = json!({
904            "bundle": bundle,
905            "signature": {
906                "hash_alg": "sha256",
907                "created_at": "",
908                "signer": {
909                    "type": "human",
910                    "kids": [classical_kid, pq_kid]
911                },
912                "manifest_hash": manifest_hash,
913                "composite_sig": sig
914            }
915        });
916
917        serde_json::to_string(&signed_bundle).unwrap_or_else(|_| json_err("serialize failed"))
918    }
919
920    // ── Proof of Possession ───────────────────────────────────────────────
921
922    /// Generate a proof-of-possession for key registration.
923    #[wasm_bindgen]
924    pub fn wasm_generate_registration_pop(
925        challenge_json: &str,
926        sk_b64: &str,
927        alg: &str,
928    ) -> String {
929        let challenge: PopChallenge = match serde_json::from_str(challenge_json) {
930            Ok(c) => c,
931            Err(e) => return json_err(&format!("Challenge parse: {}", e)),
932        };
933        let provider = match provider_for_alg(alg) {
934            Ok(p) => p,
935            Err(e) => return json_err(&e),
936        };
937
938        match generate_registration_pop(provider.as_ref(), &challenge, sk_b64) {
939            Ok(entry) => serde_json::to_string(&entry).unwrap_or_else(|_| json_err("serialize failed")),
940            Err(e) => json_err(&e.to_string()),
941        }
942    }
943
944    // ── ML-KEM-768 Key Encapsulation ─────────────────────────────────────
945
946    /// Generate an ML-KEM-768 keypair (encapsulation key + decapsulation key).
947    #[wasm_bindgen]
948    pub fn wasm_ml_kem_768_keygen() -> String {
949        use crate::providers::ml_kem_768::MlKem768Provider;
950        use crate::v2::crypto_provider::KemProvider;
951        let provider = MlKem768Provider;
952        match provider.generate_keypair() {
953            Ok(kp) => serde_json::to_string(&json!({
954                "alg": "ml-kem-768",
955                "kid": kp.kid,
956                "public_key_b64": kp.public_key_b64,
957                "secret_key_b64": kp.secret_key_b64
958            })).unwrap(),
959            Err(e) => json_err(&e.to_string()),
960        }
961    }
962
963    /// Encapsulate a shared secret using an ML-KEM-768 public key.
964    /// Returns { shared_secret_hex, ciphertext_b64 }.
965    #[wasm_bindgen]
966    pub fn wasm_ml_kem_768_encapsulate(public_key_b64: &str) -> String {
967        use crate::providers::ml_kem_768::MlKem768Provider;
968        use crate::v2::crypto_provider::KemProvider;
969        let provider = MlKem768Provider;
970        match provider.encapsulate(public_key_b64) {
971            Ok((ss, ct)) => serde_json::to_string(&json!({
972                "shared_secret_hex": hex::encode(&ss),
973                "ciphertext_b64": BASE64.encode(&ct)
974            })).unwrap(),
975            Err(e) => json_err(&e.to_string()),
976        }
977    }
978
979    /// Decapsulate a shared secret from ciphertext using an ML-KEM-768 secret key.
980    /// Returns the shared secret as hex.
981    #[wasm_bindgen]
982    pub fn wasm_ml_kem_768_decapsulate(ciphertext_b64: &str, secret_key_b64: &str) -> String {
983        use crate::providers::ml_kem_768::MlKem768Provider;
984        use crate::v2::crypto_provider::KemProvider;
985        let provider = MlKem768Provider;
986        let ct = match BASE64.decode(ciphertext_b64) {
987            Ok(b) => b,
988            Err(e) => return json_err(&format!("base64 decode: {}", e)),
989        };
990        match provider.decapsulate(&ct, secret_key_b64) {
991            Ok(ss) => hex::encode(ss),
992            Err(e) => json_err(&e.to_string()),
993        }
994    }
995
996    /// Verify a proof-of-possession for key registration.
997    #[wasm_bindgen]
998    pub fn wasm_verify_registration_pop(
999        challenge_json: &str,
1000        pop_json: &str,
1001        pk_b64: &str,
1002        alg: &str,
1003    ) -> String {
1004        let challenge: PopChallenge = match serde_json::from_str(challenge_json) {
1005            Ok(c) => c,
1006            Err(e) => return json_err(&format!("Challenge parse: {}", e)),
1007        };
1008        let pop: SignatureEntry = match serde_json::from_str(pop_json) {
1009            Ok(p) => p,
1010            Err(e) => return json_err(&format!("PoP parse: {}", e)),
1011        };
1012        let provider = match provider_for_alg(alg) {
1013            Ok(p) => p,
1014            Err(e) => return json_err(&e),
1015        };
1016
1017        match verify_registration_pop(provider.as_ref(), &challenge, &pop, pk_b64) {
1018            Ok(valid) => serde_json::to_string(&json!({ "valid": valid })).unwrap(),
1019            Err(e) => json_err(&e.to_string()),
1020        }
1021    }
1022}