use ash_core::{
ash_build_proof, ash_verify_proof, ash_derive_client_secret,
ash_canonicalize_json, ash_canonicalize_query,
ash_hash_body, ash_timing_safe_equal, ash_normalize_binding,
ash_build_proof_scoped, ash_verify_proof_scoped,
ash_build_proof_unified, ash_verify_proof_unified,
ash_extract_scoped_fields, ash_validate_timestamp,
};
use std::collections::HashSet;
#[test]
fn test_rejects_empty_nonce() {
let result = ash_derive_client_secret("", "ctx_test", "POST|/api|");
assert!(result.is_err());
}
#[test]
fn test_rejects_short_nonce() {
let result = ash_derive_client_secret("abcd1234", "ctx_test", "POST|/api|");
assert!(result.is_err());
}
#[test]
fn test_rejects_non_hex_nonce() {
let result = ash_derive_client_secret("ghijklmnopqrstuvwxyz123456789012", "ctx_test", "POST|/api|");
assert!(result.is_err());
}
#[test]
fn test_rejects_empty_context_id() {
let nonce = "a".repeat(64);
let result = ash_derive_client_secret(&nonce, "", "POST|/api|");
assert!(result.is_err());
}
#[test]
fn test_rejects_empty_binding() {
let nonce = "a".repeat(64);
let result = ash_derive_client_secret(&nonce, "ctx_test", "");
assert!(result.is_err());
assert!(result.unwrap_err().message().contains("binding"));
}
#[test]
fn test_rejects_invalid_json() {
let result = ash_canonicalize_json("not valid json");
assert!(result.is_err());
}
#[test]
fn test_rejects_json_with_nan() {
let result = ash_canonicalize_json(r#"{"value": NaN}"#);
assert!(result.is_err());
}
#[test]
fn test_rejects_json_with_infinity() {
let result = ash_canonicalize_json(r#"{"value": Infinity}"#);
assert!(result.is_err());
}
#[test]
fn test_json_injection_prevented() {
let payload = r#"{"key": "value\", \"injected\": \"true"}"#;
let result = ash_canonicalize_json(payload);
if let Ok(canonical) = result {
assert!(!canonical.contains(r#""injected""#));
}
}
#[test]
fn test_prototype_pollution_in_scope() {
let payload = serde_json::json!({"__proto__": {"polluted": true}, "safe": 1});
let result = ash_extract_scoped_fields(&payload, &["__proto__"]);
assert!(result.is_ok());
let extracted = result.unwrap();
assert!(extracted.get("__proto__").is_some());
}
#[test]
fn test_constructor_field_in_scope() {
let payload = serde_json::json!({"constructor": {"polluted": true}, "safe": 1});
let result = ash_extract_scoped_fields(&payload, &["constructor"]);
assert!(result.is_ok());
let extracted = result.unwrap();
assert!(extracted.get("constructor").is_some());
}
#[test]
fn test_path_traversal_in_scope_prevented() {
let payload = serde_json::json!({"data": {"nested": 1}});
let result = ash_extract_scoped_fields(&payload, &["../etc/passwd"]);
if let Ok(extracted) = result {
let is_empty = extracted.as_object().map(|o| o.is_empty()).unwrap_or(true);
assert!(is_empty);
}
}
#[test]
fn test_nonce_uniqueness() {
let mut nonces = HashSet::new();
for _ in 0..1000 {
let nonce = format!("{:064x}", rand::random::<u128>());
nonces.insert(nonce);
}
assert_eq!(nonces.len(), 1000, "Nonces should be unique");
}
#[test]
fn test_proof_is_deterministic() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let body_hash = "b".repeat(64);
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let proof1 = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
let proof2 = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
assert_eq!(proof1, proof2, "Same inputs should produce same proof");
}
#[test]
fn test_different_inputs_produce_different_proofs() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let proof1 = ash_build_proof(&secret, timestamp, binding, &"a".repeat(64)).unwrap();
let proof2 = ash_build_proof(&secret, timestamp, binding, &"b".repeat(64)).unwrap();
assert_ne!(proof1, proof2, "Different inputs should produce different proofs");
}
#[test]
fn test_timing_safe_comparison() {
assert!(ash_timing_safe_equal(b"abc", b"abc"));
assert!(!ash_timing_safe_equal(b"abc", b"abd"));
assert!(!ash_timing_safe_equal(b"abc", b"abcd"));
assert!(!ash_timing_safe_equal(b"", b"a"));
}
#[test]
fn test_timestamp_validation_rejects_old() {
let old_timestamp = (chrono::Utc::now().timestamp() - 3600).to_string();
let result = ash_validate_timestamp(&old_timestamp, 300, 60);
assert!(result.is_err());
}
#[test]
fn test_timestamp_validation_rejects_future() {
let future_timestamp = (chrono::Utc::now().timestamp() + 3600).to_string();
let result = ash_validate_timestamp(&future_timestamp, 300, 60);
assert!(result.is_err());
}
#[test]
fn test_timestamp_validation_accepts_current() {
let current_timestamp = chrono::Utc::now().timestamp().to_string();
let result = ash_validate_timestamp(¤t_timestamp, 300, 60);
assert!(result.is_ok());
}
#[test]
fn test_error_messages_do_not_leak_secrets() {
let nonce = "secret_nonce_".to_string() + &"a".repeat(51);
let result = ash_derive_client_secret(&nonce, "ctx", "POST|/|");
if let Err(e) = result {
let error_msg = e.to_string();
assert!(!error_msg.contains("secret_nonce_"),
"Error message should not contain the nonce");
}
}
#[test]
fn test_verification_failure_does_not_leak_expected_proof() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let body_hash = "b".repeat(64);
let wrong_proof = "c".repeat(64);
let result = ash_verify_proof(&nonce, context_id, binding, ×tamp, &body_hash, &wrong_proof);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_rejects_oversized_json() {
let large_data = "x".repeat(11 * 1024 * 1024);
let large_json = format!(r#"{{"data": "{}"}}"#, large_data);
let result = ash_canonicalize_json(&large_json);
assert!(result.is_err());
}
#[test]
fn test_rejects_deeply_nested_json() {
let mut json = String::from("1");
for _ in 0..100 {
json = format!(r#"{{"a": {}}}"#, json);
}
let result = ash_canonicalize_json(&json);
assert!(result.is_err());
}
#[test]
fn test_rejects_oversized_nonce() {
let long_nonce = "a".repeat(513);
let result = ash_derive_client_secret(&long_nonce, "ctx", "POST|/|");
assert!(result.is_err());
}
#[test]
fn test_rejects_oversized_binding() {
let long_path = "/".to_string() + &"a".repeat(9000);
let binding = ash_normalize_binding("POST", &long_path, "").unwrap();
let nonce = "a".repeat(64);
let result = ash_derive_client_secret(&nonce, "ctx", &binding);
assert!(result.is_err());
}
#[test]
fn test_scope_rejects_too_many_fields() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let mut payload_map = serde_json::Map::new();
for i in 0..150 {
payload_map.insert(format!("field{}", i), serde_json::json!(i));
}
let payload = serde_json::Value::Object(payload_map);
let payload_str = serde_json::to_string(&payload).unwrap();
let scope: Vec<&str> = (0..150).map(|i| Box::leak(format!("field{}", i).into_boxed_str()) as &str).collect();
let result = ash_build_proof_scoped(&secret, ×tamp, binding, &payload_str, &scope);
assert!(result.is_err());
}
#[test]
fn test_scope_field_name_length_limit() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let long_field = "a".repeat(100);
let payload = serde_json::json!({&long_field: 1});
let payload_str = serde_json::to_string(&payload).unwrap();
let result = ash_build_proof_scoped(&secret, ×tamp, binding, &payload_str, &[&long_field]);
assert!(result.is_err());
}
#[test]
fn test_double_encoding_handled() {
let query = "key=%252F";
let result = ash_canonicalize_query(query);
assert!(result.is_ok());
assert!(result.unwrap().contains("%252F"));
}
#[test]
fn test_mixed_case_hex_normalized() {
let query = "key=%2f"; let canonical = ash_canonicalize_query(query).unwrap();
assert!(canonical.contains("%2F"), "Should uppercase hex digits");
}
#[test]
fn test_unicode_normalization_nfc() {
let nfd = r#"{"text": "cafe\u0301"}"#; let nfc = r#"{"text": "café"}"#;
let canonical_nfd = ash_canonicalize_json(nfd).unwrap();
let canonical_nfc = ash_canonicalize_json(nfc).unwrap();
assert_eq!(canonical_nfd, canonical_nfc, "Unicode should normalize to NFC");
}
#[test]
fn test_verify_rejects_tampered_binding() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let body_hash = ash_hash_body("{}");
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let proof = ash_build_proof(&secret, ×tamp, binding, &body_hash).unwrap();
let tampered_binding = "POST|/api/admin|";
let result = ash_verify_proof(&nonce, context_id, tampered_binding, ×tamp, &body_hash, &proof).unwrap();
assert!(!result, "Tampered binding should fail verification");
}
#[test]
fn test_verify_rejects_tampered_body() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let body_hash = ash_hash_body(r#"{"amount": 100}"#);
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let proof = ash_build_proof(&secret, ×tamp, binding, &body_hash).unwrap();
let tampered_body_hash = ash_hash_body(r#"{"amount": 10000}"#);
let result = ash_verify_proof(&nonce, context_id, binding, ×tamp, &tampered_body_hash, &proof).unwrap();
assert!(!result, "Tampered body should fail verification");
}
#[test]
fn test_verify_rejects_wrong_proof_format() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let body_hash = "b".repeat(64);
let short_proof = "abc123";
let result = ash_verify_proof(&nonce, context_id, binding, ×tamp, &body_hash, short_proof);
assert!(result.is_err() || !result.unwrap());
let invalid_proof = "g".repeat(64);
let result = ash_verify_proof(&nonce, context_id, binding, ×tamp, &body_hash, &invalid_proof);
assert!(result.is_err() || !result.unwrap());
}
#[test]
fn test_scoped_proof_protects_specified_fields() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let payload = r#"{"amount": 100, "memo": "test"}"#;
let scope = vec!["amount"];
let (proof, scope_hash) = ash_build_proof_scoped(&secret, ×tamp, binding, payload, &scope).unwrap();
let modified_payload = r#"{"amount": 100, "memo": "modified"}"#;
let result = ash_verify_proof_scoped(&nonce, context_id, binding, ×tamp, modified_payload, &scope, &scope_hash, &proof).unwrap();
assert!(result, "Unscoped field change should verify");
let tampered_payload = r#"{"amount": 10000, "memo": "test"}"#;
let result = ash_verify_proof_scoped(&nonce, context_id, binding, ×tamp, tampered_payload, &scope, &scope_hash, &proof).unwrap();
assert!(!result, "Scoped field change should fail verification");
}
#[test]
fn test_chained_proof_integrity() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let timestamp = chrono::Utc::now().timestamp().to_string();
let binding1 = "POST|/api/step1|";
let secret1 = ash_derive_client_secret(&nonce, context_id, binding1).unwrap();
let payload1 = r#"{"step": 1}"#;
let result1 = ash_build_proof_unified(&secret1, ×tamp, binding1, payload1, &[], None).unwrap();
let binding2 = "POST|/api/step2|";
let secret2 = ash_derive_client_secret(&nonce, context_id, binding2).unwrap();
let payload2 = r#"{"step": 2}"#;
let result2 = ash_build_proof_unified(&secret2, ×tamp, binding2, payload2, &[], Some(&result1.proof)).unwrap();
let scope: &[&str] = &[];
let valid = ash_verify_proof_unified(
&nonce, context_id, binding2, ×tamp, payload2,
&result2.proof, scope, &result2.scope_hash,
Some(&result1.proof), &result2.chain_hash
).unwrap();
assert!(valid, "Valid chain should verify");
let wrong_proof = "d".repeat(64);
let invalid = ash_verify_proof_unified(
&nonce, context_id, binding2, ×tamp, payload2,
&result2.proof, scope, &result2.scope_hash,
Some(&wrong_proof), &result2.chain_hash
).unwrap();
assert!(!invalid, "Wrong chain should fail verification");
}