use std::any::Any;
use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct IssuedCertificate {
pub certificate: Vec<u8>,
pub metadata: Option<serde_json::Value>,
}
#[derive(Clone, Serialize, Deserialize, Default)]
pub struct CertificateResource {
#[serde(default)]
pub sans: Vec<String>,
#[serde(skip)]
pub certificate_pem: Vec<u8>,
#[serde(skip)]
pub private_key_pem: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issuer_data: Option<serde_json::Value>,
}
impl std::fmt::Debug for CertificateResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CertificateResource")
.field("sans", &self.sans)
.field("certificate_bytes", &self.certificate_pem.len())
.finish_non_exhaustive()
}
}
impl CertificateResource {
#[must_use]
pub fn names_key(&self) -> String {
let mut names = self.sans.clone();
names.sort();
let joined = names.join(",");
if joined.len() > 1024 {
let mut end = 1024;
while !joined.is_char_boundary(end) {
end -= 1;
}
format!("{}_trunc", &joined[..end])
} else {
joined
}
}
}
#[async_trait]
pub trait Issuer: Send + Sync + std::fmt::Debug {
async fn issue(
&self,
ct: &CancellationToken,
csr: &crate::issuer::Csr,
attempt: u32,
) -> Result<IssuedCertificate>;
async fn issue_with_replaces(
&self,
ct: &CancellationToken,
csr: &crate::issuer::Csr,
attempt: u32,
_replaces: Option<&str>,
) -> Result<IssuedCertificate> {
self.issue(ct, csr, attempt).await
}
fn issuer_key(&self) -> String;
fn as_any(&self) -> &dyn Any;
async fn pre_check(
&self,
_ct: &CancellationToken,
_names: &[String],
_interactive: bool,
) -> Result<()> {
Ok(())
}
async fn revoke(
&self,
_ct: &CancellationToken,
_resource: &CertificateResource,
_reason: RevocationReason,
) -> Result<()> {
Err(crate::error::Error::Issuer(
crate::error::IssuerError::Other("revocation not supported by this issuer".into()),
))
}
async fn get_renewal_info(
&self,
_ct: &CancellationToken,
_cert: &crate::certificate::Certificate,
) -> Result<crate::certificate::RenewalInfo> {
Err(crate::error::Error::Issuer(
crate::error::IssuerError::Other("ARI not supported by this issuer".into()),
))
}
}
#[async_trait]
pub trait PreChecker: Send + Sync {
async fn pre_check(
&self,
ct: &CancellationToken,
names: &[String],
interactive: bool,
) -> Result<()>;
}
#[async_trait]
pub trait Revoker: Send + Sync {
async fn revoke(
&self,
ct: &CancellationToken,
resource: &CertificateResource,
reason: RevocationReason,
) -> Result<()>;
}
#[async_trait]
impl<T: Issuer + ?Sized> PreChecker for T {
async fn pre_check(
&self,
ct: &CancellationToken,
names: &[String],
interactive: bool,
) -> Result<()> {
Issuer::pre_check(self, ct, names, interactive).await
}
}
#[async_trait]
impl<T: Issuer + ?Sized> Revoker for T {
async fn revoke(
&self,
ct: &CancellationToken,
resource: &CertificateResource,
reason: RevocationReason,
) -> Result<()> {
Issuer::revoke(self, ct, resource, reason).await
}
}
#[derive(Debug, Clone)]
pub struct Csr {
pub der: Vec<u8>,
pub dns_names: Vec<String>,
pub ip_addresses: Vec<std::net::IpAddr>,
}
impl Csr {
#[must_use]
pub fn primary_name(&self) -> &str {
self.dns_names.first().map(String::as_str).unwrap_or("")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum RevocationReason {
#[default]
Unspecified = 0,
KeyCompromise = 1,
CaCompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
PrivilegeWithdrawn = 9,
AuthorityCompromise = 10,
}
#[must_use]
pub fn as_acme_issuer(issuer: &Arc<dyn Issuer>) -> Option<&crate::acme::AcmeIssuer> {
issuer.as_any().downcast_ref::<crate::acme::AcmeIssuer>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn review_certificate_resource_debug_redacts_secrets() {
let resource = CertificateResource {
private_key_pem: vec![197, 198, 199],
issuer_data: Some(serde_json::json!({"secret": "issuer-secret"})),
..Default::default()
};
let debug = format!("{resource:?}");
assert!(!debug.contains("197"));
assert!(!debug.contains("issuer-secret"));
}
#[test]
fn names_key_sorts_and_truncates() {
let r = CertificateResource {
sans: vec!["b.com".into(), "a.com".into()],
..Default::default()
};
assert_eq!(r.names_key(), "a.com,b.com");
let long = CertificateResource {
sans: vec!["x".repeat(2000)],
..Default::default()
};
assert_eq!(long.names_key().len(), 1024 + "_trunc".len());
let unicode = CertificateResource {
sans: vec!["例".repeat(600)],
..Default::default()
};
assert!(unicode.names_key().ends_with("_trunc"));
}
#[test]
fn revocation_reason_codes() {
assert_eq!(RevocationReason::KeyCompromise as u8, 1);
assert_eq!(RevocationReason::Superseded as u8, 4);
}
}