herolib_crypt/httpsig/
parser.rs1use crate::httpsig::error::HttpSigError;
4use base64::Engine;
5
6#[derive(Debug, Clone)]
8pub struct SignatureParams {
9 pub label: String,
10 pub components: Vec<String>,
11 pub keyid: String,
12 pub alg: String,
13 pub created: u64,
14}
15
16pub fn extract_key_id(signature_input: &str) -> Result<String, HttpSigError> {
28 let params = parse_signature_input(signature_input)?;
29 Ok(params.keyid)
30}
31
32pub fn parse_signature_input(header: &str) -> Result<SignatureParams, HttpSigError> {
39 let parts: Vec<&str> = header.splitn(2, '=').collect();
41 if parts.len() != 2 {
42 return Err(HttpSigError::ParseError(
43 "Invalid Signature-Input format".to_string(),
44 ));
45 }
46
47 let label = parts[0].trim().to_string();
48 let rest = parts[1].trim();
49
50 let components = parse_components_list(rest)?;
52
53 let params_start = rest.find(");").ok_or_else(|| {
55 HttpSigError::ParseError("Missing closing parenthesis in component list".to_string())
56 })? + 2;
57
58 let params_str = &rest[params_start..];
59
60 let keyid = extract_param(params_str, "keyid")?;
62 let alg = extract_param(params_str, "alg")?;
63 let created_str = extract_param(params_str, "created")?;
64 let created = created_str.parse::<u64>().map_err(|_| {
65 HttpSigError::ParseError(format!("Invalid created timestamp: {}", created_str))
66 })?;
67
68 Ok(SignatureParams {
69 label,
70 components,
71 keyid,
72 alg,
73 created,
74 })
75}
76
77fn parse_components_list(input: &str) -> Result<Vec<String>, HttpSigError> {
81 let start = input.find('(').ok_or_else(|| {
82 HttpSigError::ParseError("Missing opening parenthesis in component list".to_string())
83 })?;
84
85 let end = input.find(')').ok_or_else(|| {
86 HttpSigError::ParseError("Missing closing parenthesis in component list".to_string())
87 })?;
88
89 let list_str = &input[start + 1..end];
90
91 let components: Vec<String> = list_str
92 .split_whitespace()
93 .map(|s| s.trim_matches('"').to_string())
94 .filter(|s| !s.is_empty())
95 .collect();
96
97 if components.is_empty() {
98 return Err(HttpSigError::ParseError(
99 "Empty component list".to_string(),
100 ));
101 }
102
103 Ok(components)
104}
105
106fn extract_param(params: &str, name: &str) -> Result<String, HttpSigError> {
110 let pattern = format!("{}=", name);
111 let start = params
112 .find(&pattern)
113 .ok_or_else(|| HttpSigError::ParseError(format!("Missing parameter: {}", name)))?
114 + pattern.len();
115
116 let rest = ¶ms[start..];
117
118 if rest.starts_with('"') {
120 let end = rest[1..]
121 .find('"')
122 .ok_or_else(|| HttpSigError::ParseError(format!("Unclosed quote for {}", name)))?
123 + 1;
124 Ok(rest[1..end].to_string())
125 } else {
126 let end = rest.find(';').unwrap_or(rest.len());
128 Ok(rest[..end].trim().to_string())
129 }
130}
131
132pub fn parse_signature(header: &str, label: &str) -> Result<Vec<u8>, HttpSigError> {
136 let pattern = format!("{}=:", label);
137 let start = header.find(&pattern).ok_or_else(|| {
138 HttpSigError::InvalidSignature(format!("Signature label '{}' not found", label))
139 })? + pattern.len();
140
141 let rest = &header[start..];
142 let end = rest
143 .find(':')
144 .ok_or_else(|| HttpSigError::InvalidSignature("Missing closing colon".to_string()))?;
145
146 let b64_sig = &rest[..end];
147
148 base64::engine::general_purpose::STANDARD
149 .decode(b64_sig)
150 .map_err(|e| HttpSigError::InvalidSignature(format!("Invalid base64: {}", e)))
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_parse_signature_input() {
159 let header = r#"sig1=("@method" "@path" "content-digest");keyid="user-123";alg="ed25519";created=1234567890"#;
160 let params = parse_signature_input(header).unwrap();
161
162 assert_eq!(params.label, "sig1");
163 assert_eq!(params.components.len(), 3);
164 assert_eq!(params.components[0], "@method");
165 assert_eq!(params.components[1], "@path");
166 assert_eq!(params.components[2], "content-digest");
167 assert_eq!(params.keyid, "user-123");
168 assert_eq!(params.alg, "ed25519");
169 assert_eq!(params.created, 1234567890);
170 }
171
172 #[test]
173 fn test_extract_key_id() {
174 let header = r#"sig1=("@method");keyid="test-key";alg="ed25519";created=123"#;
175 let key_id = extract_key_id(header).unwrap();
176 assert_eq!(key_id, "test-key");
177 }
178
179 #[test]
180 fn test_parse_components_list() {
181 let input = r#"("@method" "@path" "content-digest");keyid="x""#;
182 let components = parse_components_list(input).unwrap();
183
184 assert_eq!(components.len(), 3);
185 assert_eq!(components[0], "@method");
186 assert_eq!(components[1], "@path");
187 assert_eq!(components[2], "content-digest");
188 }
189
190 #[test]
191 fn test_extract_param_quoted() {
192 let params = r#"keyid="user-123";alg="ed25519""#;
193 assert_eq!(extract_param(params, "keyid").unwrap(), "user-123");
194 assert_eq!(extract_param(params, "alg").unwrap(), "ed25519");
195 }
196
197 #[test]
198 fn test_extract_param_unquoted() {
199 let params = "created=1234567890;other=value";
200 assert_eq!(extract_param(params, "created").unwrap(), "1234567890");
201 }
202
203 #[test]
204 fn test_parse_signature() {
205 let header = "sig1=:YWJjZGVm:";
206 let sig_bytes = parse_signature(header, "sig1").unwrap();
207 assert_eq!(sig_bytes, b"abcdef");
208 }
209
210 #[test]
211 fn test_parse_signature_invalid_label() {
212 let header = "sig1=:YWJjZGVm:";
213 let result = parse_signature(header, "sig2");
214 assert!(matches!(result, Err(HttpSigError::InvalidSignature(_))));
215 }
216}