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