use base64::prelude::*;
use ring::hmac;
use serde_json::Value;
use crate::eab::EabJws;
const ALG: &str = "HS256";
pub(crate) fn build(
kid: &str,
hmac_secret: &[u8],
account_jwk: &Value,
new_account_url: &str,
) -> EabJws {
let protected = serde_json::json!({ "alg": ALG, "kid": kid, "url": new_account_url });
let protected_b64 =
BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(&protected).unwrap_or_default());
let payload_b64 =
BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(account_jwk).unwrap_or_default());
let key = hmac::Key::new(hmac::HMAC_SHA256, hmac_secret);
let signature = hmac::sign(&key, format!("{protected_b64}.{payload_b64}").as_bytes());
EabJws {
protected: protected_b64,
payload: payload_b64,
signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()),
}
}
pub(crate) fn decode_secret(value: &str) -> Option<Vec<u8>> {
if value.is_empty() {
return None;
}
BASE64_URL_SAFE_NO_PAD
.decode(value)
.or_else(|_| BASE64_URL_SAFE.decode(value))
.or_else(|_| BASE64_STANDARD.decode(value))
.ok()
.filter(|decoded| !decoded.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eab::{EabError, parse_header, verify_payload_and_signature};
use crate::extractors::acme::Jwk;
const SECRET: &[u8] = b"01234567890123456789012345678901";
const URL: &str = "https://upstream.example/newAccount";
fn jwk_value() -> Value {
serde_json::json!({ "crv": "P-256", "kty": "EC", "x": "x-value", "y": "y-value" })
}
fn jwk_typed() -> Jwk {
Jwk::EC {
crv: "P-256".to_string(),
x: "x-value".to_string(),
y: "y-value".to_string(),
}
}
#[test]
fn a_built_eab_verifies_with_this_crates_own_verifier() {
let eab = build("kid-1", SECRET, &jwk_value(), URL);
let header = parse_header(&eab, URL).expect("the header must be well formed");
assert_eq!(header.kid, "kid-1");
assert_eq!(header.alg, "HS256");
verify_payload_and_signature(&eab, SECRET, &jwk_typed())
.expect("the signature must verify against the same secret");
}
#[test]
fn the_url_is_bound_into_the_signature() {
let eab = build("kid-1", SECRET, &jwk_value(), URL);
assert!(matches!(
parse_header(&eab, "https://upstream.example/other"),
Err(EabError::Malformed(_))
));
}
#[test]
fn a_different_secret_does_not_verify() {
let eab = build("kid-1", SECRET, &jwk_value(), URL);
assert!(matches!(
verify_payload_and_signature(&eab, b"a completely different secret!!", &jwk_typed()),
Err(EabError::BadSignature)
));
}
#[test]
fn the_account_key_is_bound_into_the_payload() {
let eab = build("kid-1", SECRET, &jwk_value(), URL);
let other = Jwk::EC {
crv: "P-256".to_string(),
x: "different".to_string(),
y: "different".to_string(),
};
assert!(matches!(
verify_payload_and_signature(&eab, SECRET, &other),
Err(EabError::Malformed(_))
));
}
#[test]
fn a_real_account_keys_jwk_round_trips() {
let pkcs8 = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
.unwrap()
.serialize_der();
let account = super::super::client::AccountKey::from_pkcs8(&pkcs8).unwrap();
let eab = build("kid-1", SECRET, &account.jwk(), URL);
parse_header(&eab, URL).unwrap();
let spki_jwk: Jwk = serde_json::from_value(account.jwk()).unwrap();
verify_payload_and_signature(&eab, SECRET, &spki_jwk).unwrap();
}
#[test]
fn a_base64url_secret_decodes() {
let secret = b"01234567890123456789012345678901";
let encoded = BASE64_URL_SAFE_NO_PAD.encode(secret);
assert_eq!(decode_secret(&encoded).as_deref(), Some(&secret[..]));
}
#[test]
fn a_standard_base64_secret_decodes() {
let secret = b"\xff\xfe\xfd\xfc some bytes";
let encoded = BASE64_STANDARD.encode(secret);
assert_eq!(decode_secret(&encoded).as_deref(), Some(&secret[..]));
}
#[test]
fn a_non_base64_secret_is_refused() {
assert!(decode_secret("not base64!!!").is_none());
assert!(decode_secret("").is_none());
}
}