use base64::prelude::*;
use ring::hmac;
use serde::Deserialize;
use crate::error::Problem;
use crate::extractors::acme::Jwk;
pub(crate) type EabJws = crate::extractors::acme::AcmeJwsRequest;
#[derive(Debug, Deserialize)]
pub(crate) struct EabHeader {
pub alg: String,
pub kid: String,
pub url: String,
}
#[derive(Debug)]
pub(crate) enum EabError {
Malformed(&'static str),
BadSignature,
}
const SUPPORTED_ALG: &str = "HS256";
pub(crate) fn parse_header(eab: &EabJws, expected_url: &str) -> Result<EabHeader, EabError> {
let protected_bytes = BASE64_URL_SAFE_NO_PAD
.decode(&eab.protected)
.map_err(|_| EabError::Malformed("EAB protected base64 invalid"))?;
let header: EabHeader = serde_json::from_slice(&protected_bytes)
.map_err(|_| EabError::Malformed("EAB protected JSON invalid"))?;
if header.alg != SUPPORTED_ALG {
return Err(EabError::Malformed("EAB alg must be HS256"));
}
if header.url != expected_url {
return Err(EabError::Malformed("EAB url does not match the request"));
}
Ok(header)
}
pub(crate) fn verify_payload_and_signature(
eab: &EabJws,
hmac_secret: &[u8],
outer_jwk: &Jwk,
) -> Result<(), EabError> {
let payload_bytes = BASE64_URL_SAFE_NO_PAD
.decode(&eab.payload)
.map_err(|_| EabError::Malformed("EAB payload base64 invalid"))?;
let inner_jwk: Jwk = serde_json::from_slice(&payload_bytes)
.map_err(|_| EabError::Malformed("EAB payload is not a JWK"))?;
if &inner_jwk != outer_jwk {
return Err(EabError::Malformed(
"EAB payload JWK does not match the account key",
));
}
let sig_bytes = BASE64_URL_SAFE_NO_PAD
.decode(&eab.signature)
.map_err(|_| EabError::Malformed("EAB signature base64 invalid"))?;
let signing_input = format!("{}.{}", eab.protected, eab.payload);
let key = hmac::Key::new(hmac::HMAC_SHA256, hmac_secret);
hmac::verify(&key, signing_input.as_bytes(), &sig_bytes).map_err(|_| EabError::BadSignature)
}
pub(crate) fn eab_problem(error: EabError) -> Problem {
match error {
EabError::Malformed(detail) => Problem::malformed(detail),
EabError::BadSignature => {
Problem::unauthorized("External Account Binding signature invalid")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn jwk_a() -> Jwk {
Jwk::EC {
crv: "P-256".to_string(),
x: "x-a".to_string(),
y: "y-a".to_string(),
}
}
fn jwk_b() -> Jwk {
Jwk::EC {
crv: "P-256".to_string(),
x: "x-b".to_string(),
y: "y-b".to_string(),
}
}
fn b64_json(value: &serde_json::Value) -> String {
BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(value).unwrap())
}
fn jwk_json(jwk: &Jwk) -> serde_json::Value {
match jwk {
Jwk::EC { crv, x, y } => json!({ "kty": "EC", "crv": crv, "x": x, "y": y }),
Jwk::RSA { n, e } => json!({ "kty": "RSA", "n": n, "e": e }),
}
}
fn build(secret: &[u8], alg: &str, kid: &str, url: &str, jwk: &Jwk) -> EabJws {
let protected = json!({ "alg": alg, "kid": kid, "url": url });
let protected_b64 = b64_json(&protected);
let payload_b64 = b64_json(&jwk_json(jwk));
let signing_input = format!("{protected_b64}.{payload_b64}");
let key = hmac::Key::new(hmac::HMAC_SHA256, secret);
let signature = hmac::sign(&key, signing_input.as_bytes());
EabJws {
protected: protected_b64,
payload: payload_b64,
signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()),
}
}
const SECRET: &[u8] = b"01234567890123456789012345678901";
const URL: &str = "http://localhost:3000/newAccount";
#[test]
fn parse_header_accepts_hs256_and_matching_url() {
let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
let header = parse_header(&eab, URL).unwrap();
assert_eq!(header.kid, "kid-1");
}
#[test]
fn parse_header_rejects_unsupported_alg() {
let eab = build(SECRET, "HS384", "kid-1", URL, &jwk_a());
assert!(matches!(
parse_header(&eab, URL),
Err(EabError::Malformed(_))
));
}
#[test]
fn parse_header_rejects_url_mismatch() {
let eab = build(
SECRET,
"HS256",
"kid-1",
"http://localhost:3000/other",
&jwk_a(),
);
assert!(matches!(
parse_header(&eab, URL),
Err(EabError::Malformed(_))
));
}
#[test]
fn parse_header_rejects_malformed_base64_and_json() {
let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
eab.protected = "!!!not-base64!!!".to_string();
assert!(matches!(
parse_header(&eab, URL),
Err(EabError::Malformed(_))
));
let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
eab.protected = BASE64_URL_SAFE_NO_PAD.encode(b"not json");
assert!(matches!(
parse_header(&eab, URL),
Err(EabError::Malformed(_))
));
}
#[test]
fn parse_header_ignores_an_extra_nonce_field() {
let protected = json!({ "alg": "HS256", "kid": "kid-1", "url": URL, "nonce": "n" });
let protected_b64 = b64_json(&protected);
let eab = EabJws {
protected: protected_b64,
payload: String::new(),
signature: String::new(),
};
assert!(parse_header(&eab, URL).is_ok());
}
#[test]
fn verify_payload_and_signature_accepts_correct_hmac() {
let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
assert!(verify_payload_and_signature(&eab, SECRET, &jwk_a()).is_ok());
}
#[test]
fn verify_payload_and_signature_rejects_wrong_secret() {
let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
assert!(matches!(
verify_payload_and_signature(&eab, b"a-completely-different-secret!!", &jwk_a()),
Err(EabError::BadSignature)
));
}
#[test]
fn verify_payload_and_signature_rejects_a_tampered_signature() {
let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
eab.signature = BASE64_URL_SAFE_NO_PAD.encode([0u8; 32]);
assert!(matches!(
verify_payload_and_signature(&eab, SECRET, &jwk_a()),
Err(EabError::BadSignature)
));
}
#[test]
fn verify_payload_and_signature_rejects_a_jwk_payload_mismatch() {
let eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
assert!(matches!(
verify_payload_and_signature(&eab, SECRET, &jwk_b()),
Err(EabError::Malformed(_))
));
}
#[test]
fn verify_payload_and_signature_rejects_malformed_payload_json() {
let mut eab = build(SECRET, "HS256", "kid-1", URL, &jwk_a());
eab.payload = BASE64_URL_SAFE_NO_PAD.encode(b"not a jwk");
assert!(matches!(
verify_payload_and_signature(&eab, SECRET, &jwk_a()),
Err(EabError::Malformed(_))
));
}
}