Skip to main content

certmagic/
issuer.rs

1//! Issuer abstraction: certificate sources.
2
3use std::any::Any;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use tokio_util::sync::CancellationToken;
9
10use crate::error::Result;
11
12/// A certificate that an issuer produced.
13#[derive(Debug, Clone)]
14pub struct IssuedCertificate {
15    /// The certificate chain as PEM (leaf first).
16    pub certificate: Vec<u8>,
17    /// Issuer-specific metadata, serialized into the stored meta JSON
18    ///.
19    pub metadata: Option<serde_json::Value>,
20}
21
22/// A certificate resource as represented in storage
23///.
24#[derive(Clone, Serialize, Deserialize, Default)]
25pub struct CertificateResource {
26    /// All names covered by the certificate.
27    #[serde(default)]
28    pub sans: Vec<String>,
29    /// The certificate chain, PEM.
30    ///
31    /// This is persisted in the sibling `.crt` key, not in the metadata JSON.
32    /// `skip` also prevents private/certificate material from being duplicated
33    /// into metadata or exposed when callers serialize the resource.
34    #[serde(skip)]
35    pub certificate_pem: Vec<u8>,
36    /// The private key, PEM.
37    ///
38    /// This is persisted in the sibling `.key` key, not in the metadata JSON.
39    #[serde(skip)]
40    pub private_key_pem: Vec<u8>,
41    /// Issuer-specific data.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub issuer_data: Option<serde_json::Value>,
44}
45
46impl std::fmt::Debug for CertificateResource {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("CertificateResource")
49            .field("sans", &self.sans)
50            .field("certificate_bytes", &self.certificate_pem.len())
51            .finish_non_exhaustive()
52    }
53}
54
55impl CertificateResource {
56    /// Stable key for the set of names: sorted SANs joined by commas,
57    /// truncated with `_trunc` beyond 1024 chars.
58    #[must_use]
59    pub fn names_key(&self) -> String {
60        let mut names = self.sans.clone();
61        names.sort();
62        let joined = names.join(",");
63        if joined.len() > 1024 {
64            // SANs are normally ASCII, but storage keys also accept Unicode.
65            // Never slice a UTF-8 string at a continuation byte when keeping
66            // the bounded key prefix.
67            let mut end = 1024;
68            while !joined.is_char_boundary(end) {
69                end -= 1;
70            }
71            format!("{}_trunc", &joined[..end])
72        } else {
73            joined
74        }
75    }
76}
77
78/// A source of certificates.
79#[async_trait]
80pub trait Issuer: Send + Sync + std::fmt::Debug {
81    /// Obtain a certificate for the CSR.
82    ///
83    /// `attempt` is the retry counter (0 = first attempt). Issuers may use
84    /// it to
85    /// switch to a test CA on retries.
86    async fn issue(
87        &self,
88        ct: &CancellationToken,
89        csr: &crate::issuer::Csr,
90        attempt: u32,
91    ) -> Result<IssuedCertificate>;
92
93    /// Obtain a certificate while optionally telling an ACME-capable issuer
94    /// which certificate is being replaced for ARI-aware renewal. Custom
95    /// issuers keep the original behavior by default.
96    async fn issue_with_replaces(
97        &self,
98        ct: &CancellationToken,
99        csr: &crate::issuer::Csr,
100        attempt: u32,
101        _replaces: Option<&str>,
102    ) -> Result<IssuedCertificate> {
103        self.issue(ct, csr, attempt).await
104    }
105
106    /// Unique key identifying this issuer (storage prefix).
107    fn issuer_key(&self) -> String;
108
109    /// Access this issuer as `dyn Any` so callers can recover the concrete
110    /// type from an erased `Arc<dyn Issuer>` (trait objects need the
111    /// explicit downcast hook).
112    fn as_any(&self) -> &dyn Any;
113
114    /// Optional pre-checks before issuance.
115    async fn pre_check(
116        &self,
117        _ct: &CancellationToken,
118        _names: &[String],
119        _interactive: bool,
120    ) -> Result<()> {
121        Ok(())
122    }
123
124    /// Optional revocation.
125    async fn revoke(
126        &self,
127        _ct: &CancellationToken,
128        _resource: &CertificateResource,
129        _reason: RevocationReason,
130    ) -> Result<()> {
131        Err(crate::error::Error::Issuer(
132            crate::error::IssuerError::Other("revocation not supported by this issuer".into()),
133        ))
134    }
135
136    /// Optional ARI support.
137    async fn get_renewal_info(
138        &self,
139        _ct: &CancellationToken,
140        _cert: &crate::certificate::Certificate,
141    ) -> Result<crate::certificate::RenewalInfo> {
142        Err(crate::error::Error::Issuer(
143            crate::error::IssuerError::Other("ARI not supported by this issuer".into()),
144        ))
145    }
146}
147
148/// Optional pre-issuance validation capability.
149///
150/// Certmagic keeps these hooks as default methods on [`Issuer`] so that an
151/// issuer does not need to implement a second trait just to opt out of a
152/// check.  This trait is provided as a named capability for applications
153/// that model issuer capabilities separately.  Its signature deliberately
154/// includes the cancellation scope used
155/// by the rest of certmagic's async API.
156#[async_trait]
157pub trait PreChecker: Send + Sync {
158    /// Validate the requested names before contacting the CA.
159    async fn pre_check(
160        &self,
161        ct: &CancellationToken,
162        names: &[String],
163        interactive: bool,
164    ) -> Result<()>;
165}
166
167/// Optional certificate revocation capability.
168///
169/// Built-in issuers expose the same operation through [`Issuer::revoke`].
170/// This separate trait is useful for callers that need to advertise a
171/// revocation-only dependency without taking an entire issuer value.
172#[async_trait]
173pub trait Revoker: Send + Sync {
174    /// Revoke a certificate resource for the given reason.
175    async fn revoke(
176        &self,
177        ct: &CancellationToken,
178        resource: &CertificateResource,
179        reason: RevocationReason,
180    ) -> Result<()>;
181}
182
183/// Every [`Issuer`] automatically provides the pre-check capability.
184#[async_trait]
185impl<T: Issuer + ?Sized> PreChecker for T {
186    async fn pre_check(
187        &self,
188        ct: &CancellationToken,
189        names: &[String],
190        interactive: bool,
191    ) -> Result<()> {
192        Issuer::pre_check(self, ct, names, interactive).await
193    }
194}
195
196/// Every [`Issuer`] automatically provides the revocation capability.
197#[async_trait]
198impl<T: Issuer + ?Sized> Revoker for T {
199    async fn revoke(
200        &self,
201        ct: &CancellationToken,
202        resource: &CertificateResource,
203        reason: RevocationReason,
204    ) -> Result<()> {
205        Issuer::revoke(self, ct, resource, reason).await
206    }
207}
208
209/// A CSR together with the names it requests.
210#[derive(Debug, Clone)]
211pub struct Csr {
212    /// DER-encoded PKCS#10 request.
213    pub der: Vec<u8>,
214    /// DNS names requested.
215    pub dns_names: Vec<String>,
216    /// IP addresses requested.
217    pub ip_addresses: Vec<std::net::IpAddr>,
218}
219
220impl Csr {
221    /// The primary name (first DNS name).
222    #[must_use]
223    pub fn primary_name(&self) -> &str {
224        self.dns_names.first().map(String::as_str).unwrap_or("")
225    }
226}
227
228/// Certificate revocation reasons (RFC 5280 §5.3.1).
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
230#[repr(u8)]
231pub enum RevocationReason {
232    /// Unspecified (0).
233    #[default]
234    Unspecified = 0,
235    /// Key compromised (1).
236    KeyCompromise = 1,
237    /// CA compromised (2).
238    CaCompromise = 2,
239    /// Affiliation changed (3).
240    AffiliationChanged = 3,
241    /// Superseded (4).
242    Superseded = 4,
243    /// Cessation of operation (5).
244    CessationOfOperation = 5,
245    /// Privilege withdrawn (9).
246    PrivilegeWithdrawn = 9,
247    /// Authority compromise (10).
248    AuthorityCompromise = 10,
249}
250
251/// Recover the concrete ACME issuer behind an erased issuer handle. Returns
252/// `None` for issuers that are not an [`crate::acme::AcmeIssuer`].
253#[must_use]
254pub fn as_acme_issuer(issuer: &Arc<dyn Issuer>) -> Option<&crate::acme::AcmeIssuer> {
255    issuer.as_any().downcast_ref::<crate::acme::AcmeIssuer>()
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn review_certificate_resource_debug_redacts_secrets() {
264        let resource = CertificateResource {
265            private_key_pem: vec![197, 198, 199],
266            issuer_data: Some(serde_json::json!({"secret": "issuer-secret"})),
267            ..Default::default()
268        };
269        let debug = format!("{resource:?}");
270        assert!(!debug.contains("197"));
271        assert!(!debug.contains("issuer-secret"));
272    }
273
274    #[test]
275    fn names_key_sorts_and_truncates() {
276        let r = CertificateResource {
277            sans: vec!["b.com".into(), "a.com".into()],
278            ..Default::default()
279        };
280        assert_eq!(r.names_key(), "a.com,b.com");
281
282        let long = CertificateResource {
283            sans: vec!["x".repeat(2000)],
284            ..Default::default()
285        };
286        assert_eq!(long.names_key().len(), 1024 + "_trunc".len());
287
288        let unicode = CertificateResource {
289            sans: vec!["例".repeat(600)],
290            ..Default::default()
291        };
292        assert!(unicode.names_key().ends_with("_trunc"));
293    }
294
295    #[test]
296    fn revocation_reason_codes() {
297        assert_eq!(RevocationReason::KeyCompromise as u8, 1);
298        assert_eq!(RevocationReason::Superseded as u8, 4);
299    }
300}