1use sha2::{Digest, Sha256};
97
98use crate::compare::ash_timing_safe_equal;
99use crate::errors::{AshError, AshErrorCode};
100
101pub const ASH_SDK_VERSION: &str = "2.3.5";
107
108pub const ASH_VERSION_PREFIX: &str = "ASHv2.1";
110
111use hmac::{Hmac, Mac};
116use sha2::Sha256 as HmacSha256;
117
118type HmacSha256Type = Hmac<HmacSha256>;
119
120const MIN_NONCE_BYTES: usize = 16;
122
123const MIN_NONCE_HEX_CHARS: usize = 32;
126
127const MAX_ARRAY_INDEX: usize = 10000;
131
132const MAX_TOTAL_ARRAY_ALLOCATION: usize = 10000;
135
136const MAX_SCOPE_PATH_DEPTH: usize = 32;
139
140const MAX_TIMESTAMP: u64 = 32503680000;
143
144const MAX_SCOPE_FIELDS: usize = 100;
147
148const SCOPE_FIELD_DELIMITER: char = '\x1F';
151
152const MAX_BINDING_LENGTH: usize = 8192; const MAX_CONTEXT_ID_LENGTH: usize = 256;
159
160const MAX_NONCE_LENGTH: usize = 512;
166
167const MAX_SCOPE_FIELD_NAME_LENGTH: usize = 64;
170
171const MAX_TOTAL_SCOPE_LENGTH: usize = 4096;
174
175pub fn ash_generate_nonce(bytes: usize) -> Result<String, AshError> {
191 if bytes < MIN_NONCE_BYTES {
193 return Err(AshError::new(
194 AshErrorCode::ValidationError,
195 format!("Nonce must be at least {} bytes for adequate entropy", MIN_NONCE_BYTES),
196 ));
197 }
198
199 use getrandom::getrandom;
200 let mut buf = vec![0u8; bytes];
201 getrandom(&mut buf).map_err(|e| {
202 AshError::new(
203 AshErrorCode::InternalError,
204 format!("Random number generation failed: {}", e),
205 )
206 })?;
207 Ok(hex::encode(buf))
208}
209
210pub fn ash_generate_nonce_or_panic(bytes: usize) -> String {
222 ash_generate_nonce(bytes).expect("Nonce generation failed (check byte count >= 16 and RNG availability)")
223}
224
225pub fn ash_generate_context_id() -> Result<String, AshError> {
230 Ok(format!("ash_{}", ash_generate_nonce(16)?))
231}
232
233pub fn ash_generate_context_id_256() -> Result<String, AshError> {
238 Ok(format!("ash_{}", ash_generate_nonce(32)?))
239}
240
241pub fn ash_derive_client_secret(nonce: &str, context_id: &str, binding: &str) -> Result<String, AshError> {
276 crate::validate::ash_validate_nonce(nonce)?;
278
279 if context_id.is_empty() {
281 return Err(AshError::new(
282 AshErrorCode::ValidationError,
283 "context_id cannot be empty",
284 ));
285 }
286
287 if context_id.len() > MAX_CONTEXT_ID_LENGTH {
289 return Err(AshError::new(
290 AshErrorCode::ValidationError,
291 format!("context_id exceeds maximum length of {} characters", MAX_CONTEXT_ID_LENGTH),
292 ));
293 }
294
295 if !context_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.') {
298 return Err(AshError::new(
299 AshErrorCode::ValidationError,
300 "context_id must contain only ASCII alphanumeric characters, underscore, hyphen, or dot",
301 ));
302 }
303
304 if binding.is_empty() {
309 return Err(AshError::new(
310 AshErrorCode::ValidationError,
311 "binding cannot be empty",
312 ));
313 }
314
315 if binding.len() > MAX_BINDING_LENGTH {
317 return Err(AshError::new(
318 AshErrorCode::ValidationError,
319 format!("binding exceeds maximum length of {} bytes", MAX_BINDING_LENGTH),
320 ));
321 }
322
323 let mut mac =
324 HmacSha256Type::new_from_slice(nonce.as_bytes()).expect("HMAC can take key of any size");
325 mac.update(format!("{}|{}", context_id, binding).as_bytes());
326 Ok(hex::encode(mac.finalize().into_bytes()))
327}
328
329const SHA256_HEX_LENGTH: usize = 64;
331
332pub fn ash_build_proof(
351 client_secret: &str,
352 timestamp: &str,
353 binding: &str,
354 body_hash: &str,
355) -> Result<String, AshError> {
356 if client_secret.is_empty() {
358 return Err(AshError::new(
359 AshErrorCode::ValidationError,
360 "client_secret cannot be empty",
361 ));
362 }
363 if timestamp.is_empty() {
364 return Err(AshError::new(
365 AshErrorCode::ValidationError,
366 "timestamp cannot be empty",
367 ));
368 }
369 if binding.is_empty() {
370 return Err(AshError::new(
371 AshErrorCode::ValidationError,
372 "binding cannot be empty",
373 ));
374 }
375
376 if binding.len() > MAX_BINDING_LENGTH {
378 return Err(AshError::new(
379 AshErrorCode::ValidationError,
380 format!("binding exceeds maximum length of {} bytes", MAX_BINDING_LENGTH),
381 ));
382 }
383
384 if body_hash.len() != SHA256_HEX_LENGTH {
386 return Err(AshError::new(
387 AshErrorCode::ValidationError,
388 format!(
389 "body_hash must be {} hex characters (SHA-256), got {}",
390 SHA256_HEX_LENGTH,
391 body_hash.len()
392 ),
393 ));
394 }
395 if !body_hash.chars().all(|c| c.is_ascii_hexdigit()) {
396 return Err(AshError::new(
397 AshErrorCode::ValidationError,
398 "body_hash must contain only hexadecimal characters (0-9, a-f, A-F)",
399 ));
400 }
401
402 let message = format!("{}|{}|{}", timestamp, binding, body_hash);
403 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
404 .expect("HMAC can take key of any size");
405 mac.update(message.as_bytes());
406 Ok(hex::encode(mac.finalize().into_bytes()))
407}
408
409pub fn ash_verify_proof(
421 nonce: &str,
422 context_id: &str,
423 binding: &str,
424 timestamp: &str,
425 body_hash: &str,
426 client_proof: &str,
427) -> Result<bool, AshError> {
428 ash_validate_timestamp_format(timestamp)?;
430
431 let client_secret = ash_derive_client_secret(nonce, context_id, binding)?;
432 let expected_proof = ash_build_proof(&client_secret, timestamp, binding, body_hash)?;
433 Ok(ash_timing_safe_equal(expected_proof.as_bytes(), client_proof.as_bytes()))
434}
435
436#[allow(clippy::too_many_arguments)]
484pub fn ash_verify_proof_with_freshness(
485 nonce: &str,
486 context_id: &str,
487 binding: &str,
488 timestamp: &str,
489 body_hash: &str,
490 client_proof: &str,
491 max_age_seconds: u64,
492 clock_skew_seconds: u64,
493) -> Result<bool, AshError> {
494 ash_validate_timestamp(timestamp, max_age_seconds, clock_skew_seconds)?;
496
497 let client_secret = ash_derive_client_secret(nonce, context_id, binding)?;
499 let expected_proof = ash_build_proof(&client_secret, timestamp, binding, body_hash)?;
500 Ok(ash_timing_safe_equal(expected_proof.as_bytes(), client_proof.as_bytes()))
501}
502
503pub fn ash_hash_body(canonical_body: &str) -> String {
505 let mut hasher = Sha256::new();
506 hasher.update(canonical_body.as_bytes());
507 hex::encode(hasher.finalize())
508}
509
510fn ash_normalize_scope(scope: &[&str]) -> Vec<String> {
513 let mut sorted: Vec<String> = scope.iter().map(|s| s.to_string()).collect();
514 sorted.sort();
515 sorted.dedup();
516 sorted
517}
518
519fn ash_join_scope_fields(scope: &[&str]) -> Result<String, AshError> {
527 let mut total_length: usize = 0;
528
529 for field in scope {
530 if field.is_empty() {
532 return Err(AshError::new(
533 AshErrorCode::ValidationError,
534 "Scope field names cannot be empty",
535 ));
536 }
537
538 if field.len() > MAX_SCOPE_FIELD_NAME_LENGTH {
540 return Err(AshError::new(
541 AshErrorCode::ValidationError,
542 format!("Scope field name exceeds maximum length of {} characters", MAX_SCOPE_FIELD_NAME_LENGTH),
543 ));
544 }
545
546 total_length = total_length.saturating_add(field.len()).saturating_add(1);
548
549 if field.contains(SCOPE_FIELD_DELIMITER) {
552 return Err(AshError::new(
553 AshErrorCode::ValidationError,
554 "Scope field contains reserved delimiter character (U+001F)",
555 ));
556 }
557 }
558
559 if total_length > MAX_TOTAL_SCOPE_LENGTH {
561 return Err(AshError::new(
562 AshErrorCode::ValidationError,
563 format!("Total scope length exceeds maximum of {} bytes", MAX_TOTAL_SCOPE_LENGTH),
564 ));
565 }
566
567 let normalized = ash_normalize_scope(scope);
568 Ok(normalized.join(&SCOPE_FIELD_DELIMITER.to_string()))
569}
570
571pub fn ash_hash_scope(scope: &[&str]) -> Result<String, AshError> {
578 if scope.is_empty() {
579 return Ok(String::new());
580 }
581 Ok(ash_hash_body(&ash_join_scope_fields(scope)?))
582}
583
584#[cfg(test)]
585mod tests_proof {
586 use super::*;
587
588 const TEST_NONCE: &str = "0123456789abcdef0123456789abcdef"; const TEST_NONCE_2: &str = "fedcba9876543210fedcba9876543210"; const TEST_BODY_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
593 #[allow(dead_code)]
594 const TEST_BODY_HASH_2: &str = "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592";
595
596 #[test]
597 fn test_derive_client_secret_deterministic() {
598 let secret1 = ash_derive_client_secret(TEST_NONCE, "ctx_abc", "POST /login").unwrap();
599 let secret2 = ash_derive_client_secret(TEST_NONCE, "ctx_abc", "POST /login").unwrap();
600 assert_eq!(secret1, secret2);
601 }
602
603 #[test]
604 fn test_derive_client_secret_different_inputs() {
605 let secret1 = ash_derive_client_secret(TEST_NONCE, "ctx_abc", "POST /login").unwrap();
606 let secret2 = ash_derive_client_secret(TEST_NONCE_2, "ctx_abc", "POST /login").unwrap();
607 assert_ne!(secret1, secret2);
608 }
609
610 #[test]
611 fn test_derive_client_secret_rejects_short_nonce() {
612 let result = ash_derive_client_secret("short", "ctx_abc", "POST /login");
614 assert!(result.is_err());
615 assert!(result.unwrap_err().message().contains("hex characters"));
616 }
617
618 #[test]
619 fn test_derive_client_secret_rejects_non_hex_nonce() {
620 let result = ash_derive_client_secret("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "ctx_abc", "POST /login");
622 assert!(result.is_err());
623 assert!(result.unwrap_err().message().contains("hexadecimal"));
624 }
625
626 #[test]
627 fn test_derive_client_secret_rejects_delimiter_in_context_id() {
628 let result = ash_derive_client_secret(TEST_NONCE, "ctx|abc", "POST /login");
631 assert!(result.is_err());
632 let msg = result.unwrap_err().message().to_lowercase();
634 assert!(msg.contains("delimiter") || msg.contains("alphanumeric") || msg.contains("character"));
635 }
636
637 #[test]
638 fn test_derive_client_secret_allows_delimiter_in_binding() {
639 let result = ash_derive_client_secret(TEST_NONCE, "ctx_abc", "POST|/login|");
642 assert!(result.is_ok());
643 }
644
645 #[test]
646 fn test_derive_client_secret_rejects_empty_context_id() {
647 let result = ash_derive_client_secret(TEST_NONCE, "", "POST /login");
649 assert!(result.is_err());
650 assert!(result.unwrap_err().message().contains("empty"));
651 }
652
653 #[test]
654 fn test_build_proof_deterministic() {
655 let proof1 = ash_build_proof("secret", "1234567890", "POST /login", TEST_BODY_HASH).unwrap();
656 let proof2 = ash_build_proof("secret", "1234567890", "POST /login", TEST_BODY_HASH).unwrap();
657 assert_eq!(proof1, proof2);
658 }
659
660 #[test]
661 fn test_build_proof_rejects_empty_inputs() {
662 assert!(ash_build_proof("", "1234567890", "POST /login", TEST_BODY_HASH).is_err());
664 assert!(ash_build_proof("secret", "", "POST /login", TEST_BODY_HASH).is_err());
665 assert!(ash_build_proof("secret", "1234567890", "", TEST_BODY_HASH).is_err());
666 assert!(ash_build_proof("secret", "1234567890", "POST /login", "").is_err());
668 }
669
670 #[test]
671 fn test_build_proof_rejects_invalid_body_hash() {
672 assert!(ash_build_proof("secret", "1234567890", "POST /login", "abc123").is_err());
675 let too_long = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855aa";
677 assert!(ash_build_proof("secret", "1234567890", "POST /login", too_long).is_err());
678 let non_hex = "g3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
680 assert!(ash_build_proof("secret", "1234567890", "POST /login", non_hex).is_err());
681 }
682
683 #[test]
684 fn test_ash_verify_proof() {
685 let nonce = TEST_NONCE;
686 let context_id = "ctx_abc";
687 let binding = "POST /login";
688 let timestamp = "1234567890";
689
690 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
691 let proof = ash_build_proof(&client_secret, timestamp, binding, TEST_BODY_HASH).unwrap();
692
693 assert!(ash_verify_proof(
694 nonce, context_id, binding, timestamp, TEST_BODY_HASH, &proof
695 ).unwrap());
696 }
697
698 #[test]
699 fn test_ash_hash_body() {
700 let hash = ash_hash_body(r#"{"name":"John"}"#);
701 assert_eq!(hash.len(), 64); }
703
704 #[test]
705 fn test_timestamp_rejects_leading_zeros() {
706 let result = ash_validate_timestamp_format("0123456789");
708 assert!(result.is_err());
709 assert!(result.unwrap_err().message().contains("leading zeros"));
710
711 let result = ash_validate_timestamp_format("0");
713 assert!(result.is_ok());
714 }
715
716 #[test]
718 fn test_context_id_max_length() {
719 let long_context = "a".repeat(257); let result = ash_derive_client_secret(TEST_NONCE, &long_context, "POST|/api|");
721 assert!(result.is_err());
722 assert!(result.unwrap_err().message().contains("maximum length"));
723 }
724
725 #[test]
726 fn test_context_id_at_max_length() {
727 let max_context = "a".repeat(256); let result = ash_derive_client_secret(TEST_NONCE, &max_context, "POST|/api|");
729 assert!(result.is_ok());
730 }
731
732 #[test]
733 fn test_context_id_rejects_invalid_chars() {
734 let result = ash_derive_client_secret(TEST_NONCE, "ctx with space", "POST|/api|");
736 assert!(result.is_err());
737 assert!(result.unwrap_err().message().contains("alphanumeric"));
738
739 let result = ash_derive_client_secret(TEST_NONCE, "ctx@special", "POST|/api|");
740 assert!(result.is_err());
741
742 let result = ash_derive_client_secret(TEST_NONCE, "ctx\x00null", "POST|/api|");
743 assert!(result.is_err());
744 }
745
746 #[test]
747 fn test_context_id_allows_valid_chars() {
748 let result = ash_derive_client_secret(TEST_NONCE, "ctx_ABC-123.test", "POST|/api|");
750 assert!(result.is_ok());
751 }
752
753 #[test]
755 fn test_nonce_max_length() {
756 let long_nonce = "0".repeat(513); let result = ash_derive_client_secret(&long_nonce, "ctx_test", "POST|/api|");
758 assert!(result.is_err());
759 assert!(result.unwrap_err().message().contains("maximum length"));
760 }
761
762 #[test]
763 fn test_nonce_at_max_length() {
764 let max_nonce = "0".repeat(512); let result = ash_derive_client_secret(&max_nonce, "ctx_test", "POST|/api|");
766 assert!(result.is_ok());
767 }
768}
769
770#[cfg(test)]
772mod tests_sec_scope_001 {
773 use super::*;
774
775 #[test]
776 fn test_scope_field_name_max_length() {
777 let long_field = "a".repeat(65); let scope = vec![long_field.as_str()];
779 let result = ash_hash_scope(&scope);
780 assert!(result.is_err());
781 assert!(result.unwrap_err().message().contains("maximum length"));
782 }
783
784 #[test]
785 fn test_scope_field_name_at_max_length() {
786 let max_field = "a".repeat(64); let scope = vec![max_field.as_str()];
788 let result = ash_hash_scope(&scope);
789 assert!(result.is_ok());
790 }
791
792 #[test]
793 fn test_scope_total_length_limit() {
794 let fields: Vec<String> = (0..100).map(|i| format!("field_{:045}", i)).collect();
797 let scope: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
798 let result = ash_hash_scope(&scope);
799 assert!(result.is_err());
800 assert!(result.unwrap_err().message().contains("Total scope length"));
801 }
802
803 #[test]
804 fn test_scope_within_total_length_limit() {
805 let fields: Vec<String> = (0..50).map(|i| format!("field_{:043}", i)).collect();
807 let scope: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
808 let result = ash_hash_scope(&scope);
809 assert!(result.is_ok());
810 }
811}
812
813use serde_json::{Map, Value};
818
819use crate::canonicalize::ash_canonicalize_json_value;
820
821pub const DEFAULT_MAX_TIMESTAMP_AGE_SECONDS: u64 = 300;
823
824pub const DEFAULT_CLOCK_SKEW_SECONDS: u64 = 60;
826
827pub fn ash_validate_timestamp_format(timestamp: &str) -> Result<u64, AshError> {
840 if timestamp.is_empty() {
842 return Err(AshError::new(
843 AshErrorCode::TimestampInvalid,
844 "Timestamp cannot be empty",
845 ));
846 }
847
848 if !timestamp.chars().all(|c| c.is_ascii_digit()) {
849 return Err(AshError::new(
850 AshErrorCode::TimestampInvalid,
851 "Timestamp must contain only digits (0-9)",
852 ));
853 }
854
855 if timestamp.len() > 1 && timestamp.starts_with('0') {
858 return Err(AshError::new(
859 AshErrorCode::TimestampInvalid,
860 "Timestamp must not have leading zeros",
861 ));
862 }
863
864 let ts: u64 = timestamp.parse().map_err(|_| {
866 AshError::new(
867 AshErrorCode::TimestampInvalid,
868 "Timestamp must be a valid integer",
869 )
870 })?;
871
872 if ts > MAX_TIMESTAMP {
874 return Err(AshError::new(
875 AshErrorCode::TimestampInvalid,
876 "Timestamp exceeds maximum allowed value",
877 ));
878 }
879
880 Ok(ts)
881}
882
883pub fn ash_validate_timestamp(
919 timestamp: &str,
920 max_age_seconds: u64,
921 clock_skew_seconds: u64,
922) -> Result<(), AshError> {
923 use std::time::{SystemTime, UNIX_EPOCH};
924
925 let ts = ash_validate_timestamp_format(timestamp)?;
927
928 let now = SystemTime::now()
930 .duration_since(UNIX_EPOCH)
931 .map_err(|_| {
932 AshError::new(
933 AshErrorCode::InternalError,
934 "System time error",
935 )
936 })?
937 .as_secs();
938
939 if ts > now.saturating_add(clock_skew_seconds) {
942 return Err(AshError::new(
943 AshErrorCode::TimestampInvalid,
944 "Timestamp is in the future",
945 ));
946 }
947
948 if now > ts && now - ts > max_age_seconds {
950 return Err(AshError::new(
951 AshErrorCode::TimestampInvalid,
952 "Timestamp has expired",
953 ));
954 }
955
956 Ok(())
957}
958
959pub fn ash_extract_scoped_fields(payload: &Value, scope: &[&str]) -> Result<Value, AshError> {
980 ash_extract_scoped_fields_internal(payload, scope, false)
981}
982
983pub fn ash_extract_scoped_fields_strict(
1009 payload: &Value,
1010 scope: &[&str],
1011 strict: bool,
1012) -> Result<Value, AshError> {
1013 ash_extract_scoped_fields_internal(payload, scope, strict)
1014}
1015
1016fn ash_extract_scoped_fields_internal(
1018 payload: &Value,
1019 scope: &[&str],
1020 strict: bool,
1021) -> Result<Value, AshError> {
1022 if scope.is_empty() {
1023 return Ok(payload.clone());
1024 }
1025
1026 if scope.len() > MAX_SCOPE_FIELDS {
1028 return Err(AshError::new(
1029 AshErrorCode::ValidationError,
1030 format!("Scope exceeds maximum of {} fields", MAX_SCOPE_FIELDS),
1031 ));
1032 }
1033
1034 let total_allocation = ash_calculate_total_array_allocation(scope);
1036 if total_allocation > MAX_TOTAL_ARRAY_ALLOCATION {
1037 return Err(AshError::new(
1038 AshErrorCode::ValidationError,
1039 format!(
1040 "Scope array indices exceed maximum total allocation of {} elements",
1041 MAX_TOTAL_ARRAY_ALLOCATION
1042 ),
1043 ));
1044 }
1045
1046 let mut result = Map::new();
1047
1048 for field_path in scope {
1049 let value = ash_get_nested_value(payload, field_path);
1050 if let Some(v) = value {
1051 ash_set_nested_value(&mut result, field_path, v);
1052 } else if strict {
1053 return Err(AshError::new(
1055 AshErrorCode::ScopedFieldMissing,
1056 format!("Required scoped field missing: {}", field_path),
1057 ));
1058 }
1059 }
1060
1061 Ok(Value::Object(result))
1062}
1063
1064fn ash_calculate_total_array_allocation(scope: &[&str]) -> usize {
1068 let mut total = 0usize;
1069 for path in scope {
1070 for part in path.split('.') {
1071 let notation = ash_parse_all_array_indices(part);
1072 for idx in ¬ation.indices {
1073 total = total.saturating_add(idx.saturating_add(1));
1076 }
1077 }
1078 }
1079 total
1080}
1081
1082fn ash_get_nested_value(payload: &Value, path: &str) -> Option<Value> {
1083 ash_get_nested_value_with_depth(payload, path, 0)
1084}
1085
1086fn ash_get_nested_value_with_depth(payload: &Value, path: &str, depth: usize) -> Option<Value> {
1089 if depth > MAX_SCOPE_PATH_DEPTH {
1091 return None;
1092 }
1093
1094 let parts: Vec<&str> = path.split('.').collect();
1095
1096 if parts.len() > MAX_SCOPE_PATH_DEPTH {
1098 return None;
1099 }
1100
1101 let mut current = payload;
1102
1103 for part in parts {
1104 let indices = ash_parse_all_array_indices(part);
1106
1107 match current {
1108 Value::Object(map) => {
1109 current = map.get(indices.key)?;
1110 for idx in &indices.indices {
1112 if *idx > MAX_ARRAY_INDEX {
1114 return None;
1115 }
1116 if let Value::Array(arr) = current {
1117 current = arr.get(*idx)?;
1118 } else {
1119 return None;
1120 }
1121 }
1122 }
1123 Value::Array(arr) => {
1124 let idx: usize = indices.key.parse().ok()?;
1126 if idx > MAX_ARRAY_INDEX {
1128 return None;
1129 }
1130 current = arr.get(idx)?;
1131 for idx in &indices.indices {
1133 if *idx > MAX_ARRAY_INDEX {
1134 return None;
1135 }
1136 if let Value::Array(arr) = current {
1137 current = arr.get(*idx)?;
1138 } else {
1139 return None;
1140 }
1141 }
1142 }
1143 _ => return None,
1144 }
1145 }
1146
1147 Some(current.clone())
1148}
1149
1150struct ArrayNotation<'a> {
1153 key: &'a str,
1155 indices: Vec<usize>,
1157}
1158
1159fn ash_parse_all_array_indices(part: &str) -> ArrayNotation<'_> {
1172 let bracket_start = match part.find('[') {
1173 Some(pos) => pos,
1174 None => return ArrayNotation { key: part, indices: vec![] },
1175 };
1176
1177 let key = &part[..bracket_start];
1178 let mut indices = Vec::new();
1179 let mut remaining = &part[bracket_start..];
1180
1181 while remaining.starts_with('[') {
1183 let bracket_end = match remaining.find(']') {
1184 Some(pos) => pos,
1185 None => break, };
1187
1188 let index_str = &remaining[1..bracket_end];
1189 if index_str.is_empty() {
1190 break; }
1192
1193 match index_str.parse::<usize>() {
1194 Ok(idx) => indices.push(idx),
1195 Err(_) => break, }
1197
1198 remaining = &remaining[bracket_end + 1..];
1199 }
1200
1201 if !remaining.is_empty() {
1204 return ArrayNotation { key, indices: vec![] };
1207 }
1208
1209 ArrayNotation { key, indices }
1210}
1211
1212fn ash_set_nested_value(result: &mut Map<String, Value>, path: &str, value: Value) {
1222 ash_set_nested_value_with_depth(result, path, value, 0);
1223}
1224
1225fn ash_set_nested_value_with_depth(result: &mut Map<String, Value>, path: &str, value: Value, depth: usize) {
1227 if depth > MAX_SCOPE_PATH_DEPTH {
1229 return;
1230 }
1231
1232 let parts: Vec<&str> = path.split('.').collect();
1233 if parts.len() > MAX_SCOPE_PATH_DEPTH {
1234 return; }
1236
1237 if parts.len() == 1 {
1238 let notation = ash_parse_all_array_indices(parts[0]);
1239 if notation.indices.is_empty() {
1240 result.insert(notation.key.to_string(), value);
1242 } else {
1243 ash_set_value_at_indices(result, notation.key, ¬ation.indices, value);
1245 }
1246 return;
1247 }
1248
1249 let notation = ash_parse_all_array_indices(parts[0]);
1250 let remaining_path = parts[1..].join(".");
1251
1252 if notation.indices.is_empty() {
1253 let nested = result
1255 .entry(notation.key.to_string())
1256 .or_insert_with(|| Value::Object(Map::new()));
1257
1258 if let Value::Object(nested_map) = nested {
1259 ash_set_nested_value_with_depth(nested_map, &remaining_path, value, depth + 1);
1260 }
1261 } else {
1262 let target = ash_get_or_create_at_indices(result, notation.key, ¬ation.indices);
1264 if let Some(Value::Object(nested_map)) = target {
1265 ash_set_nested_value_with_depth(nested_map, &remaining_path, value, depth + 1);
1266 }
1267 }
1268}
1269
1270fn ash_set_value_at_indices(result: &mut Map<String, Value>, key: &str, indices: &[usize], value: Value) {
1273 if indices.is_empty() {
1274 result.insert(key.to_string(), value);
1275 return;
1276 }
1277
1278 for idx in indices {
1280 if *idx > MAX_ARRAY_INDEX {
1281 return; }
1283 }
1284
1285 let arr = result
1287 .entry(key.to_string())
1288 .or_insert_with(|| Value::Array(Vec::new()));
1289
1290 ash_set_value_in_nested_array(arr, indices, value);
1291}
1292
1293fn ash_set_value_in_nested_array(current: &mut Value, indices: &[usize], value: Value) {
1295 if indices.is_empty() {
1296 *current = value;
1297 return;
1298 }
1299
1300 let idx = indices[0];
1301 let remaining = &indices[1..];
1302
1303 if !current.is_array() {
1305 *current = Value::Array(Vec::new());
1306 }
1307
1308 if let Value::Array(arr) = current {
1309 while arr.len() <= idx {
1311 if remaining.is_empty() {
1312 arr.push(Value::Null);
1313 } else {
1314 arr.push(Value::Array(Vec::new()));
1315 }
1316 }
1317
1318 if remaining.is_empty() {
1319 arr[idx] = value;
1320 } else {
1321 ash_set_value_in_nested_array(&mut arr[idx], remaining, value);
1322 }
1323 }
1324}
1325
1326fn ash_get_or_create_at_indices<'a>(
1329 result: &'a mut Map<String, Value>,
1330 key: &str,
1331 indices: &[usize],
1332) -> Option<&'a mut Value> {
1333 if indices.is_empty() {
1334 return result.get_mut(key);
1335 }
1336
1337 for idx in indices {
1339 if *idx > MAX_ARRAY_INDEX {
1340 return None;
1341 }
1342 }
1343
1344 let arr = result
1346 .entry(key.to_string())
1347 .or_insert_with(|| Value::Array(Vec::new()));
1348
1349 ash_navigate_to_nested_index(arr, indices)
1350}
1351
1352fn ash_navigate_to_nested_index<'a>(current: &'a mut Value, indices: &[usize]) -> Option<&'a mut Value> {
1354 if indices.is_empty() {
1355 return Some(current);
1356 }
1357
1358 let idx = indices[0];
1359 let remaining = &indices[1..];
1360
1361 if !current.is_array() {
1363 *current = Value::Array(Vec::new());
1364 }
1365
1366 if let Value::Array(arr) = current {
1367 while arr.len() <= idx {
1369 if remaining.is_empty() {
1370 arr.push(Value::Object(Map::new()));
1371 } else {
1372 arr.push(Value::Array(Vec::new()));
1373 }
1374 }
1375
1376 if remaining.is_empty() {
1377 if !arr[idx].is_object() {
1379 arr[idx] = Value::Object(Map::new());
1380 }
1381 Some(&mut arr[idx])
1382 } else {
1383 ash_navigate_to_nested_index(&mut arr[idx], remaining)
1384 }
1385 } else {
1386 None
1387 }
1388}
1389pub fn ash_build_proof_scoped(
1401 client_secret: &str,
1402 timestamp: &str,
1403 binding: &str,
1404 payload: &str,
1405 scope: &[&str],
1406) -> Result<(String, String), AshError> {
1407 if client_secret.is_empty() {
1409 return Err(AshError::new(
1410 AshErrorCode::ValidationError,
1411 "client_secret cannot be empty",
1412 ));
1413 }
1414 if timestamp.is_empty() {
1415 return Err(AshError::new(
1416 AshErrorCode::ValidationError,
1417 "timestamp cannot be empty",
1418 ));
1419 }
1420 if binding.is_empty() {
1421 return Err(AshError::new(
1422 AshErrorCode::ValidationError,
1423 "binding cannot be empty",
1424 ));
1425 }
1426
1427 let json_payload: Value = if payload.is_empty() || payload.trim().is_empty() {
1429 Value::Object(serde_json::Map::new())
1430 } else {
1431 serde_json::from_str(payload)
1433 .map_err(|_e| AshError::canonicalization_error())?
1434 };
1435
1436 let scoped_payload = ash_extract_scoped_fields(&json_payload, scope)?;
1437
1438 let canonical_scoped = ash_canonicalize_json_value(&scoped_payload)?;
1440
1441 let body_hash = ash_hash_body(&canonical_scoped);
1442
1443 let scope_hash = ash_hash_scope(scope)?;
1445
1446 let message = format!("{}|{}|{}|{}", timestamp, binding, body_hash, scope_hash);
1447 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
1448 .expect("HMAC can take key of any size");
1449 mac.update(message.as_bytes());
1450 let proof = hex::encode(mac.finalize().into_bytes());
1451
1452 Ok((proof, scope_hash))
1453}
1454
1455#[allow(clippy::too_many_arguments)]
1457pub fn ash_verify_proof_scoped(
1458 nonce: &str,
1459 context_id: &str,
1460 binding: &str,
1461 timestamp: &str,
1462 payload: &str,
1463 scope: &[&str],
1464 scope_hash: &str,
1465 client_proof: &str,
1466) -> Result<bool, AshError> {
1467 ash_validate_timestamp_format(timestamp)?;
1469
1470 if scope.is_empty() && !scope_hash.is_empty() {
1472 return Err(AshError::new(
1473 AshErrorCode::ScopeMismatch,
1474 "scope_hash must be empty when scope is empty",
1475 ));
1476 }
1477
1478 let expected_scope_hash = ash_hash_scope(scope)?;
1480 if !ash_timing_safe_equal(expected_scope_hash.as_bytes(), scope_hash.as_bytes()) {
1481 return Ok(false);
1482 }
1483
1484 let client_secret = ash_derive_client_secret(nonce, context_id, binding)?;
1485
1486 let (expected_proof, _) =
1487 ash_build_proof_scoped(&client_secret, timestamp, binding, payload, scope)?;
1488
1489 Ok(ash_timing_safe_equal(
1490 expected_proof.as_bytes(),
1491 client_proof.as_bytes(),
1492 ))
1493}
1494
1495pub fn ash_hash_scoped_body(payload: &str, scope: &[&str]) -> Result<String, AshError> {
1500 ash_hash_scoped_body_internal(payload, scope, false)
1501}
1502
1503pub fn ash_hash_scoped_body_strict(payload: &str, scope: &[&str]) -> Result<String, AshError> {
1522 ash_hash_scoped_body_internal(payload, scope, true)
1523}
1524
1525fn ash_hash_scoped_body_internal(payload: &str, scope: &[&str], strict: bool) -> Result<String, AshError> {
1528 let json_payload: Value = if payload.is_empty() || payload.trim().is_empty() {
1530 Value::Object(serde_json::Map::new())
1531 } else {
1532 serde_json::from_str(payload)
1534 .map_err(|_e| AshError::canonicalization_error())?
1535 };
1536
1537 let scoped_payload = ash_extract_scoped_fields_internal(&json_payload, scope, strict)?;
1538
1539 let canonical_scoped = ash_canonicalize_json_value(&scoped_payload)?;
1541
1542 Ok(ash_hash_body(&canonical_scoped))
1543}
1544
1545#[cfg(test)]
1546mod tests_scoping {
1547 use super::*;
1548
1549 const TEST_NONCE: &str = "0123456789abcdef0123456789abcdef";
1551
1552 #[test]
1553 fn test_build_verify_scoped_proof() {
1554 let nonce = TEST_NONCE;
1555 let context_id = "ctx_abc123";
1556 let binding = "POST /transfer";
1557 let timestamp = "1234567890";
1558 let payload = r#"{"amount":1000,"recipient":"user1","notes":"hi"}"#;
1559 let scope = vec!["amount", "recipient"];
1560
1561 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
1562 let (proof, scope_hash) =
1563 ash_build_proof_scoped(&client_secret, timestamp, binding, payload, &scope).unwrap();
1564
1565 let is_valid = ash_verify_proof_scoped(
1566 nonce,
1567 context_id,
1568 binding,
1569 timestamp,
1570 payload,
1571 &scope,
1572 &scope_hash,
1573 &proof,
1574 )
1575 .unwrap();
1576
1577 assert!(is_valid);
1578 }
1579
1580 #[test]
1581 fn test_scoped_proof_ignores_unscoped_changes() {
1582 let nonce = TEST_NONCE;
1583 let context_id = "ctx_abc123";
1584 let binding = "POST /transfer";
1585 let timestamp = "1234567890";
1586 let scope = vec!["amount", "recipient"];
1587
1588 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
1589
1590 let payload1 = r#"{"amount":1000,"recipient":"user1","notes":"hello"}"#;
1591 let (proof, scope_hash) =
1592 ash_build_proof_scoped(&client_secret, timestamp, binding, payload1, &scope).unwrap();
1593
1594 let payload2 = r#"{"amount":1000,"recipient":"user1","notes":"world"}"#;
1595
1596 let is_valid = ash_verify_proof_scoped(
1597 nonce,
1598 context_id,
1599 binding,
1600 timestamp,
1601 payload2,
1602 &scope,
1603 &scope_hash,
1604 &proof,
1605 )
1606 .unwrap();
1607
1608 assert!(is_valid);
1609 }
1610
1611 #[test]
1612 fn test_scoped_proof_detects_scoped_changes() {
1613 let nonce = TEST_NONCE;
1614 let context_id = "ctx_abc123";
1615 let binding = "POST /transfer";
1616 let timestamp = "1234567890";
1617 let scope = vec!["amount", "recipient"];
1618
1619 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
1620
1621 let payload1 = r#"{"amount":1000,"recipient":"user1","notes":"hello"}"#;
1622 let (proof, scope_hash) =
1623 ash_build_proof_scoped(&client_secret, timestamp, binding, payload1, &scope).unwrap();
1624
1625 let payload2 = r#"{"amount":9999,"recipient":"user1","notes":"hello"}"#;
1626
1627 let is_valid = ash_verify_proof_scoped(
1628 nonce,
1629 context_id,
1630 binding,
1631 timestamp,
1632 payload2,
1633 &scope,
1634 &scope_hash,
1635 &proof,
1636 )
1637 .unwrap();
1638
1639 assert!(!is_valid);
1640 }
1641
1642 #[test]
1643 fn test_extract_scoped_fields_with_array_index() {
1644 let payload: Value = serde_json::from_str(
1646 r#"{"items":[{"id":1,"name":"a"},{"id":2,"name":"b"}],"total":100}"#
1647 ).unwrap();
1648
1649 let scope = vec!["items[0]"];
1650 let scoped = ash_extract_scoped_fields(&payload, &scope).unwrap();
1651
1652 assert!(scoped.is_object());
1654 let items = scoped.get("items").expect("should have items key");
1655 assert!(items.is_array(), "items should be an array, got: {:?}", items);
1656 let arr = items.as_array().unwrap();
1657 assert_eq!(arr.len(), 1);
1658 assert_eq!(arr[0]["id"], 1);
1659 }
1660
1661 #[test]
1662 fn test_extract_scoped_fields_with_nested_array_path() {
1663 let payload: Value = serde_json::from_str(
1665 r#"{"items":[{"id":1,"name":"a"},{"id":2,"name":"b"}]}"#
1666 ).unwrap();
1667
1668 let scope = vec!["items[0].id"];
1669 let scoped = ash_extract_scoped_fields(&payload, &scope).unwrap();
1670
1671 assert!(scoped.is_object());
1673 let items = scoped.get("items").expect("should have items key");
1674 assert!(items.is_array(), "items should be an array");
1675 let arr = items.as_array().unwrap();
1676 assert_eq!(arr.len(), 1);
1677 assert_eq!(arr[0]["id"], 1);
1678 }
1679}
1680
1681#[derive(Debug, Clone, PartialEq)]
1687pub struct UnifiedProofResult {
1688 pub proof: String,
1690 pub scope_hash: String,
1692 pub chain_hash: String,
1694}
1695
1696pub fn ash_hash_proof(proof: &str) -> Result<String, AshError> {
1704 if proof.is_empty() {
1706 return Err(AshError::new(
1707 AshErrorCode::ValidationError,
1708 "proof cannot be empty for chain hashing",
1709 ));
1710 }
1711 let mut hasher = Sha256::new();
1712 hasher.update(proof.as_bytes());
1713 Ok(hex::encode(hasher.finalize()))
1714}
1715
1716pub fn ash_build_proof_unified(
1745 client_secret: &str,
1746 timestamp: &str,
1747 binding: &str,
1748 payload: &str,
1749 scope: &[&str],
1750 previous_proof: Option<&str>,
1751) -> Result<UnifiedProofResult, AshError> {
1752 if client_secret.is_empty() {
1754 return Err(AshError::new(
1755 AshErrorCode::ValidationError,
1756 "client_secret cannot be empty",
1757 ));
1758 }
1759 if timestamp.is_empty() {
1760 return Err(AshError::new(
1761 AshErrorCode::ValidationError,
1762 "timestamp cannot be empty",
1763 ));
1764 }
1765 if binding.is_empty() {
1766 return Err(AshError::new(
1767 AshErrorCode::ValidationError,
1768 "binding cannot be empty",
1769 ));
1770 }
1771
1772 let json_payload: Value = if payload.is_empty() || payload.trim().is_empty() {
1774 Value::Object(serde_json::Map::new())
1775 } else {
1776 serde_json::from_str(payload)
1778 .map_err(|_e| AshError::canonicalization_error())?
1779 };
1780
1781 let scoped_payload = ash_extract_scoped_fields(&json_payload, scope)?;
1782
1783 let canonical_scoped = ash_canonicalize_json_value(&scoped_payload)?;
1785
1786 let body_hash = ash_hash_body(&canonical_scoped);
1787
1788 let scope_hash = ash_hash_scope(scope)?;
1791
1792 let chain_hash = match previous_proof {
1795 Some(prev) if !prev.is_empty() => ash_hash_proof(prev)?,
1796 _ => String::new(),
1797 };
1798
1799 let message = format!(
1801 "{}|{}|{}|{}|{}",
1802 timestamp, binding, body_hash, scope_hash, chain_hash
1803 );
1804
1805 let mut mac = HmacSha256Type::new_from_slice(client_secret.as_bytes())
1806 .expect("HMAC can take key of any size");
1807 mac.update(message.as_bytes());
1808 let proof = hex::encode(mac.finalize().into_bytes());
1809
1810 Ok(UnifiedProofResult {
1811 proof,
1812 scope_hash,
1813 chain_hash,
1814 })
1815}
1816
1817#[allow(clippy::too_many_arguments)]
1829pub fn ash_verify_proof_unified(
1830 nonce: &str,
1831 context_id: &str,
1832 binding: &str,
1833 timestamp: &str,
1834 payload: &str,
1835 client_proof: &str,
1836 scope: &[&str],
1837 scope_hash: &str,
1838 previous_proof: Option<&str>,
1839 chain_hash: &str,
1840) -> Result<bool, AshError> {
1841 ash_validate_timestamp_format(timestamp)?;
1843
1844 if scope.is_empty() && !scope_hash.is_empty() {
1846 return Err(AshError::new(
1847 AshErrorCode::ScopeMismatch,
1848 "scope_hash must be empty when scope is empty",
1849 ));
1850 }
1851
1852 if !scope.is_empty() {
1855 let expected_scope_hash = ash_hash_scope(scope)?;
1856 if !ash_timing_safe_equal(expected_scope_hash.as_bytes(), scope_hash.as_bytes()) {
1857 return Ok(false);
1858 }
1859 }
1860
1861 let has_previous = previous_proof.is_some_and(|p| !p.is_empty());
1863 if !has_previous && !chain_hash.is_empty() {
1864 return Err(AshError::new(
1865 AshErrorCode::ChainBroken,
1866 "chain_hash must be empty when previous_proof is absent",
1867 ));
1868 }
1869
1870 if let Some(prev) = previous_proof {
1873 if !prev.is_empty() {
1874 let expected_chain_hash = ash_hash_proof(prev)?;
1875 if !ash_timing_safe_equal(expected_chain_hash.as_bytes(), chain_hash.as_bytes()) {
1876 return Ok(false);
1877 }
1878 }
1879 }
1880
1881 let client_secret = ash_derive_client_secret(nonce, context_id, binding)?;
1883
1884 let result = ash_build_proof_unified(
1885 &client_secret,
1886 timestamp,
1887 binding,
1888 payload,
1889 scope,
1890 previous_proof,
1891 )?;
1892
1893 Ok(ash_timing_safe_equal(
1894 result.proof.as_bytes(),
1895 client_proof.as_bytes(),
1896 ))
1897}
1898
1899#[cfg(test)]
1900mod tests_unified {
1901 use super::*;
1902
1903 const TEST_NONCE: &str = "0123456789abcdef0123456789abcdef";
1905
1906 #[test]
1907 fn test_unified_basic() {
1908 let nonce = TEST_NONCE;
1909 let context_id = "ctx_abc123";
1910 let binding = "POST /api/test";
1911 let timestamp = "1234567890";
1912 let payload = r#"{"name":"John","age":30}"#;
1913
1914 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
1915 let result = ash_build_proof_unified(
1916 &client_secret,
1917 timestamp,
1918 binding,
1919 payload,
1920 &[], None, )
1923 .unwrap();
1924
1925 assert!(!result.proof.is_empty());
1926 assert!(result.scope_hash.is_empty());
1927 assert!(result.chain_hash.is_empty());
1928
1929 let is_valid = ash_verify_proof_unified(
1930 nonce,
1931 context_id,
1932 binding,
1933 timestamp,
1934 payload,
1935 &result.proof,
1936 &[],
1937 "",
1938 None,
1939 "",
1940 )
1941 .unwrap();
1942
1943 assert!(is_valid);
1944 }
1945
1946 #[test]
1947 fn test_unified_scoped_only() {
1948 let nonce = TEST_NONCE;
1949 let context_id = "ctx_abc123";
1950 let binding = "POST /transfer";
1951 let timestamp = "1234567890";
1952 let payload = r#"{"amount":1000,"recipient":"user1","notes":"hi"}"#;
1953 let scope = vec!["amount", "recipient"];
1954
1955 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
1956 let result = ash_build_proof_unified(
1957 &client_secret,
1958 timestamp,
1959 binding,
1960 payload,
1961 &scope,
1962 None, )
1964 .unwrap();
1965
1966 assert!(!result.proof.is_empty());
1967 assert!(!result.scope_hash.is_empty());
1968 assert!(result.chain_hash.is_empty());
1969
1970 let is_valid = ash_verify_proof_unified(
1971 nonce,
1972 context_id,
1973 binding,
1974 timestamp,
1975 payload,
1976 &result.proof,
1977 &scope,
1978 &result.scope_hash,
1979 None,
1980 "",
1981 )
1982 .unwrap();
1983
1984 assert!(is_valid);
1985 }
1986
1987 #[test]
1988 fn test_unified_chained_only() {
1989 let nonce = TEST_NONCE;
1990 let context_id = "ctx_abc123";
1991 let binding = "POST /checkout";
1992 let timestamp = "1234567890";
1993 let payload = r#"{"cart_id":"cart_123"}"#;
1994 let previous_proof = "abc123def456";
1995
1996 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
1997 let result = ash_build_proof_unified(
1998 &client_secret,
1999 timestamp,
2000 binding,
2001 payload,
2002 &[], Some(previous_proof),
2004 )
2005 .unwrap();
2006
2007 assert!(!result.proof.is_empty());
2008 assert!(result.scope_hash.is_empty());
2009 assert!(!result.chain_hash.is_empty());
2010
2011 let is_valid = ash_verify_proof_unified(
2012 nonce,
2013 context_id,
2014 binding,
2015 timestamp,
2016 payload,
2017 &result.proof,
2018 &[],
2019 "",
2020 Some(previous_proof),
2021 &result.chain_hash,
2022 )
2023 .unwrap();
2024
2025 assert!(is_valid);
2026 }
2027
2028 #[test]
2029 fn test_unified_full() {
2030 let nonce = TEST_NONCE;
2031 let context_id = "ctx_abc123";
2032 let binding = "POST /payment";
2033 let timestamp = "1234567890";
2034 let payload = r#"{"amount":500,"currency":"USD","notes":"tip"}"#;
2035 let scope = vec!["amount", "currency"];
2036 let previous_proof = "checkout_proof_xyz";
2037
2038 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2039 let result = ash_build_proof_unified(
2040 &client_secret,
2041 timestamp,
2042 binding,
2043 payload,
2044 &scope,
2045 Some(previous_proof),
2046 )
2047 .unwrap();
2048
2049 assert!(!result.proof.is_empty());
2050 assert!(!result.scope_hash.is_empty());
2051 assert!(!result.chain_hash.is_empty());
2052
2053 let is_valid = ash_verify_proof_unified(
2054 nonce,
2055 context_id,
2056 binding,
2057 timestamp,
2058 payload,
2059 &result.proof,
2060 &scope,
2061 &result.scope_hash,
2062 Some(previous_proof),
2063 &result.chain_hash,
2064 )
2065 .unwrap();
2066
2067 assert!(is_valid);
2068 }
2069
2070 #[test]
2071 fn test_unified_chain_broken() {
2072 let nonce = TEST_NONCE;
2073 let context_id = "ctx_abc123";
2074 let binding = "POST /payment";
2075 let timestamp = "1234567890";
2076 let payload = r#"{"amount":500}"#;
2077 let previous_proof = "original_proof";
2078
2079 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2080 let result = ash_build_proof_unified(
2081 &client_secret,
2082 timestamp,
2083 binding,
2084 payload,
2085 &[],
2086 Some(previous_proof),
2087 )
2088 .unwrap();
2089
2090 let is_valid = ash_verify_proof_unified(
2092 nonce,
2093 context_id,
2094 binding,
2095 timestamp,
2096 payload,
2097 &result.proof,
2098 &[],
2099 "",
2100 Some("tampered_proof"), &result.chain_hash,
2102 )
2103 .unwrap();
2104
2105 assert!(!is_valid);
2106 }
2107
2108 #[test]
2109 fn test_unified_scope_tampered() {
2110 let nonce = TEST_NONCE;
2111 let context_id = "ctx_abc123";
2112 let binding = "POST /transfer";
2113 let timestamp = "1234567890";
2114 let payload = r#"{"amount":1000,"recipient":"user1"}"#;
2115 let scope = vec!["amount"];
2116
2117 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2118 let result =
2119 ash_build_proof_unified(&client_secret, timestamp, binding, payload, &scope, None)
2120 .unwrap();
2121
2122 let tampered_scope = vec!["recipient"];
2124 let is_valid = ash_verify_proof_unified(
2125 nonce,
2126 context_id,
2127 binding,
2128 timestamp,
2129 payload,
2130 &result.proof,
2131 &tampered_scope, &result.scope_hash, None,
2134 "",
2135 )
2136 .unwrap();
2137
2138 assert!(!is_valid);
2139 }
2140
2141 #[test]
2142 fn test_ash_hash_proof() {
2143 let proof = "test_proof_123";
2144 let hash1 = ash_hash_proof(proof).unwrap();
2145 let hash2 = ash_hash_proof(proof).unwrap();
2146
2147 assert_eq!(hash1, hash2);
2148 assert_eq!(hash1.len(), 64); }
2150
2151 #[test]
2152 fn test_ash_hash_proof_rejects_empty() {
2153 let result = ash_hash_proof("");
2155 assert!(result.is_err());
2156 assert!(result.unwrap_err().message().contains("empty"));
2157 }
2158
2159 #[test]
2162 fn test_unified_rejects_scope_hash_when_scope_empty() {
2163 let nonce = TEST_NONCE;
2165 let context_id = "ctx_abc123";
2166 let binding = "POST /api/test";
2167 let timestamp = "1234567890";
2168 let payload = r#"{"name":"John"}"#;
2169
2170 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2171 let result = ash_build_proof_unified(
2172 &client_secret,
2173 timestamp,
2174 binding,
2175 payload,
2176 &[], None,
2178 )
2179 .unwrap();
2180
2181 let verify_result = ash_verify_proof_unified(
2183 nonce,
2184 context_id,
2185 binding,
2186 timestamp,
2187 payload,
2188 &result.proof,
2189 &[], "fake_scope_hash", None,
2192 "",
2193 );
2194
2195 assert!(verify_result.is_err());
2196 assert_eq!(verify_result.unwrap_err().code(), crate::AshErrorCode::ScopeMismatch);
2197 }
2198
2199 #[test]
2200 fn test_unified_rejects_chain_hash_when_no_previous_proof() {
2201 let nonce = TEST_NONCE;
2203 let context_id = "ctx_abc123";
2204 let binding = "POST /api/test";
2205 let timestamp = "1234567890";
2206 let payload = r#"{"name":"John"}"#;
2207
2208 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2209 let result = ash_build_proof_unified(
2210 &client_secret,
2211 timestamp,
2212 binding,
2213 payload,
2214 &[],
2215 None, )
2217 .unwrap();
2218
2219 let verify_result = ash_verify_proof_unified(
2221 nonce,
2222 context_id,
2223 binding,
2224 timestamp,
2225 payload,
2226 &result.proof,
2227 &[],
2228 "",
2229 None, "fake_chain_hash", );
2232
2233 assert!(verify_result.is_err());
2234 assert_eq!(verify_result.unwrap_err().code(), crate::AshErrorCode::ChainBroken);
2235 }
2236}
2237
2238#[cfg(test)]
2240mod tests_sec011 {
2241 use super::*;
2242
2243 #[test]
2244 fn test_large_array_index_rejected() {
2245 let payload: Value = serde_json::from_str(
2247 r#"{"items":[{"id":1}]}"#
2248 ).unwrap();
2249
2250 let scope = vec!["items[999999]"];
2252 let result = ash_extract_scoped_fields(&payload, &scope);
2253
2254 assert!(result.is_err());
2255 assert!(result.unwrap_err().message().contains("allocation"));
2256 }
2257
2258 #[test]
2259 fn test_valid_array_index_works() {
2260 let payload: Value = serde_json::from_str(
2262 r#"{"items":[{"id":1},{"id":2},{"id":3}]}"#
2263 ).unwrap();
2264
2265 let scope = vec!["items[1]"];
2266 let scoped = ash_extract_scoped_fields(&payload, &scope).unwrap();
2267
2268 assert!(scoped.is_object());
2269 let items = scoped.get("items").expect("should have items");
2270 let arr = items.as_array().unwrap();
2271 assert_eq!(arr.len(), 2); assert_eq!(arr[1]["id"], 2);
2273 }
2274
2275 #[test]
2276 fn test_moderate_array_index_within_limit() {
2277 let payload: Value = serde_json::from_str(
2279 r#"{"items":[{"id":1}]}"#
2280 ).unwrap();
2281
2282 let scope = vec!["items[99]"];
2284 let result = ash_extract_scoped_fields(&payload, &scope);
2285
2286 assert!(result.is_ok());
2288 }
2289}
2290
2291#[cfg(test)]
2293mod tests_sec018 {
2294 use super::*;
2295
2296 #[test]
2297 fn test_rejects_unreasonably_large_timestamp() {
2298 let huge_timestamp = "99999999999999999"; let result = ash_validate_timestamp(huge_timestamp, 300, 60);
2301 assert!(result.is_err());
2302 assert!(result.unwrap_err().message().contains("maximum"));
2303 }
2304
2305 #[test]
2306 fn test_accepts_normal_timestamp() {
2307 use std::time::{SystemTime, UNIX_EPOCH};
2308 let now = SystemTime::now()
2309 .duration_since(UNIX_EPOCH)
2310 .unwrap()
2311 .as_secs();
2312 let result = ash_validate_timestamp(&now.to_string(), 300, 60);
2313 assert!(result.is_ok());
2314 }
2315}
2316
2317#[cfg(test)]
2319mod tests_sec019 {
2320 use super::*;
2321
2322 #[test]
2323 fn test_deep_scope_path_ignored() {
2324 let payload: Value = serde_json::json!({"a": {"b": {"c": 1}}});
2326
2327 let deep_path = (0..35).map(|_| "x").collect::<Vec<_>>().join(".");
2329 let scope = vec![deep_path.as_str()];
2330 let scoped = ash_extract_scoped_fields(&payload, &scope).unwrap();
2331
2332 assert!(scoped.is_object());
2334 assert!(scoped.as_object().unwrap().is_empty());
2335 }
2336
2337 #[test]
2338 fn test_normal_depth_path_works() {
2339 let payload: Value = serde_json::json!({"a": {"b": {"c": 1}}});
2341 let scope = vec!["a.b.c"];
2342 let scoped = ash_extract_scoped_fields(&payload, &scope).unwrap();
2343
2344 assert!(scoped.is_object());
2345 let c_value = scoped.get("a")
2346 .and_then(|a| a.get("b"))
2347 .and_then(|b| b.get("c"));
2348 assert_eq!(c_value, Some(&serde_json::json!(1)));
2349 }
2350}
2351
2352#[cfg(test)]
2354mod tests_bug022 {
2355 use super::*;
2356
2357 #[test]
2358 fn test_multi_dimensional_array_get() {
2359 let payload: Value = serde_json::json!({
2361 "matrix": [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
2362 });
2363
2364 let scope = vec!["matrix[1][2]"];
2365 let scoped = ash_extract_scoped_fields(&payload, &scope).unwrap();
2366
2367 assert!(scoped.is_object());
2369 let matrix = scoped.get("matrix").expect("should have matrix");
2370 let arr = matrix.as_array().unwrap();
2371 assert_eq!(arr.len(), 2); let inner = arr[1].as_array().unwrap();
2374 assert_eq!(inner.len(), 3); assert_eq!(inner[2], 6);
2376 }
2377
2378 #[test]
2379 fn test_multi_dimensional_array_nested_object() {
2380 let payload: Value = serde_json::json!({
2382 "items": [
2383 [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}],
2384 [{"id": 3, "name": "c"}, {"id": 4, "name": "d"}]
2385 ]
2386 });
2387
2388 let scope = vec!["items[1][0].name"];
2389 let scoped = ash_extract_scoped_fields(&payload, &scope).unwrap();
2390
2391 let items = scoped.get("items").expect("should have items");
2393 let outer = items.as_array().unwrap();
2394 assert_eq!(outer.len(), 2);
2395 let inner = outer[1].as_array().unwrap();
2396 assert_eq!(inner.len(), 1);
2397 let obj = inner[0].as_object().unwrap();
2398 assert_eq!(obj.get("name").unwrap(), "c");
2399 }
2400
2401 #[test]
2402 fn test_ash_parse_all_array_indices() {
2403 let notation = ash_parse_all_array_indices("items[0][1][2]");
2405 assert_eq!(notation.key, "items");
2406 assert_eq!(notation.indices, vec![0, 1, 2]);
2407
2408 let notation2 = ash_parse_all_array_indices("simple");
2409 assert_eq!(notation2.key, "simple");
2410 assert!(notation2.indices.is_empty());
2411
2412 let notation3 = ash_parse_all_array_indices("arr[5]");
2413 assert_eq!(notation3.key, "arr");
2414 assert_eq!(notation3.indices, vec![5]);
2415 }
2416
2417 #[test]
2418 fn test_multi_dimensional_invalid_index() {
2419 let notation = ash_parse_all_array_indices("items[0][abc][2]");
2421 assert_eq!(notation.key, "items");
2422 assert!(notation.indices.is_empty());
2425 }
2426
2427 #[test]
2428 fn test_multi_dimensional_trailing_text() {
2429 let notation = ash_parse_all_array_indices("items[0][1]extra");
2431 assert_eq!(notation.key, "items");
2432 assert!(notation.indices.is_empty());
2434 }
2435}
2436
2437#[cfg(test)]
2439mod tests_bug023 {
2440 use super::*;
2441
2442 const TEST_NONCE: &str = "0123456789abcdef0123456789abcdef";
2443
2444 #[test]
2445 fn test_scope_order_independent() {
2446 let scope1 = vec!["amount", "recipient"];
2448 let scope2 = vec!["recipient", "amount"];
2449
2450 let hash1 = ash_hash_scope(&scope1).unwrap();
2451 let hash2 = ash_hash_scope(&scope2).unwrap();
2452
2453 assert_eq!(hash1, hash2, "Scope order should not affect hash");
2454 }
2455
2456 #[test]
2457 fn test_scope_deduplication() {
2458 let scope1 = vec!["amount", "amount", "recipient"];
2460 let scope2 = vec!["amount", "recipient"];
2461
2462 let hash1 = ash_hash_scope(&scope1).unwrap();
2463 let hash2 = ash_hash_scope(&scope2).unwrap();
2464
2465 assert_eq!(hash1, hash2, "Duplicate fields should be deduplicated");
2466 }
2467
2468 #[test]
2469 fn test_scope_rejects_delimiter_in_field_name() {
2470 let scope_with_delimiter = vec!["amount", "field\x1Fname"];
2472
2473 let result = ash_hash_scope(&scope_with_delimiter);
2474 assert!(result.is_err(), "Should reject field names containing delimiter");
2475 assert!(result.unwrap_err().message().contains("delimiter"));
2476 }
2477
2478 #[test]
2479 fn test_scoped_proof_order_independent() {
2480 let nonce = TEST_NONCE;
2482 let context_id = "ctx_abc123";
2483 let binding = "POST /transfer";
2484 let timestamp = "1234567890";
2485 let payload = r#"{"amount":1000,"recipient":"user1","notes":"hi"}"#;
2486
2487 let client_scope = vec!["recipient", "amount"];
2489 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2490 let (proof, scope_hash) =
2491 ash_build_proof_scoped(&client_secret, timestamp, binding, payload, &client_scope).unwrap();
2492
2493 let server_scope = vec!["amount", "recipient"];
2495 let is_valid = ash_verify_proof_scoped(
2496 nonce,
2497 context_id,
2498 binding,
2499 timestamp,
2500 payload,
2501 &server_scope, &scope_hash,
2503 &proof,
2504 )
2505 .unwrap();
2506
2507 assert!(is_valid, "Verification should succeed regardless of scope order");
2508 }
2509}
2510
2511#[cfg(test)]
2513mod tests_bug036 {
2514 use super::*;
2515
2516 #[test]
2517 fn test_rejects_excessive_array_allocation() {
2518 let payload: Value = serde_json::json!({});
2520
2521 let scope = vec![
2523 "items[9999]",
2524 "other[9999]",
2525 ];
2526 let result = ash_extract_scoped_fields(&payload, &scope);
2529 assert!(result.is_err());
2530 assert!(result.unwrap_err().message().contains("allocation"));
2531 }
2532
2533 #[test]
2534 fn test_accepts_reasonable_array_allocation() {
2535 let payload: Value = serde_json::json!({
2537 "items": [{"id": 1}, {"id": 2}, {"id": 3}]
2538 });
2539
2540 let scope = vec!["items[0]", "items[1]", "items[2]"];
2541 let result = ash_extract_scoped_fields(&payload, &scope);
2544 assert!(result.is_ok());
2545 }
2546
2547 #[test]
2548 fn test_allocation_calculation() {
2549 let scope = vec!["items[10]", "matrix[5][5]"];
2551 let total = ash_calculate_total_array_allocation(&scope);
2555 assert_eq!(total, 23);
2556 }
2557
2558 #[test]
2559 fn test_allocation_calculation_overflow_protection() {
2560 let scope = vec!["items[18446744073709551615]"]; let total = ash_calculate_total_array_allocation(&scope);
2567 assert_eq!(total, usize::MAX);
2569 }
2570}
2571
2572#[cfg(test)]
2574mod tests_bug039 {
2575 use super::*;
2576
2577 #[test]
2578 fn test_rejects_empty_scope_field_name() {
2579 let scope = vec!["amount", ""];
2581 let result = ash_hash_scope(&scope);
2582 assert!(result.is_err());
2583 assert!(result.unwrap_err().message().contains("empty"));
2584 }
2585
2586 #[test]
2587 fn test_rejects_only_empty_scope_field() {
2588 let scope = vec![""];
2589 let result = ash_hash_scope(&scope);
2590 assert!(result.is_err());
2591 }
2592
2593 #[test]
2594 fn test_accepts_valid_scope_fields() {
2595 let scope = vec!["amount", "recipient"];
2596 let result = ash_hash_scope(&scope);
2597 assert!(result.is_ok());
2598 }
2599}
2600
2601#[cfg(test)]
2603mod tests_bug024 {
2604 use super::*;
2605
2606 const TEST_NONCE: &str = "0123456789abcdef0123456789abcdef";
2607
2608 #[test]
2609 fn test_empty_payload_scoped() {
2610 let client_secret = "test_secret";
2612 let timestamp = "1234567890";
2613 let binding = "POST /api/test";
2614
2615 let result1 = ash_build_proof_scoped(client_secret, timestamp, binding, "", &[]);
2617 assert!(result1.is_ok(), "Empty string payload should work");
2618
2619 let result2 = ash_build_proof_scoped(client_secret, timestamp, binding, " ", &[]);
2620 assert!(result2.is_ok(), "Whitespace-only payload should work");
2621 }
2622
2623 #[test]
2624 fn test_empty_payload_unified() {
2625 let client_secret = "test_secret";
2627 let timestamp = "1234567890";
2628 let binding = "POST /api/test";
2629
2630 let result = ash_build_proof_unified(
2631 client_secret,
2632 timestamp,
2633 binding,
2634 "",
2635 &[],
2636 None,
2637 );
2638 assert!(result.is_ok(), "Empty string payload should work");
2639 }
2640
2641 #[test]
2642 fn test_empty_payload_hash_scoped_body() {
2643 let result = ash_hash_scoped_body("", &[]);
2645 assert!(result.is_ok(), "Empty payload should work");
2646
2647 let result2 = ash_hash_scoped_body(" ", &[]);
2648 assert!(result2.is_ok(), "Whitespace payload should work");
2649 }
2650
2651 #[test]
2652 fn test_empty_payload_produces_consistent_hash() {
2653 let hash1 = ash_hash_scoped_body("", &[]).unwrap();
2655 let hash2 = ash_hash_scoped_body("{}", &[]).unwrap();
2656
2657 assert_eq!(hash1, hash2, "Empty string and {{}} should produce same hash");
2658 }
2659
2660 #[test]
2661 fn test_empty_payload_verification() {
2662 let nonce = TEST_NONCE;
2664 let context_id = "ctx_abc123";
2665 let binding = "POST /api/test";
2666 let timestamp = "1234567890";
2667
2668 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2669 let result = ash_build_proof_unified(
2670 &client_secret,
2671 timestamp,
2672 binding,
2673 "",
2674 &[],
2675 None,
2676 ).unwrap();
2677
2678 let is_valid = ash_verify_proof_unified(
2680 nonce,
2681 context_id,
2682 binding,
2683 timestamp,
2684 "",
2685 &result.proof,
2686 &[],
2687 "",
2688 None,
2689 "",
2690 ).unwrap();
2691
2692 assert!(is_valid);
2693 }
2694}
2695
2696#[cfg(test)]
2698mod tests_bug046_047 {
2699 use super::*;
2700
2701 #[test]
2702 fn test_build_proof_scoped_rejects_empty_client_secret() {
2703 let result = ash_build_proof_scoped("", "1234567890", "POST|/api|", "{}", &[]);
2705 assert!(result.is_err());
2706 assert!(result.unwrap_err().message().contains("client_secret"));
2707 }
2708
2709 #[test]
2710 fn test_build_proof_scoped_rejects_empty_timestamp() {
2711 let result = ash_build_proof_scoped("secret", "", "POST|/api|", "{}", &[]);
2713 assert!(result.is_err());
2714 assert!(result.unwrap_err().message().contains("timestamp"));
2715 }
2716
2717 #[test]
2718 fn test_build_proof_scoped_rejects_empty_binding() {
2719 let result = ash_build_proof_scoped("secret", "1234567890", "", "{}", &[]);
2721 assert!(result.is_err());
2722 assert!(result.unwrap_err().message().contains("binding"));
2723 }
2724
2725 #[test]
2726 fn test_build_proof_unified_rejects_empty_client_secret() {
2727 let result = ash_build_proof_unified("", "1234567890", "POST|/api|", "{}", &[], None);
2729 assert!(result.is_err());
2730 assert!(result.unwrap_err().message().contains("client_secret"));
2731 }
2732
2733 #[test]
2734 fn test_build_proof_unified_rejects_empty_timestamp() {
2735 let result = ash_build_proof_unified("secret", "", "POST|/api|", "{}", &[], None);
2737 assert!(result.is_err());
2738 assert!(result.unwrap_err().message().contains("timestamp"));
2739 }
2740
2741 #[test]
2742 fn test_build_proof_unified_rejects_empty_binding() {
2743 let result = ash_build_proof_unified("secret", "1234567890", "", "{}", &[], None);
2745 assert!(result.is_err());
2746 assert!(result.unwrap_err().message().contains("binding"));
2747 }
2748}
2749
2750#[cfg(test)]
2752mod tests_bug049 {
2753 use super::*;
2754
2755 const TEST_NONCE: &str = "0123456789abcdef0123456789abcdef";
2756
2757 #[test]
2758 fn test_verify_proof_scoped_rejects_scope_hash_when_scope_empty() {
2759 let nonce = TEST_NONCE;
2761 let context_id = "ctx_abc123";
2762 let binding = "POST /api/test";
2763 let timestamp = "1234567890";
2764 let payload = r#"{"name":"John"}"#;
2765
2766 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2767 let (proof, _) = ash_build_proof_scoped(
2768 &client_secret,
2769 timestamp,
2770 binding,
2771 payload,
2772 &[], ).unwrap();
2774
2775 let verify_result = ash_verify_proof_scoped(
2777 nonce,
2778 context_id,
2779 binding,
2780 timestamp,
2781 payload,
2782 &[], "fake_scope_hash", &proof,
2785 );
2786
2787 assert!(verify_result.is_err());
2788 assert_eq!(verify_result.unwrap_err().code(), crate::AshErrorCode::ScopeMismatch);
2789 }
2790
2791 #[test]
2792 fn test_verify_proof_scoped_accepts_valid_empty_scope() {
2793 let nonce = TEST_NONCE;
2795 let context_id = "ctx_abc123";
2796 let binding = "POST /api/test";
2797 let timestamp = "1234567890";
2798 let payload = r#"{"name":"John"}"#;
2799
2800 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2801 let (proof, scope_hash) = ash_build_proof_scoped(
2802 &client_secret,
2803 timestamp,
2804 binding,
2805 payload,
2806 &[], ).unwrap();
2808
2809 assert!(scope_hash.is_empty(), "scope_hash should be empty for empty scope");
2810
2811 let verify_result = ash_verify_proof_scoped(
2813 nonce,
2814 context_id,
2815 binding,
2816 timestamp,
2817 payload,
2818 &[],
2819 "",
2820 &proof,
2821 );
2822
2823 assert!(verify_result.is_ok());
2824 assert!(verify_result.unwrap());
2825 }
2826}
2827
2828#[cfg(test)]
2830mod tests_security_audit {
2831 use super::*;
2832
2833 const TEST_NONCE: &str = "0123456789abcdef0123456789abcdef";
2834 const TEST_BODY_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
2835
2836 #[test]
2837 fn test_sec_audit_004_binding_length_limit_derive() {
2838 let long_binding = "a".repeat(8193); let result = ash_derive_client_secret(TEST_NONCE, "ctx_abc", &long_binding);
2841 assert!(result.is_err());
2842 assert!(result.unwrap_err().message().contains("maximum length"));
2843 }
2844
2845 #[test]
2846 fn test_sec_audit_004_binding_length_limit_build() {
2847 let long_binding = "a".repeat(8193); let result = ash_build_proof("secret", "1234567890", &long_binding, TEST_BODY_HASH);
2850 assert!(result.is_err());
2851 assert!(result.unwrap_err().message().contains("maximum length"));
2852 }
2853
2854 #[test]
2855 fn test_sec_audit_004_binding_at_limit_ok() {
2856 let long_binding = "a".repeat(8192);
2858 let result = ash_build_proof("secret", "1234567890", &long_binding, TEST_BODY_HASH);
2859 assert!(result.is_ok());
2860 }
2861
2862 #[test]
2863 fn test_sec_audit_002_verify_with_freshness() {
2864 use std::time::{SystemTime, UNIX_EPOCH};
2866
2867 let nonce = TEST_NONCE;
2868 let context_id = "ctx_abc123";
2869 let binding = "POST|/api/test|";
2870 let now = SystemTime::now()
2871 .duration_since(UNIX_EPOCH)
2872 .unwrap()
2873 .as_secs();
2874 let timestamp = now.to_string();
2875
2876 let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2877 let proof = ash_build_proof(&client_secret, ×tamp, binding, TEST_BODY_HASH).unwrap();
2878
2879 let result = ash_verify_proof_with_freshness(
2881 nonce, context_id, binding, ×tamp, TEST_BODY_HASH, &proof,
2882 300, 60
2883 );
2884 assert!(result.is_ok());
2885 assert!(result.unwrap());
2886 }
2887
2888 #[test]
2889 fn test_sec_audit_002_verify_with_freshness_rejects_expired() {
2890 use std::time::{SystemTime, UNIX_EPOCH};
2892
2893 let nonce = TEST_NONCE;
2894 let context_id = "ctx_abc123";
2895 let binding = "POST|/api/test|";
2896 let now = SystemTime::now()
2897 .duration_since(UNIX_EPOCH)
2898 .unwrap()
2899 .as_secs();
2900 let old_timestamp = (now - 600).to_string(); let client_secret = ash_derive_client_secret(nonce, context_id, binding).unwrap();
2903 let proof = ash_build_proof(&client_secret, &old_timestamp, binding, TEST_BODY_HASH).unwrap();
2904
2905 let result = ash_verify_proof_with_freshness(
2907 nonce, context_id, binding, &old_timestamp, TEST_BODY_HASH, &proof,
2908 300, 60 );
2910 assert!(result.is_err());
2911 assert!(result.unwrap_err().message().contains("expired"));
2912 }
2913
2914 #[test]
2915 fn test_sec_audit_003_generic_error_message() {
2916 let field_with_delimiter = format!("secret_field{}name", SCOPE_FIELD_DELIMITER);
2918 let result = ash_hash_scope(&[&field_with_delimiter]);
2919 assert!(result.is_err());
2920 let error_msg = result.unwrap_err().message().to_string();
2922 assert!(!error_msg.contains("secret_field")); assert!(!error_msg.contains("name")); assert!(error_msg.contains("delimiter")); }
2926}