use std::sync::Arc;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
use serde_json::{Value, json};
use x509_parser::prelude::FromDer;
use crate::acme::provider;
use crate::error::{AcmeError, Error, Result};
pub const JWS_CONTENT_TYPE: &str = "jose+json";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureAlgorithm {
Es256,
Ed25519,
Rs256,
}
impl SignatureAlgorithm {
#[must_use]
pub fn jose_name(self) -> &'static str {
match self {
Self::Es256 => "ES256",
Self::Ed25519 => "EdDSA",
Self::Rs256 => "RS256",
}
}
}
#[derive(Clone)]
pub struct AccountKey {
pkcs8: Arc<Vec<u8>>,
alg: SignatureAlgorithm,
}
impl std::fmt::Debug for AccountKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AccountKey")
.field("algorithm", &self.alg)
.finish_non_exhaustive()
}
}
impl AccountKey {
pub fn generate_es256() -> Result<Self> {
Ok(Self {
pkcs8: Arc::new(provider::generate_es256()?),
alg: SignatureAlgorithm::Es256,
})
}
pub fn generate_ed25519() -> Result<Self> {
Ok(Self {
pkcs8: Arc::new(provider::generate_ed25519()?),
alg: SignatureAlgorithm::Ed25519,
})
}
#[cfg(feature = "rsa")]
pub fn generate_rsa2048() -> Result<Self> {
let key =
rcgen::KeyPair::generate_rsa_for(&rcgen::PKCS_RSA_SHA256, rcgen::RsaKeySize::_2048)
.map_err(|e| Error::Acme(AcmeError::Jws(format!("RSA keygen: {e}"))))?;
Ok(Self {
pkcs8: Arc::new(key.serialize_der().to_vec()),
alg: SignatureAlgorithm::Rs256,
})
}
pub fn from_pkcs8_der(der: &[u8]) -> Result<Self> {
if let Some(alg) = provider::detect_algorithm(der) {
return Ok(Self {
pkcs8: Arc::new(der.to_vec()),
alg,
});
}
Err(Error::Acme(AcmeError::Jws(
"unsupported or invalid PKCS#8 account key".into(),
)))
}
pub fn from_pkcs8_pem(pem: &[u8]) -> Result<Self> {
let section = crate::pem::first_section_with_label(pem, &["PRIVATE KEY"])
.ok_or_else(|| Error::Acme(AcmeError::Jws("no PRIVATE KEY PEM block".into())))?;
Self::from_pkcs8_der(§ion.der)
}
#[must_use]
pub fn pkcs8_der(&self) -> &[u8] {
&self.pkcs8
}
#[must_use]
pub fn algorithm(&self) -> SignatureAlgorithm {
self.alg
}
pub fn jwk(&self) -> Result<Jwk> {
match self.alg {
SignatureAlgorithm::Es256 => {
let pk = provider::public_key(&self.pkcs8, self.alg)?; if pk.len() != 65 || pk[0] != 0x04 {
return Err(Error::Acme(AcmeError::Jws(
"unexpected public key encoding".into(),
)));
}
Ok(Jwk {
value: json!({
"kty": "EC",
"crv": "P-256",
"x": B64URL.encode(&pk[1..33]),
"y": B64URL.encode(&pk[33..65]),
}),
})
}
SignatureAlgorithm::Ed25519 => {
let pk = provider::public_key(&self.pkcs8, self.alg)?;
Ok(Jwk {
value: json!({
"kty": "OKP",
"crv": "Ed25519",
"x": B64URL.encode(pk),
}),
})
}
SignatureAlgorithm::Rs256 => {
let public = provider::public_key(&self.pkcs8, self.alg)?;
let (_, rsa) = x509_parser::public_key::RSAPublicKey::from_der(&public)
.map_err(|e| Error::Acme(AcmeError::Jws(format!("RSA public key: {e}"))))?;
Ok(Jwk {
value: json!({
"kty": "RSA",
"n": B64URL.encode(rsa.modulus.strip_prefix(&[0]).unwrap_or(rsa.modulus)),
"e": B64URL.encode(rsa.exponent.strip_prefix(&[0]).unwrap_or(rsa.exponent)),
}),
})
}
}
}
pub fn thumbprint(&self) -> Result<Vec<u8>> {
let jwk = self.jwk()?.value;
let canonical = thumbprint_input(&jwk)?;
let mut digest = [0u8; 32];
use sha2::Digest;
digest.copy_from_slice(sha2::Sha256::digest(canonical.as_bytes()).as_slice());
Ok(digest.to_vec())
}
pub fn thumbprint_b64(&self) -> Result<String> {
Ok(B64URL.encode(self.thumbprint()?))
}
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
provider::sign(&self.pkcs8, self.alg, message)
}
pub fn sign_jws(
&self,
url: &str,
nonce: &str,
kid: Option<&str>,
payload: Option<&Value>,
) -> Result<Value> {
let mut protected = json!({
"alg": self.alg.jose_name(),
"nonce": nonce,
"url": url,
});
match kid {
Some(kid) => protected["kid"] = Value::String(kid.to_owned()),
None => protected["jwk"] = self.jwk()?.value,
}
let protected_b64 = B64URL.encode(
serde_json::to_string(&protected)
.map_err(|e| Error::Acme(AcmeError::Jws(format!("protected: {e}"))))?,
);
let payload_b64 = match payload {
Some(v) => B64URL.encode(
serde_json::to_vec(v)
.map_err(|e| Error::Acme(AcmeError::Jws(format!("payload: {e}"))))?,
),
None => String::new(),
};
let signing_input = format!("{protected_b64}.{payload_b64}");
let sig = self.sign(signing_input.as_bytes())?;
Ok(json!({
"protected": protected_b64,
"payload": payload_b64,
"signature": B64URL.encode(sig),
}))
}
}
#[derive(Debug, Clone)]
pub struct Jwk {
pub value: Value,
}
fn thumbprint_input(jwk: &Value) -> Result<String> {
let mut required: Vec<(String, &Value)> = jwk
.as_object()
.ok_or_else(|| Error::Acme(AcmeError::Jws("jwk not an object".into())))?
.iter()
.filter(|(k, _)| matches!(k.as_str(), "kty" | "crv" | "x" | "y" | "n" | "e"))
.map(|(k, v)| (k.clone(), v))
.collect();
required.sort_by(|a, b| a.0.cmp(&b.0));
let map: serde_json::Map<String, Value> =
required.into_iter().map(|(k, v)| (k, v.clone())).collect();
serde_json::to_string(&Value::Object(map))
.map_err(|e| Error::Acme(AcmeError::Jws(format!("thumbprint json: {e}"))))
}
pub fn key_authorization(token: &str, thumbprint_b64: &str) -> Result<String> {
Ok(format!("{token}.{thumbprint_b64}"))
}
pub fn dns_01_txt_value(key_auth: &str) -> Result<String> {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(key_auth.as_bytes());
Ok(B64URL.encode(hasher.finalize()))
}
pub fn tls_alpn_01_digest(key_auth: &str) -> Result<[u8; 32]> {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(key_auth.as_bytes());
let out: [u8; 32] = hasher.finalize().into();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn review_account_key_debug_redacts_private_material() {
let key = AccountKey::generate_es256().unwrap();
let debug = format!("{key:?}");
assert!(!debug.contains(&format!("{:?}", key.pkcs8)));
assert!(!debug.contains("pkcs8"));
assert!(debug.contains("Es256"));
}
#[test]
fn key_authorization_digest_known_answer() {
let ka = key_authorization("token", "thumbprint").unwrap();
assert_eq!(ka, "token.thumbprint");
assert_eq!(
dns_01_txt_value(&ka).unwrap(),
"61rBZ_4knHblO0MNoxFsXZ_eTFUHum0B6IVRbhvUn5I"
);
let digest = tls_alpn_01_digest(&ka).unwrap();
assert_eq!(
hex::encode(digest),
"eb5ac167fe249c76e53b430da3116c5d9fde4c5507ba6d01e885516e1bd49f92"
);
}
#[test]
fn es256_keygen_jwk_thumbprint_sign_verify() {
let key = AccountKey::generate_es256().unwrap();
let jwk = key.jwk().unwrap().value;
assert_eq!(jwk["kty"], "EC");
assert_eq!(jwk["crv"], "P-256");
let x = jwk["x"].as_str().unwrap();
let y = jwk["y"].as_str().unwrap();
assert!(
!x.contains('=') && !y.contains('='),
"base64url has no padding"
);
let tp = key.thumbprint().unwrap();
assert_eq!(tp.len(), 32);
assert_eq!(key.thumbprint().unwrap(), tp);
let msg = b"protected.payload";
let sig = key.sign(msg).unwrap();
assert_eq!(sig.len(), 64, "fixed-width P-256 r||s");
let public = provider::public_key(key.pkcs8_der(), key.algorithm()).unwrap();
assert!(provider::verify_signature(
msg,
&der_ecdsa_signature_for_test(&sig),
&public,
"1.2.840.10045.4.3.2"
));
}
fn der_ecdsa_signature_for_test(fixed: &[u8]) -> Vec<u8> {
fn integer(bytes: &[u8]) -> Vec<u8> {
let first_nonzero = bytes
.iter()
.position(|byte| *byte != 0)
.unwrap_or(bytes.len().saturating_sub(1));
let mut value = bytes[first_nonzero..].to_vec();
if value.first().is_some_and(|byte| byte & 0x80 != 0) {
value.insert(0, 0);
}
let mut out = vec![0x02, value.len() as u8];
out.extend(value);
out
}
let r = integer(&fixed[..fixed.len() / 2]);
let s = integer(&fixed[fixed.len() / 2..]);
let mut content = r;
content.extend(s);
let mut out = vec![0x30, content.len() as u8];
out.extend(content);
out
}
#[cfg(feature = "rsa")]
#[test]
fn rsa2048_keygen_jwk_and_sign() {
let key = AccountKey::generate_rsa2048().unwrap();
assert_eq!(key.algorithm(), SignatureAlgorithm::Rs256);
let jwk = key.jwk().unwrap().value;
assert_eq!(jwk["kty"], "RSA");
assert!(!jwk["n"].as_str().unwrap().is_empty());
assert_eq!(jwk["e"], "AQAB");
assert_eq!(key.sign(b"account-payload").unwrap().len(), 256);
}
#[test]
fn ed25519_keygen_and_sign() {
let key = AccountKey::generate_ed25519().unwrap();
let jwk = key.jwk().unwrap().value;
assert_eq!(jwk["kty"], "OKP");
let sig = key.sign(b"abc").unwrap();
assert_eq!(sig.len(), 64);
}
#[test]
fn jws_structure_with_kid_and_jwk() {
let key = AccountKey::generate_es256().unwrap();
let jws = key
.sign_jws(
"https://ca.example/new-acct",
"nonce1",
None,
Some(&json!({})),
)
.unwrap();
let protected: Value =
serde_json::from_slice(&B64URL.decode(jws["protected"].as_str().unwrap()).unwrap())
.unwrap();
assert_eq!(protected["alg"], "ES256");
assert_eq!(protected["nonce"], "nonce1");
assert_eq!(protected["url"], "https://ca.example/new-acct");
assert!(protected["jwk"].is_object());
assert_eq!(
jws["payload"], "e30",
"payload is base64url of the JSON body"
);
let jws_kid = key
.sign_jws("https://ca.example/order", "n2", Some("kid-1"), None)
.unwrap();
let protected: Value = serde_json::from_slice(
&B64URL
.decode(jws_kid["protected"].as_str().unwrap())
.unwrap(),
)
.unwrap();
assert_eq!(protected["kid"], "kid-1");
assert!(protected.get("jwk").is_none());
assert_eq!(jws_kid["payload"], "", "POST-as-GET empty payload");
assert!(!jws_kid["signature"].as_str().unwrap().is_empty());
}
#[test]
fn pkcs8_roundtrip() {
let key = AccountKey::generate_es256().unwrap();
let imported = AccountKey::from_pkcs8_der(key.pkcs8_der()).unwrap();
assert_eq!(imported.thumbprint().unwrap(), key.thumbprint().unwrap());
let pem = crate::pem::encode("PRIVATE KEY", key.pkcs8_der());
let from_pem = AccountKey::from_pkcs8_pem(&pem).unwrap();
assert_eq!(from_pem.thumbprint().unwrap(), key.thumbprint().unwrap());
}
}