ash_core/lib.rs
1//! # ASH Core
2//!
3//! ASH (Anti-tamper Security Hash) is a request integrity and anti-replay protection library.
4//!
5//! This crate provides the core functionality for:
6//! - Deterministic JSON and URL-encoded payload canonicalization
7//! - Cryptographic proof generation and verification
8//! - Constant-time comparison for timing-attack resistance
9//! - Binding normalization for endpoint protection
10//!
11//! ## Features
12//!
13//! - **Tamper Detection**: Cryptographic proof ensures payload integrity
14//! - **Replay Prevention**: One-time contexts prevent request replay
15//! - **Deterministic**: Byte-identical output across all platforms
16//! - **WASM Compatible**: Works in browsers and server environments
17//!
18//! ## Example
19//!
20//! ```rust
21//! use ash_core::{canonicalize_json, build_proof, AshMode};
22//!
23//! // Canonicalize a JSON payload
24//! let canonical = canonicalize_json(r#"{"z":1,"a":2}"#).unwrap();
25//! assert_eq!(canonical, r#"{"a":2,"z":1}"#);
26//!
27//! // Build a proof
28//! let proof = build_proof(
29//! AshMode::Balanced,
30//! "POST /api/update",
31//! "context-id-123",
32//! None,
33//! &canonical,
34//! ).unwrap();
35//! ```
36//!
37//! ## Security Notes
38//!
39//! ASH verifies **what** is being submitted, not **who** is submitting it.
40//! It should be used alongside authentication systems (JWT, OAuth, etc.).
41
42mod canonicalize;
43mod compare;
44mod errors;
45mod proof;
46mod types;
47
48pub use canonicalize::{canonicalize_json, canonicalize_urlencoded};
49pub use compare::timing_safe_equal;
50pub use errors::{AshError, AshErrorCode};
51pub use proof::{build_proof, verify_proof};
52pub use types::{AshMode, BuildProofInput, VerifyInput};
53
54/// Normalize a binding string to canonical form.
55///
56/// Bindings are in the format: `METHOD /path`
57///
58/// # Normalization Rules
59/// - Method is uppercased
60/// - Path must start with `/`
61/// - Query string is excluded
62/// - Duplicate slashes are collapsed
63/// - Trailing slash is removed (except for root `/`)
64///
65/// # Example
66///
67/// ```rust
68/// use ash_core::normalize_binding;
69///
70/// let binding = normalize_binding("post", "/api//users/").unwrap();
71/// assert_eq!(binding, "POST /api/users");
72/// ```
73pub fn normalize_binding(method: &str, path: &str) -> Result<String, AshError> {
74 // Validate method
75 let method = method.trim().to_uppercase();
76 if method.is_empty() {
77 return Err(AshError::new(
78 AshErrorCode::MalformedRequest,
79 "Method cannot be empty",
80 ));
81 }
82
83 // Validate path starts with /
84 let path = path.trim();
85 if !path.starts_with('/') {
86 return Err(AshError::new(
87 AshErrorCode::MalformedRequest,
88 "Path must start with /",
89 ));
90 }
91
92 // Remove query string
93 let path = path.split('?').next().unwrap_or(path);
94
95 // Collapse duplicate slashes and normalize
96 let mut normalized = String::with_capacity(path.len());
97 let mut prev_slash = false;
98
99 for ch in path.chars() {
100 if ch == '/' {
101 if !prev_slash {
102 normalized.push(ch);
103 }
104 prev_slash = true;
105 } else {
106 normalized.push(ch);
107 prev_slash = false;
108 }
109 }
110
111 // Remove trailing slash (except for root)
112 if normalized.len() > 1 && normalized.ends_with('/') {
113 normalized.pop();
114 }
115
116 Ok(format!("{} {}", method, normalized))
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_normalize_binding_basic() {
125 assert_eq!(
126 normalize_binding("POST", "/api/users").unwrap(),
127 "POST /api/users"
128 );
129 }
130
131 #[test]
132 fn test_normalize_binding_lowercase_method() {
133 assert_eq!(
134 normalize_binding("post", "/api/users").unwrap(),
135 "POST /api/users"
136 );
137 }
138
139 #[test]
140 fn test_normalize_binding_duplicate_slashes() {
141 assert_eq!(
142 normalize_binding("GET", "/api//users///profile").unwrap(),
143 "GET /api/users/profile"
144 );
145 }
146
147 #[test]
148 fn test_normalize_binding_trailing_slash() {
149 assert_eq!(
150 normalize_binding("PUT", "/api/users/").unwrap(),
151 "PUT /api/users"
152 );
153 }
154
155 #[test]
156 fn test_normalize_binding_root() {
157 assert_eq!(normalize_binding("GET", "/").unwrap(), "GET /");
158 }
159
160 #[test]
161 fn test_normalize_binding_query_string() {
162 assert_eq!(
163 normalize_binding("GET", "/api/users?page=1").unwrap(),
164 "GET /api/users"
165 );
166 }
167
168 #[test]
169 fn test_normalize_binding_empty_method() {
170 assert!(normalize_binding("", "/api").is_err());
171 }
172
173 #[test]
174 fn test_normalize_binding_no_leading_slash() {
175 assert!(normalize_binding("GET", "api/users").is_err());
176 }
177}