1use base64::prelude::*;
33use ring::hmac;
34use serde::Deserialize;
35
36use crate::error::Problem;
37use crate::jws::Jwk;
38
39pub type EabJws = crate::jws::AcmeJwsRequest;
42
43#[derive(Debug, Deserialize)]
46pub struct EabHeader {
47 pub alg: String,
48 pub kid: String,
49 pub url: String,
50}
51
52#[derive(Debug)]
59pub enum EabError {
60 Malformed(&'static str),
61 BadSignature,
62}
63
64const SUPPORTED_ALG: &str = "HS256";
67
68pub fn parse_header(eab: &EabJws, expected_url: &str) -> Result<EabHeader, EabError> {
77 let protected_bytes = BASE64_URL_SAFE_NO_PAD
78 .decode(&eab.protected)
79 .map_err(|_| EabError::Malformed("EAB protected base64 invalid"))?;
80
81 let header: EabHeader = serde_json::from_slice(&protected_bytes)
82 .map_err(|_| EabError::Malformed("EAB protected JSON invalid"))?;
83
84 if header.alg != SUPPORTED_ALG {
85 return Err(EabError::Malformed("EAB alg must be HS256"));
86 }
87 if header.url != expected_url {
88 return Err(EabError::Malformed("EAB url does not match the request"));
89 }
90
91 Ok(header)
92}
93
94pub fn verify_payload_and_signature(
99 eab: &EabJws,
100 hmac_secret: &[u8],
101 outer_jwk: &Jwk,
102) -> Result<(), EabError> {
103 let payload_bytes = BASE64_URL_SAFE_NO_PAD
104 .decode(&eab.payload)
105 .map_err(|_| EabError::Malformed("EAB payload base64 invalid"))?;
106 let inner_jwk: Jwk = serde_json::from_slice(&payload_bytes)
107 .map_err(|_| EabError::Malformed("EAB payload is not a JWK"))?;
108 if &inner_jwk != outer_jwk {
109 return Err(EabError::Malformed(
110 "EAB payload JWK does not match the account key",
111 ));
112 }
113
114 let sig_bytes = BASE64_URL_SAFE_NO_PAD
115 .decode(&eab.signature)
116 .map_err(|_| EabError::Malformed("EAB signature base64 invalid"))?;
117
118 let signing_input = format!("{}.{}", eab.protected, eab.payload);
119 let key = hmac::Key::new(hmac::HMAC_SHA256, hmac_secret);
120 hmac::verify(&key, signing_input.as_bytes(), &sig_bytes).map_err(|_| EabError::BadSignature)
121}
122
123pub fn eab_problem(error: EabError) -> Problem {
130 match error {
131 EabError::Malformed(detail) => Problem::malformed(detail),
132 EabError::BadSignature => {
133 Problem::unauthorized("External Account Binding signature invalid")
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use serde_json::json;
142
143 fn jwk_a() -> Jwk {
144 Jwk::EC {
145 crv: "P-256".to_string(),
146 x: "x-a".to_string(),
147 y: "y-a".to_string(),
148 }
149 }
150
151 fn jwk_b() -> Jwk {
152 Jwk::EC {
153 crv: "P-256".to_string(),
154 x: "x-b".to_string(),
155 y: "y-b".to_string(),
156 }
157 }
158
159 fn b64_json(value: &serde_json::Value) -> String {
160 BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(value).unwrap())
161 }
162
163 fn jwk_json(jwk: &Jwk) -> serde_json::Value {
164 match jwk {
165 Jwk::EC { crv, x, y } => json!({ "kty": "EC", "crv": crv, "x": x, "y": y }),
166 Jwk::RSA { n, e } => json!({ "kty": "RSA", "n": n, "e": e }),
167 }
168 }
169
170 fn build(secret: &[u8], alg: &str, kid: &str, url: &str, jwk: &Jwk) -> EabJws {
173 let protected = json!({ "alg": alg, "kid": kid, "url": url });
174 let protected_b64 = b64_json(&protected);
175 let payload_b64 = b64_json(&jwk_json(jwk));
176 let signing_input = format!("{protected_b64}.{payload_b64}");
177 let key = hmac::Key::new(hmac::HMAC_SHA256, secret);
178 let signature = hmac::sign(&key, signing_input.as_bytes());
179 EabJws {
180 protected: protected_b64,
181 payload: payload_b64,
182 signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()),
183 }
184 }
185
186 const SECRET: &[u8] = b"01234567890123456789012345678901";
187 const URL: &str = "http://localhost:3000/newAccount";
188
189 #[test]
190 fn parse_header_accepts_hs256_and_matching_url() {
191 let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
192 let header = parse_header(&eab, URL).unwrap();
193 assert_eq!(header.kid, "kid-1");
194 }
195
196 #[test]
197 fn parse_header_rejects_unsupported_alg() {
198 let eab = build(SECRET, "HS384", "kid-1", URL, &jwk_a());
199 assert!(matches!(
200 parse_header(&eab, URL),
201 Err(EabError::Malformed(_))
202 ));
203 }
204
205 #[test]
206 fn parse_header_rejects_url_mismatch() {
207 let eab = build(
208 SECRET,
209 "HS256",
210 "kid-1",
211 "http://localhost:3000/other",
212 &jwk_a(),
213 );
214 assert!(matches!(
215 parse_header(&eab, URL),
216 Err(EabError::Malformed(_))
217 ));
218 }
219
220 #[test]
221 fn parse_header_rejects_malformed_base64_and_json() {
222 let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
223 eab.protected = "!!!not-base64!!!".to_string();
224 assert!(matches!(
225 parse_header(&eab, URL),
226 Err(EabError::Malformed(_))
227 ));
228
229 let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
230 eab.protected = BASE64_URL_SAFE_NO_PAD.encode(b"not json");
231 assert!(matches!(
232 parse_header(&eab, URL),
233 Err(EabError::Malformed(_))
234 ));
235 }
236
237 #[test]
240 fn parse_header_ignores_an_extra_nonce_field() {
241 let protected = json!({ "alg": "HS256", "kid": "kid-1", "url": URL, "nonce": "n" });
242 let protected_b64 = b64_json(&protected);
243 let eab = EabJws {
244 protected: protected_b64,
245 payload: String::new(),
246 signature: String::new(),
247 };
248 assert!(parse_header(&eab, URL).is_ok());
249 }
250
251 #[test]
252 fn verify_payload_and_signature_accepts_correct_hmac() {
253 let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
254 assert!(verify_payload_and_signature(&eab, SECRET, &jwk_a()).is_ok());
255 }
256
257 #[test]
258 fn verify_payload_and_signature_rejects_wrong_secret() {
259 let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
260 assert!(matches!(
261 verify_payload_and_signature(&eab, b"a-completely-different-secret!!", &jwk_a()),
262 Err(EabError::BadSignature)
263 ));
264 }
265
266 #[test]
267 fn verify_payload_and_signature_rejects_a_tampered_signature() {
268 let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
269 eab.signature = BASE64_URL_SAFE_NO_PAD.encode([0u8; 32]);
270 assert!(matches!(
271 verify_payload_and_signature(&eab, SECRET, &jwk_a()),
272 Err(EabError::BadSignature)
273 ));
274 }
275
276 #[test]
277 fn verify_payload_and_signature_rejects_a_jwk_payload_mismatch() {
278 let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
282 assert!(matches!(
283 verify_payload_and_signature(&eab, SECRET, &jwk_b()),
284 Err(EabError::Malformed(_))
285 ));
286 }
287
288 #[test]
289 fn verify_payload_and_signature_rejects_malformed_payload_json() {
290 let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
291 eab.payload = BASE64_URL_SAFE_NO_PAD.encode(b"not a jwk");
292 assert!(matches!(
293 verify_payload_and_signature(&eab, SECRET, &jwk_a()),
294 Err(EabError::Malformed(_))
295 ));
296 }
297}