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 =
396 HmacSha256Type::new_from_slice(nonce.as_bytes()).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(
475 nonce, context_id, binding, timestamp, body_hash, &proof
476 ));
477 }
478
479 #[test]
480 fn test_hash_body() {
481 let hash = hash_body(r#"{"name":"John"}"#);
482 assert_eq!(hash.len(), 64); }
484}
485
486use serde_json::{Map, Value};
491
492pub fn extract_scoped_fields(payload: &Value, scope: &[&str]) -> Result<Value, AshError> {
494 if scope.is_empty() {
495 return Ok(payload.clone());
496 }
497
498 let mut result = Map::new();
499
500 for field_path in scope {
501 let value = get_nested_value(payload, field_path);
502 if let Some(v) = value {
503 set_nested_value(&mut result, field_path, v);
504 }
505 }
506
507 Ok(Value::Object(result))
508}
509
510fn get_nested_value(payload: &Value, path: &str) -> Option<Value> {
511 let parts: Vec<&str> = path.split('.').collect();
512 let mut current = payload;
513
514 for part in parts {
515 let (key, index) = parse_array_notation(part);
516
517 match current {
518 Value::Object(map) => {
519 current = map.get(key)?;
520 if let Some(idx) = index {
521 if let Value::Array(arr) = current {
522 current = arr.get(idx)?;
523 } else {
524 return None;
525 }
526 }
527 }
528 Value::Array(arr) => {
529 let idx: usize = key.parse().ok()?;
530 current = arr.get(idx)?;
531 }
532 _ => return None,
533 }
534 }
535
536 Some(current.clone())
537}
538
539fn parse_array_notation(part: &str) -> (&str, Option<usize>) {
540 if let Some(bracket_start) = part.find('[') {
541 if let Some(bracket_end) = part.find(']') {
542 let key = &part[..bracket_start];
543 let index_str = &part[bracket_start + 1..bracket_end];
544 if let Ok(index) = index_str.parse::<usize>() {
545 return (key, Some(index));
546 }
547 }
548 }
549 (part, None)
550}
551
552fn set_nested_value(result: &mut Map<String, Value>, path: &str, value: Value) {
553 let parts: Vec<&str> = path.split('.').collect();
554
555 if parts.len() == 1 {
556 let (key, _) = parse_array_notation(parts[0]);
557 result.insert(key.to_string(), value);
558 return;
559 }
560
561 let (first_key, _) = parse_array_notation(parts[0]);
562 let remaining_path = parts[1..].join(".");
563
564 let nested = result
565 .entry(first_key.to_string())
566 .or_insert_with(|| Value::Object(Map::new()));
567
568 if let Value::Object(nested_map) = nested {
569 set_nested_value(nested_map, &remaining_path, value);
570 }
571}
572pub fn build_proof_v21_scoped(
574 client_secret: &str,
575 timestamp: &str,
576 binding: &str,
577 payload: &str,
578 scope: &[&str],
579) -> Result<(String, String), AshError> {
580 let json_payload: Value = serde_json::from_str(payload)
581 .map_err(|e| AshError::canonicalization_failed(&format!("Invalid JSON: {}", e)))?;
582
583 let scoped_payload = extract_scoped_fields(&json_payload, scope)?;
584
585 let canonical_scoped = serde_json::to_string(&scoped_payload)
586 .map_err(|e| AshError::canonicalization_failed(&format!("Failed to serialize: {}", e)))?;
587
588 let body_hash = hash_body(&canonical_scoped);
589
590 let scope_str = scope.join(",");
591 let scope_hash = hash_body(&scope_str);
592
593 let message = format!("{}|{}|{}|{}", timestamp, binding, body_hash, scope_hash);
594 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
595 .expect("HMAC can take key of any size");
596 mac.update(message.as_bytes());
597 let proof = hex::encode(mac.finalize().into_bytes());
598
599 Ok((proof, scope_hash))
600}
601
602#[allow(clippy::too_many_arguments)]
604pub fn verify_proof_v21_scoped(
605 nonce: &str,
606 context_id: &str,
607 binding: &str,
608 timestamp: &str,
609 payload: &str,
610 scope: &[&str],
611 scope_hash: &str,
612 client_proof: &str,
613) -> Result<bool, AshError> {
614 let scope_str = scope.join(",");
615 let expected_scope_hash = hash_body(&scope_str);
616 if !timing_safe_equal(expected_scope_hash.as_bytes(), scope_hash.as_bytes()) {
617 return Ok(false);
618 }
619
620 let client_secret = derive_client_secret(nonce, context_id, binding);
621
622 let (expected_proof, _) =
623 build_proof_v21_scoped(&client_secret, timestamp, binding, payload, scope)?;
624
625 Ok(timing_safe_equal(
626 expected_proof.as_bytes(),
627 client_proof.as_bytes(),
628 ))
629}
630
631pub fn hash_scoped_body(payload: &str, scope: &[&str]) -> Result<String, AshError> {
633 let json_payload: Value = serde_json::from_str(payload)
634 .map_err(|e| AshError::canonicalization_failed(&format!("Invalid JSON: {}", e)))?;
635
636 let scoped_payload = extract_scoped_fields(&json_payload, scope)?;
637
638 let canonical_scoped = serde_json::to_string(&scoped_payload)
639 .map_err(|e| AshError::canonicalization_failed(&format!("Failed to serialize: {}", e)))?;
640
641 Ok(hash_body(&canonical_scoped))
642}
643
644#[cfg(test)]
645mod tests_v22_scoping {
646 use super::*;
647
648 #[test]
649 fn test_build_verify_scoped_proof() {
650 let nonce = "test_nonce_12345";
651 let context_id = "ctx_abc123";
652 let binding = "POST /transfer";
653 let timestamp = "1234567890";
654 let payload = r#"{"amount":1000,"recipient":"user1","notes":"hi"}"#;
655 let scope = vec!["amount", "recipient"];
656
657 let client_secret = derive_client_secret(nonce, context_id, binding);
658 let (proof, scope_hash) =
659 build_proof_v21_scoped(&client_secret, timestamp, binding, payload, &scope).unwrap();
660
661 let is_valid = verify_proof_v21_scoped(
662 nonce,
663 context_id,
664 binding,
665 timestamp,
666 payload,
667 &scope,
668 &scope_hash,
669 &proof,
670 )
671 .unwrap();
672
673 assert!(is_valid);
674 }
675
676 #[test]
677 fn test_scoped_proof_ignores_unscoped_changes() {
678 let nonce = "test_nonce_12345";
679 let context_id = "ctx_abc123";
680 let binding = "POST /transfer";
681 let timestamp = "1234567890";
682 let scope = vec!["amount", "recipient"];
683
684 let client_secret = derive_client_secret(nonce, context_id, binding);
685
686 let payload1 = r#"{"amount":1000,"recipient":"user1","notes":"hello"}"#;
687 let (proof, scope_hash) =
688 build_proof_v21_scoped(&client_secret, timestamp, binding, payload1, &scope).unwrap();
689
690 let payload2 = r#"{"amount":1000,"recipient":"user1","notes":"world"}"#;
691
692 let is_valid = verify_proof_v21_scoped(
693 nonce,
694 context_id,
695 binding,
696 timestamp,
697 payload2,
698 &scope,
699 &scope_hash,
700 &proof,
701 )
702 .unwrap();
703
704 assert!(is_valid);
705 }
706
707 #[test]
708 fn test_scoped_proof_detects_scoped_changes() {
709 let nonce = "test_nonce_12345";
710 let context_id = "ctx_abc123";
711 let binding = "POST /transfer";
712 let timestamp = "1234567890";
713 let scope = vec!["amount", "recipient"];
714
715 let client_secret = derive_client_secret(nonce, context_id, binding);
716
717 let payload1 = r#"{"amount":1000,"recipient":"user1","notes":"hello"}"#;
718 let (proof, scope_hash) =
719 build_proof_v21_scoped(&client_secret, timestamp, binding, payload1, &scope).unwrap();
720
721 let payload2 = r#"{"amount":9999,"recipient":"user1","notes":"hello"}"#;
722
723 let is_valid = verify_proof_v21_scoped(
724 nonce,
725 context_id,
726 binding,
727 timestamp,
728 payload2,
729 &scope,
730 &scope_hash,
731 &proof,
732 )
733 .unwrap();
734
735 assert!(!is_valid);
736 }
737}
738
739#[derive(Debug, Clone, PartialEq)]
745pub struct UnifiedProofResult {
746 pub proof: String,
748 pub scope_hash: String,
750 pub chain_hash: String,
752}
753
754pub fn hash_proof(proof: &str) -> String {
758 let mut hasher = Sha256::new();
759 hasher.update(proof.as_bytes());
760 hex::encode(hasher.finalize())
761}
762
763pub fn build_proof_v21_unified(
777 client_secret: &str,
778 timestamp: &str,
779 binding: &str,
780 payload: &str,
781 scope: &[&str],
782 previous_proof: Option<&str>,
783) -> Result<UnifiedProofResult, AshError> {
784 let json_payload: Value = serde_json::from_str(payload)
786 .map_err(|e| AshError::canonicalization_failed(&format!("Invalid JSON: {}", e)))?;
787
788 let scoped_payload = extract_scoped_fields(&json_payload, scope)?;
789
790 let canonical_scoped = serde_json::to_string(&scoped_payload)
791 .map_err(|e| AshError::canonicalization_failed(&format!("Failed to serialize: {}", e)))?;
792
793 let body_hash = hash_body(&canonical_scoped);
794
795 let scope_hash = if scope.is_empty() {
797 String::new()
798 } else {
799 hash_body(&scope.join(","))
800 };
801
802 let chain_hash = match previous_proof {
804 Some(prev) if !prev.is_empty() => hash_proof(prev),
805 _ => String::new(),
806 };
807
808 let message = format!(
810 "{}|{}|{}|{}|{}",
811 timestamp, binding, body_hash, scope_hash, chain_hash
812 );
813
814 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
815 .expect("HMAC can take key of any size");
816 mac.update(message.as_bytes());
817 let proof = hex::encode(mac.finalize().into_bytes());
818
819 Ok(UnifiedProofResult {
820 proof,
821 scope_hash,
822 chain_hash,
823 })
824}
825
826#[allow(clippy::too_many_arguments)]
830pub fn verify_proof_v21_unified(
831 nonce: &str,
832 context_id: &str,
833 binding: &str,
834 timestamp: &str,
835 payload: &str,
836 client_proof: &str,
837 scope: &[&str],
838 scope_hash: &str,
839 previous_proof: Option<&str>,
840 chain_hash: &str,
841) -> Result<bool, AshError> {
842 if !scope.is_empty() {
844 let expected_scope_hash = hash_body(&scope.join(","));
845 if !timing_safe_equal(expected_scope_hash.as_bytes(), scope_hash.as_bytes()) {
846 return Ok(false);
847 }
848 }
849
850 if let Some(prev) = previous_proof {
852 if !prev.is_empty() {
853 let expected_chain_hash = hash_proof(prev);
854 if !timing_safe_equal(expected_chain_hash.as_bytes(), chain_hash.as_bytes()) {
855 return Ok(false);
856 }
857 }
858 }
859
860 let client_secret = derive_client_secret(nonce, context_id, binding);
862
863 let result = build_proof_v21_unified(
864 &client_secret,
865 timestamp,
866 binding,
867 payload,
868 scope,
869 previous_proof,
870 )?;
871
872 Ok(timing_safe_equal(
873 result.proof.as_bytes(),
874 client_proof.as_bytes(),
875 ))
876}
877
878#[cfg(test)]
879mod tests_v23_unified {
880 use super::*;
881
882 #[test]
883 fn test_unified_basic() {
884 let nonce = "test_nonce_12345";
885 let context_id = "ctx_abc123";
886 let binding = "POST /api/test";
887 let timestamp = "1234567890";
888 let payload = r#"{"name":"John","age":30}"#;
889
890 let client_secret = derive_client_secret(nonce, context_id, binding);
891 let result = build_proof_v21_unified(
892 &client_secret,
893 timestamp,
894 binding,
895 payload,
896 &[], None, )
899 .unwrap();
900
901 assert!(!result.proof.is_empty());
902 assert!(result.scope_hash.is_empty());
903 assert!(result.chain_hash.is_empty());
904
905 let is_valid = verify_proof_v21_unified(
906 nonce,
907 context_id,
908 binding,
909 timestamp,
910 payload,
911 &result.proof,
912 &[],
913 "",
914 None,
915 "",
916 )
917 .unwrap();
918
919 assert!(is_valid);
920 }
921
922 #[test]
923 fn test_unified_scoped_only() {
924 let nonce = "test_nonce_12345";
925 let context_id = "ctx_abc123";
926 let binding = "POST /transfer";
927 let timestamp = "1234567890";
928 let payload = r#"{"amount":1000,"recipient":"user1","notes":"hi"}"#;
929 let scope = vec!["amount", "recipient"];
930
931 let client_secret = derive_client_secret(nonce, context_id, binding);
932 let result = build_proof_v21_unified(
933 &client_secret,
934 timestamp,
935 binding,
936 payload,
937 &scope,
938 None, )
940 .unwrap();
941
942 assert!(!result.proof.is_empty());
943 assert!(!result.scope_hash.is_empty());
944 assert!(result.chain_hash.is_empty());
945
946 let is_valid = verify_proof_v21_unified(
947 nonce,
948 context_id,
949 binding,
950 timestamp,
951 payload,
952 &result.proof,
953 &scope,
954 &result.scope_hash,
955 None,
956 "",
957 )
958 .unwrap();
959
960 assert!(is_valid);
961 }
962
963 #[test]
964 fn test_unified_chained_only() {
965 let nonce = "test_nonce_12345";
966 let context_id = "ctx_abc123";
967 let binding = "POST /checkout";
968 let timestamp = "1234567890";
969 let payload = r#"{"cart_id":"cart_123"}"#;
970 let previous_proof = "abc123def456";
971
972 let client_secret = derive_client_secret(nonce, context_id, binding);
973 let result = build_proof_v21_unified(
974 &client_secret,
975 timestamp,
976 binding,
977 payload,
978 &[], Some(previous_proof),
980 )
981 .unwrap();
982
983 assert!(!result.proof.is_empty());
984 assert!(result.scope_hash.is_empty());
985 assert!(!result.chain_hash.is_empty());
986
987 let is_valid = verify_proof_v21_unified(
988 nonce,
989 context_id,
990 binding,
991 timestamp,
992 payload,
993 &result.proof,
994 &[],
995 "",
996 Some(previous_proof),
997 &result.chain_hash,
998 )
999 .unwrap();
1000
1001 assert!(is_valid);
1002 }
1003
1004 #[test]
1005 fn test_unified_full() {
1006 let nonce = "test_nonce_12345";
1007 let context_id = "ctx_abc123";
1008 let binding = "POST /payment";
1009 let timestamp = "1234567890";
1010 let payload = r#"{"amount":500,"currency":"USD","notes":"tip"}"#;
1011 let scope = vec!["amount", "currency"];
1012 let previous_proof = "checkout_proof_xyz";
1013
1014 let client_secret = derive_client_secret(nonce, context_id, binding);
1015 let result = build_proof_v21_unified(
1016 &client_secret,
1017 timestamp,
1018 binding,
1019 payload,
1020 &scope,
1021 Some(previous_proof),
1022 )
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 )
1041 .unwrap();
1042
1043 assert!(is_valid);
1044 }
1045
1046 #[test]
1047 fn test_unified_chain_broken() {
1048 let nonce = "test_nonce_12345";
1049 let context_id = "ctx_abc123";
1050 let binding = "POST /payment";
1051 let timestamp = "1234567890";
1052 let payload = r#"{"amount":500}"#;
1053 let previous_proof = "original_proof";
1054
1055 let client_secret = derive_client_secret(nonce, context_id, binding);
1056 let result = build_proof_v21_unified(
1057 &client_secret,
1058 timestamp,
1059 binding,
1060 payload,
1061 &[],
1062 Some(previous_proof),
1063 )
1064 .unwrap();
1065
1066 let is_valid = verify_proof_v21_unified(
1068 nonce,
1069 context_id,
1070 binding,
1071 timestamp,
1072 payload,
1073 &result.proof,
1074 &[],
1075 "",
1076 Some("tampered_proof"), &result.chain_hash,
1078 )
1079 .unwrap();
1080
1081 assert!(!is_valid);
1082 }
1083
1084 #[test]
1085 fn test_unified_scope_tampered() {
1086 let nonce = "test_nonce_12345";
1087 let context_id = "ctx_abc123";
1088 let binding = "POST /transfer";
1089 let timestamp = "1234567890";
1090 let payload = r#"{"amount":1000,"recipient":"user1"}"#;
1091 let scope = vec!["amount"];
1092
1093 let client_secret = derive_client_secret(nonce, context_id, binding);
1094 let result =
1095 build_proof_v21_unified(&client_secret, timestamp, binding, payload, &scope, None)
1096 .unwrap();
1097
1098 let tampered_scope = vec!["recipient"];
1100 let is_valid = verify_proof_v21_unified(
1101 nonce,
1102 context_id,
1103 binding,
1104 timestamp,
1105 payload,
1106 &result.proof,
1107 &tampered_scope, &result.scope_hash, None,
1110 "",
1111 )
1112 .unwrap();
1113
1114 assert!(!is_valid);
1115 }
1116
1117 #[test]
1118 fn test_hash_proof() {
1119 let proof = "test_proof_123";
1120 let hash1 = hash_proof(proof);
1121 let hash2 = hash_proof(proof);
1122
1123 assert_eq!(hash1, hash2);
1124 assert_eq!(hash1.len(), 64); }
1126}