1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
10use sha2::{Digest, Sha256};
11
12use crate::compare::timing_safe_equal;
13use crate::errors::AshError;
14use crate::types::{AshMode, BuildProofInput, VerifyInput};
15
16const ASH_VERSION: &str = "ASHv1";
18
19pub fn build_proof(
57 mode: AshMode,
58 binding: &str,
59 context_id: &str,
60 nonce: Option<&str>,
61 canonical_payload: &str,
62) -> Result<String, AshError> {
63 let mut input = String::new();
65
66 input.push_str(ASH_VERSION);
68 input.push('\n');
69
70 input.push_str(&mode.to_string());
72 input.push('\n');
73
74 input.push_str(binding);
76 input.push('\n');
77
78 input.push_str(context_id);
80 input.push('\n');
81
82 if let Some(n) = nonce {
84 input.push_str(n);
85 input.push('\n');
86 }
87
88 input.push_str(canonical_payload);
90
91 let mut hasher = Sha256::new();
93 hasher.update(input.as_bytes());
94 let hash = hasher.finalize();
95
96 Ok(URL_SAFE_NO_PAD.encode(hash))
98}
99
100#[allow(dead_code)]
104pub fn ash_build_proof(input: &BuildProofInput) -> Result<String, AshError> {
105 build_proof(
106 input.mode,
107 &input.binding,
108 &input.context_id,
109 input.nonce.as_deref(),
110 &input.canonical_payload,
111 )
112}
113
114pub fn verify_proof(input: &VerifyInput) -> bool {
138 timing_safe_equal(
139 input.expected_proof.as_bytes(),
140 input.actual_proof.as_bytes(),
141 )
142}
143
144#[allow(dead_code)]
148pub fn ash_verify_proof(expected: &str, actual: &str) -> bool {
149 timing_safe_equal(expected.as_bytes(), actual.as_bytes())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_build_proof_deterministic() {
158 let proof1 = build_proof(
159 AshMode::Balanced,
160 "POST /api/test",
161 "ctx123",
162 None,
163 r#"{"a":1}"#,
164 )
165 .unwrap();
166
167 let proof2 = build_proof(
168 AshMode::Balanced,
169 "POST /api/test",
170 "ctx123",
171 None,
172 r#"{"a":1}"#,
173 )
174 .unwrap();
175
176 assert_eq!(proof1, proof2);
177 }
178
179 #[test]
180 fn test_build_proof_different_payload() {
181 let proof1 = build_proof(
182 AshMode::Balanced,
183 "POST /api/test",
184 "ctx123",
185 None,
186 r#"{"a":1}"#,
187 )
188 .unwrap();
189
190 let proof2 = build_proof(
191 AshMode::Balanced,
192 "POST /api/test",
193 "ctx123",
194 None,
195 r#"{"a":2}"#,
196 )
197 .unwrap();
198
199 assert_ne!(proof1, proof2);
200 }
201
202 #[test]
203 fn test_build_proof_different_context() {
204 let proof1 = build_proof(
205 AshMode::Balanced,
206 "POST /api/test",
207 "ctx123",
208 None,
209 r#"{"a":1}"#,
210 )
211 .unwrap();
212
213 let proof2 = build_proof(
214 AshMode::Balanced,
215 "POST /api/test",
216 "ctx456",
217 None,
218 r#"{"a":1}"#,
219 )
220 .unwrap();
221
222 assert_ne!(proof1, proof2);
223 }
224
225 #[test]
226 fn test_build_proof_different_binding() {
227 let proof1 = build_proof(
228 AshMode::Balanced,
229 "POST /api/test",
230 "ctx123",
231 None,
232 r#"{"a":1}"#,
233 )
234 .unwrap();
235
236 let proof2 = build_proof(
237 AshMode::Balanced,
238 "PUT /api/test",
239 "ctx123",
240 None,
241 r#"{"a":1}"#,
242 )
243 .unwrap();
244
245 assert_ne!(proof1, proof2);
246 }
247
248 #[test]
249 fn test_build_proof_with_nonce() {
250 let proof_without = build_proof(
251 AshMode::Balanced,
252 "POST /api/test",
253 "ctx123",
254 None,
255 r#"{"a":1}"#,
256 )
257 .unwrap();
258
259 let proof_with = build_proof(
260 AshMode::Balanced,
261 "POST /api/test",
262 "ctx123",
263 Some("nonce456"),
264 r#"{"a":1}"#,
265 )
266 .unwrap();
267
268 assert_ne!(proof_without, proof_with);
269 }
270
271 #[test]
272 fn test_build_proof_different_mode() {
273 let proof1 = build_proof(
274 AshMode::Minimal,
275 "POST /api/test",
276 "ctx123",
277 None,
278 r#"{"a":1}"#,
279 )
280 .unwrap();
281
282 let proof2 = build_proof(
283 AshMode::Strict,
284 "POST /api/test",
285 "ctx123",
286 None,
287 r#"{"a":1}"#,
288 )
289 .unwrap();
290
291 assert_ne!(proof1, proof2);
292 }
293
294 #[test]
295 fn test_verify_proof_match() {
296 let proof = build_proof(
297 AshMode::Balanced,
298 "POST /api/test",
299 "ctx123",
300 None,
301 r#"{"a":1}"#,
302 )
303 .unwrap();
304
305 let input = VerifyInput::new(&proof, &proof);
306 assert!(verify_proof(&input));
307 }
308
309 #[test]
310 fn test_verify_proof_mismatch() {
311 let proof1 = build_proof(
312 AshMode::Balanced,
313 "POST /api/test",
314 "ctx123",
315 None,
316 r#"{"a":1}"#,
317 )
318 .unwrap();
319
320 let proof2 = build_proof(
321 AshMode::Balanced,
322 "POST /api/test",
323 "ctx123",
324 None,
325 r#"{"a":2}"#,
326 )
327 .unwrap();
328
329 let input = VerifyInput::new(&proof1, &proof2);
330 assert!(!verify_proof(&input));
331 }
332
333 #[test]
334 fn test_proof_is_base64url() {
335 let proof = build_proof(
336 AshMode::Balanced,
337 "POST /api/test",
338 "ctx123",
339 None,
340 r#"{"a":1}"#,
341 )
342 .unwrap();
343
344 assert!(!proof.contains('+'));
346 assert!(!proof.contains('/'));
347 assert!(!proof.contains('='));
348
349 assert_eq!(proof.len(), 43);
351 }
352}
353
354use hmac::{Hmac, Mac};
359use sha2::Sha256 as HmacSha256;
360
361type HmacSha256Type = Hmac<HmacSha256>;
362
363#[allow(dead_code)]
365const ASH_VERSION_V21: &str = "ASHv2.1";
366
367pub fn generate_nonce(bytes: usize) -> String {
375 use getrandom::getrandom;
376 let mut buf = vec![0u8; bytes];
377 getrandom(&mut buf).expect("Failed to generate random bytes");
378 hex::encode(buf)
379}
380
381pub fn generate_context_id() -> String {
383 format!("ash_{}", generate_nonce(16))
384}
385
386pub fn derive_client_secret(nonce: &str, context_id: &str, binding: &str) -> String {
395 let mut mac = HmacSha256Type::new_from_slice(nonce.as_bytes())
396 .expect("HMAC can take key of any size");
397 mac.update(format!("{}|{}", context_id, binding).as_bytes());
398 hex::encode(mac.finalize().into_bytes())
399}
400
401pub fn build_proof_v21(
405 client_secret: &str,
406 timestamp: &str,
407 binding: &str,
408 body_hash: &str,
409) -> String {
410 let message = format!("{}|{}|{}", timestamp, binding, body_hash);
411 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
412 .expect("HMAC can take key of any size");
413 mac.update(message.as_bytes());
414 hex::encode(mac.finalize().into_bytes())
415}
416
417pub fn verify_proof_v21(
419 nonce: &str,
420 context_id: &str,
421 binding: &str,
422 timestamp: &str,
423 body_hash: &str,
424 client_proof: &str,
425) -> bool {
426 let client_secret = derive_client_secret(nonce, context_id, binding);
427 let expected_proof = build_proof_v21(&client_secret, timestamp, binding, body_hash);
428 timing_safe_equal(expected_proof.as_bytes(), client_proof.as_bytes())
429}
430
431pub fn hash_body(canonical_body: &str) -> String {
433 let mut hasher = Sha256::new();
434 hasher.update(canonical_body.as_bytes());
435 hex::encode(hasher.finalize())
436}
437
438#[cfg(test)]
439mod tests_v21 {
440 use super::*;
441
442 #[test]
443 fn test_derive_client_secret_deterministic() {
444 let secret1 = derive_client_secret("nonce123", "ctx_abc", "POST /login");
445 let secret2 = derive_client_secret("nonce123", "ctx_abc", "POST /login");
446 assert_eq!(secret1, secret2);
447 }
448
449 #[test]
450 fn test_derive_client_secret_different_inputs() {
451 let secret1 = derive_client_secret("nonce123", "ctx_abc", "POST /login");
452 let secret2 = derive_client_secret("nonce456", "ctx_abc", "POST /login");
453 assert_ne!(secret1, secret2);
454 }
455
456 #[test]
457 fn test_build_proof_v21_deterministic() {
458 let proof1 = build_proof_v21("secret", "1234567890", "POST /login", "bodyhash");
459 let proof2 = build_proof_v21("secret", "1234567890", "POST /login", "bodyhash");
460 assert_eq!(proof1, proof2);
461 }
462
463 #[test]
464 fn test_verify_proof_v21() {
465 let nonce = "nonce123";
466 let context_id = "ctx_abc";
467 let binding = "POST /login";
468 let timestamp = "1234567890";
469 let body_hash = "bodyhash123";
470
471 let client_secret = derive_client_secret(nonce, context_id, binding);
472 let proof = build_proof_v21(&client_secret, timestamp, binding, body_hash);
473
474 assert!(verify_proof_v21(nonce, context_id, binding, timestamp, body_hash, &proof));
475 }
476
477 #[test]
478 fn test_hash_body() {
479 let hash = hash_body(r#"{"name":"John"}"#);
480 assert_eq!(hash.len(), 64); }
482}
483
484use serde_json::{Map, Value};
489
490pub fn extract_scoped_fields(payload: &Value, scope: &[&str]) -> Result<Value, AshError> {
492 if scope.is_empty() {
493 return Ok(payload.clone());
494 }
495
496 let mut result = Map::new();
497
498 for field_path in scope {
499 let value = get_nested_value(payload, field_path);
500 if let Some(v) = value {
501 set_nested_value(&mut result, field_path, v);
502 }
503 }
504
505 Ok(Value::Object(result))
506}
507
508fn get_nested_value(payload: &Value, path: &str) -> Option<Value> {
509 let parts: Vec<&str> = path.split('.').collect();
510 let mut current = payload;
511
512 for part in parts {
513 let (key, index) = parse_array_notation(part);
514
515 match current {
516 Value::Object(map) => {
517 current = map.get(key)?;
518 if let Some(idx) = index {
519 if let Value::Array(arr) = current {
520 current = arr.get(idx)?;
521 } else {
522 return None;
523 }
524 }
525 }
526 Value::Array(arr) => {
527 let idx: usize = key.parse().ok()?;
528 current = arr.get(idx)?;
529 }
530 _ => return None,
531 }
532 }
533
534 Some(current.clone())
535}
536
537fn parse_array_notation(part: &str) -> (&str, Option<usize>) {
538 if let Some(bracket_start) = part.find('[') {
539 if let Some(bracket_end) = part.find(']') {
540 let key = &part[..bracket_start];
541 let index_str = &part[bracket_start + 1..bracket_end];
542 if let Ok(index) = index_str.parse::<usize>() {
543 return (key, Some(index));
544 }
545 }
546 }
547 (part, None)
548}
549
550fn set_nested_value(result: &mut Map<String, Value>, path: &str, value: Value) {
551 let parts: Vec<&str> = path.split('.').collect();
552
553 if parts.len() == 1 {
554 let (key, _) = parse_array_notation(parts[0]);
555 result.insert(key.to_string(), value);
556 return;
557 }
558
559 let (first_key, _) = parse_array_notation(parts[0]);
560 let remaining_path = parts[1..].join(".");
561
562 let nested = result
563 .entry(first_key.to_string())
564 .or_insert_with(|| Value::Object(Map::new()));
565
566 if let Value::Object(nested_map) = nested {
567 set_nested_value(nested_map, &remaining_path, value);
568 }
569}
570pub fn build_proof_v21_scoped(
572 client_secret: &str,
573 timestamp: &str,
574 binding: &str,
575 payload: &str,
576 scope: &[&str],
577) -> Result<(String, String), AshError> {
578 let json_payload: Value = serde_json::from_str(payload)
579 .map_err(|e| AshError::canonicalization_failed(&format!("Invalid JSON: {}", e)))?;
580
581 let scoped_payload = extract_scoped_fields(&json_payload, scope)?;
582
583 let canonical_scoped = serde_json::to_string(&scoped_payload)
584 .map_err(|e| AshError::canonicalization_failed(&format!("Failed to serialize: {}", e)))?;
585
586 let body_hash = hash_body(&canonical_scoped);
587
588 let scope_str = scope.join(",");
589 let scope_hash = hash_body(&scope_str);
590
591 let message = format!("{}|{}|{}|{}", timestamp, binding, body_hash, scope_hash);
592 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
593 .expect("HMAC can take key of any size");
594 mac.update(message.as_bytes());
595 let proof = hex::encode(mac.finalize().into_bytes());
596
597 Ok((proof, scope_hash))
598}
599
600pub fn verify_proof_v21_scoped(
602 nonce: &str,
603 context_id: &str,
604 binding: &str,
605 timestamp: &str,
606 payload: &str,
607 scope: &[&str],
608 scope_hash: &str,
609 client_proof: &str,
610) -> Result<bool, AshError> {
611 let scope_str = scope.join(",");
612 let expected_scope_hash = hash_body(&scope_str);
613 if !timing_safe_equal(expected_scope_hash.as_bytes(), scope_hash.as_bytes()) {
614 return Ok(false);
615 }
616
617 let client_secret = derive_client_secret(nonce, context_id, binding);
618
619 let (expected_proof, _) = build_proof_v21_scoped(
620 &client_secret,
621 timestamp,
622 binding,
623 payload,
624 scope,
625 )?;
626
627 Ok(timing_safe_equal(expected_proof.as_bytes(), client_proof.as_bytes()))
628}
629
630pub fn hash_scoped_body(payload: &str, scope: &[&str]) -> Result<String, AshError> {
632 let json_payload: Value = serde_json::from_str(payload)
633 .map_err(|e| AshError::canonicalization_failed(&format!("Invalid JSON: {}", e)))?;
634
635 let scoped_payload = extract_scoped_fields(&json_payload, scope)?;
636
637 let canonical_scoped = serde_json::to_string(&scoped_payload)
638 .map_err(|e| AshError::canonicalization_failed(&format!("Failed to serialize: {}", e)))?;
639
640 Ok(hash_body(&canonical_scoped))
641}
642
643#[cfg(test)]
644mod tests_v22_scoping {
645 use super::*;
646
647 #[test]
648 fn test_build_verify_scoped_proof() {
649 let nonce = "test_nonce_12345";
650 let context_id = "ctx_abc123";
651 let binding = "POST /transfer";
652 let timestamp = "1234567890";
653 let payload = r#"{"amount":1000,"recipient":"user1","notes":"hi"}"#;
654 let scope = vec!["amount", "recipient"];
655
656 let client_secret = derive_client_secret(nonce, context_id, binding);
657 let (proof, scope_hash) = build_proof_v21_scoped(
658 &client_secret,
659 timestamp,
660 binding,
661 payload,
662 &scope,
663 ).unwrap();
664
665 let is_valid = verify_proof_v21_scoped(
666 nonce,
667 context_id,
668 binding,
669 timestamp,
670 payload,
671 &scope,
672 &scope_hash,
673 &proof,
674 ).unwrap();
675
676 assert!(is_valid);
677 }
678
679 #[test]
680 fn test_scoped_proof_ignores_unscoped_changes() {
681 let nonce = "test_nonce_12345";
682 let context_id = "ctx_abc123";
683 let binding = "POST /transfer";
684 let timestamp = "1234567890";
685 let scope = vec!["amount", "recipient"];
686
687 let client_secret = derive_client_secret(nonce, context_id, binding);
688
689 let payload1 = r#"{"amount":1000,"recipient":"user1","notes":"hello"}"#;
690 let (proof, scope_hash) = build_proof_v21_scoped(
691 &client_secret,
692 timestamp,
693 binding,
694 payload1,
695 &scope,
696 ).unwrap();
697
698 let payload2 = r#"{"amount":1000,"recipient":"user1","notes":"world"}"#;
699
700 let is_valid = verify_proof_v21_scoped(
701 nonce,
702 context_id,
703 binding,
704 timestamp,
705 payload2,
706 &scope,
707 &scope_hash,
708 &proof,
709 ).unwrap();
710
711 assert!(is_valid);
712 }
713
714 #[test]
715 fn test_scoped_proof_detects_scoped_changes() {
716 let nonce = "test_nonce_12345";
717 let context_id = "ctx_abc123";
718 let binding = "POST /transfer";
719 let timestamp = "1234567890";
720 let scope = vec!["amount", "recipient"];
721
722 let client_secret = derive_client_secret(nonce, context_id, binding);
723
724 let payload1 = r#"{"amount":1000,"recipient":"user1","notes":"hello"}"#;
725 let (proof, scope_hash) = build_proof_v21_scoped(
726 &client_secret,
727 timestamp,
728 binding,
729 payload1,
730 &scope,
731 ).unwrap();
732
733 let payload2 = r#"{"amount":9999,"recipient":"user1","notes":"hello"}"#;
734
735 let is_valid = verify_proof_v21_scoped(
736 nonce,
737 context_id,
738 binding,
739 timestamp,
740 payload2,
741 &scope,
742 &scope_hash,
743 &proof,
744 ).unwrap();
745
746 assert!(!is_valid);
747 }
748}
749
750#[derive(Debug, Clone, PartialEq)]
756pub struct UnifiedProofResult {
757 pub proof: String,
759 pub scope_hash: String,
761 pub chain_hash: String,
763}
764
765pub fn hash_proof(proof: &str) -> String {
769 let mut hasher = Sha256::new();
770 hasher.update(proof.as_bytes());
771 hex::encode(hasher.finalize())
772}
773
774pub fn build_proof_v21_unified(
788 client_secret: &str,
789 timestamp: &str,
790 binding: &str,
791 payload: &str,
792 scope: &[&str],
793 previous_proof: Option<&str>,
794) -> Result<UnifiedProofResult, AshError> {
795 let json_payload: Value = serde_json::from_str(payload)
797 .map_err(|e| AshError::canonicalization_failed(&format!("Invalid JSON: {}", e)))?;
798
799 let scoped_payload = extract_scoped_fields(&json_payload, scope)?;
800
801 let canonical_scoped = serde_json::to_string(&scoped_payload)
802 .map_err(|e| AshError::canonicalization_failed(&format!("Failed to serialize: {}", e)))?;
803
804 let body_hash = hash_body(&canonical_scoped);
805
806 let scope_hash = if scope.is_empty() {
808 String::new()
809 } else {
810 hash_body(&scope.join(","))
811 };
812
813 let chain_hash = match previous_proof {
815 Some(prev) if !prev.is_empty() => hash_proof(prev),
816 _ => String::new(),
817 };
818
819 let message = format!(
821 "{}|{}|{}|{}|{}",
822 timestamp, binding, body_hash, scope_hash, chain_hash
823 );
824
825 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
826 .expect("HMAC can take key of any size");
827 mac.update(message.as_bytes());
828 let proof = hex::encode(mac.finalize().into_bytes());
829
830 Ok(UnifiedProofResult {
831 proof,
832 scope_hash,
833 chain_hash,
834 })
835}
836
837pub fn verify_proof_v21_unified(
841 nonce: &str,
842 context_id: &str,
843 binding: &str,
844 timestamp: &str,
845 payload: &str,
846 client_proof: &str,
847 scope: &[&str],
848 scope_hash: &str,
849 previous_proof: Option<&str>,
850 chain_hash: &str,
851) -> Result<bool, AshError> {
852 if !scope.is_empty() {
854 let expected_scope_hash = hash_body(&scope.join(","));
855 if !timing_safe_equal(expected_scope_hash.as_bytes(), scope_hash.as_bytes()) {
856 return Ok(false);
857 }
858 }
859
860 if let Some(prev) = previous_proof {
862 if !prev.is_empty() {
863 let expected_chain_hash = hash_proof(prev);
864 if !timing_safe_equal(expected_chain_hash.as_bytes(), chain_hash.as_bytes()) {
865 return Ok(false);
866 }
867 }
868 }
869
870 let client_secret = derive_client_secret(nonce, context_id, binding);
872
873 let result = build_proof_v21_unified(
874 &client_secret,
875 timestamp,
876 binding,
877 payload,
878 scope,
879 previous_proof,
880 )?;
881
882 Ok(timing_safe_equal(result.proof.as_bytes(), client_proof.as_bytes()))
883}
884
885#[cfg(test)]
886mod tests_v23_unified {
887 use super::*;
888
889 #[test]
890 fn test_unified_basic() {
891 let nonce = "test_nonce_12345";
892 let context_id = "ctx_abc123";
893 let binding = "POST /api/test";
894 let timestamp = "1234567890";
895 let payload = r#"{"name":"John","age":30}"#;
896
897 let client_secret = derive_client_secret(nonce, context_id, binding);
898 let result = build_proof_v21_unified(
899 &client_secret,
900 timestamp,
901 binding,
902 payload,
903 &[], None, ).unwrap();
906
907 assert!(!result.proof.is_empty());
908 assert!(result.scope_hash.is_empty());
909 assert!(result.chain_hash.is_empty());
910
911 let is_valid = verify_proof_v21_unified(
912 nonce,
913 context_id,
914 binding,
915 timestamp,
916 payload,
917 &result.proof,
918 &[],
919 "",
920 None,
921 "",
922 ).unwrap();
923
924 assert!(is_valid);
925 }
926
927 #[test]
928 fn test_unified_scoped_only() {
929 let nonce = "test_nonce_12345";
930 let context_id = "ctx_abc123";
931 let binding = "POST /transfer";
932 let timestamp = "1234567890";
933 let payload = r#"{"amount":1000,"recipient":"user1","notes":"hi"}"#;
934 let scope = vec!["amount", "recipient"];
935
936 let client_secret = derive_client_secret(nonce, context_id, binding);
937 let result = build_proof_v21_unified(
938 &client_secret,
939 timestamp,
940 binding,
941 payload,
942 &scope,
943 None, ).unwrap();
945
946 assert!(!result.proof.is_empty());
947 assert!(!result.scope_hash.is_empty());
948 assert!(result.chain_hash.is_empty());
949
950 let is_valid = verify_proof_v21_unified(
951 nonce,
952 context_id,
953 binding,
954 timestamp,
955 payload,
956 &result.proof,
957 &scope,
958 &result.scope_hash,
959 None,
960 "",
961 ).unwrap();
962
963 assert!(is_valid);
964 }
965
966 #[test]
967 fn test_unified_chained_only() {
968 let nonce = "test_nonce_12345";
969 let context_id = "ctx_abc123";
970 let binding = "POST /checkout";
971 let timestamp = "1234567890";
972 let payload = r#"{"cart_id":"cart_123"}"#;
973 let previous_proof = "abc123def456";
974
975 let client_secret = derive_client_secret(nonce, context_id, binding);
976 let result = build_proof_v21_unified(
977 &client_secret,
978 timestamp,
979 binding,
980 payload,
981 &[], Some(previous_proof),
983 ).unwrap();
984
985 assert!(!result.proof.is_empty());
986 assert!(result.scope_hash.is_empty());
987 assert!(!result.chain_hash.is_empty());
988
989 let is_valid = verify_proof_v21_unified(
990 nonce,
991 context_id,
992 binding,
993 timestamp,
994 payload,
995 &result.proof,
996 &[],
997 "",
998 Some(previous_proof),
999 &result.chain_hash,
1000 ).unwrap();
1001
1002 assert!(is_valid);
1003 }
1004
1005 #[test]
1006 fn test_unified_full() {
1007 let nonce = "test_nonce_12345";
1008 let context_id = "ctx_abc123";
1009 let binding = "POST /payment";
1010 let timestamp = "1234567890";
1011 let payload = r#"{"amount":500,"currency":"USD","notes":"tip"}"#;
1012 let scope = vec!["amount", "currency"];
1013 let previous_proof = "checkout_proof_xyz";
1014
1015 let client_secret = derive_client_secret(nonce, context_id, binding);
1016 let result = build_proof_v21_unified(
1017 &client_secret,
1018 timestamp,
1019 binding,
1020 payload,
1021 &scope,
1022 Some(previous_proof),
1023 ).unwrap();
1024
1025 assert!(!result.proof.is_empty());
1026 assert!(!result.scope_hash.is_empty());
1027 assert!(!result.chain_hash.is_empty());
1028
1029 let is_valid = verify_proof_v21_unified(
1030 nonce,
1031 context_id,
1032 binding,
1033 timestamp,
1034 payload,
1035 &result.proof,
1036 &scope,
1037 &result.scope_hash,
1038 Some(previous_proof),
1039 &result.chain_hash,
1040 ).unwrap();
1041
1042 assert!(is_valid);
1043 }
1044
1045 #[test]
1046 fn test_unified_chain_broken() {
1047 let nonce = "test_nonce_12345";
1048 let context_id = "ctx_abc123";
1049 let binding = "POST /payment";
1050 let timestamp = "1234567890";
1051 let payload = r#"{"amount":500}"#;
1052 let previous_proof = "original_proof";
1053
1054 let client_secret = derive_client_secret(nonce, context_id, binding);
1055 let result = build_proof_v21_unified(
1056 &client_secret,
1057 timestamp,
1058 binding,
1059 payload,
1060 &[],
1061 Some(previous_proof),
1062 ).unwrap();
1063
1064 let is_valid = verify_proof_v21_unified(
1066 nonce,
1067 context_id,
1068 binding,
1069 timestamp,
1070 payload,
1071 &result.proof,
1072 &[],
1073 "",
1074 Some("tampered_proof"), &result.chain_hash,
1076 ).unwrap();
1077
1078 assert!(!is_valid);
1079 }
1080
1081 #[test]
1082 fn test_unified_scope_tampered() {
1083 let nonce = "test_nonce_12345";
1084 let context_id = "ctx_abc123";
1085 let binding = "POST /transfer";
1086 let timestamp = "1234567890";
1087 let payload = r#"{"amount":1000,"recipient":"user1"}"#;
1088 let scope = vec!["amount"];
1089
1090 let client_secret = derive_client_secret(nonce, context_id, binding);
1091 let result = build_proof_v21_unified(
1092 &client_secret,
1093 timestamp,
1094 binding,
1095 payload,
1096 &scope,
1097 None,
1098 ).unwrap();
1099
1100 let tampered_scope = vec!["recipient"];
1102 let is_valid = verify_proof_v21_unified(
1103 nonce,
1104 context_id,
1105 binding,
1106 timestamp,
1107 payload,
1108 &result.proof,
1109 &tampered_scope, &result.scope_hash, None,
1112 "",
1113 ).unwrap();
1114
1115 assert!(!is_valid);
1116 }
1117
1118 #[test]
1119 fn test_hash_proof() {
1120 let proof = "test_proof_123";
1121 let hash1 = hash_proof(proof);
1122 let hash2 = hash_proof(proof);
1123
1124 assert_eq!(hash1, hash2);
1125 assert_eq!(hash1.len(), 64); }
1127}