1pub 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
21pub 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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 #[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 #[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 #[wasm_bindgen]
595 pub fn wasm_sha3_256(data: &str) -> String {
596 dual_hash::sha3_256_hex(data.as_bytes())
597 }
598
599 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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}