Skip to main content

dinio_sdk/models/
certificate.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::error::{Error, Result};
8
9/// A TLS certificate Relay serves for the names in it, picked by SNI (exact name, then a
10/// one-label wildcard). Core never returns the private key; the details are read from the
11/// stored chain.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Certificate {
14    /// Certificate id; stays the same across renewals.
15    pub uuid: Uuid,
16    /// The zone it belongs to. `None` for a platform certificate (Admin only).
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub zone_id: Option<Uuid>,
19    /// Platform default for names no other certificate covers. Only platform certificates
20    /// can be the default.
21    pub is_default: bool,
22    /// `Manual` or `Acme` (kept as a string for forward compatibility); see
23    /// [`CertificateSource`] and [`Certificate::source_kind`].
24    pub source: String,
25    /// SAN DNS names, lowercase; wildcards as `*.example.com`.
26    pub names: Vec<String>,
27    /// Subject of the leaf certificate.
28    pub subject: String,
29    /// Issuer of the leaf certificate.
30    pub issuer: String,
31    /// Serial number of the leaf certificate.
32    pub serial: String,
33    /// SHA-256 fingerprint of the leaf certificate.
34    pub fingerprint_sha256: String,
35    /// Unix seconds.
36    pub valid_from: i64,
37    /// Unix seconds.
38    pub valid_to: i64,
39    /// Whether `valid_to` has passed (at the time Core answered).
40    pub expired: bool,
41    /// Key algorithm and size, e.g. `EC P-256`, `RSA 2048`, `Ed25519`.
42    pub key_type: String,
43    /// Number of certificates in the chain (leaf + intermediates).
44    pub chain_length: u32,
45    /// Unix seconds.
46    pub created_at: i64,
47    /// Unix seconds.
48    pub updated_at: i64,
49}
50
51impl Certificate {
52    /// `source` in its typed form, if this SDK version knows it.
53    pub fn source_kind(&self) -> Option<CertificateSource> {
54        self.source.parse().ok()
55    }
56}
57
58/// Where a certificate comes from (`Certificate.source`).
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub enum CertificateSource {
61    /// Uploaded by a user.
62    Manual,
63    /// Issued by Core for a redirect with `tls` = `Auto`. Read-only: replacing or deleting
64    /// it answers 409 `MANAGED_CERTIFICATE`.
65    Acme,
66}
67
68impl FromStr for CertificateSource {
69    type Err = Error;
70
71    /// Case-insensitive wire label (`manual`, `Acme`).
72    fn from_str(text: &str) -> Result<Self> {
73        match text.trim().to_ascii_lowercase().as_str() {
74            "manual" => Ok(Self::Manual),
75            "acme" => Ok(Self::Acme),
76            _ => Err(Error::InvalidInput(format!(
77                "unknown certificate source {text:?} (Manual or Acme)"
78            ))),
79        }
80    }
81}
82
83/// `POST /certificates` body for a zone certificate. `Debug` does not show the key.
84#[derive(Clone, Serialize)]
85pub struct NewCertificate {
86    /// PEM chain, leaf first, up to 64 KiB.
87    pub chain_pem: String,
88    /// PEM private key of the leaf, up to 16 KiB. Core stores it as PKCS#8.
89    pub key_pem: String,
90    /// The zone; every SAN name must be served by it.
91    pub zone_id: Uuid,
92}
93
94impl fmt::Debug for NewCertificate {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.debug_struct("NewCertificate")
97            .field(
98                "chain_pem",
99                &format_args!("<{} bytes>", self.chain_pem.len()),
100            )
101            .field("key_pem", &format_args!("<redacted>"))
102            .field("zone_id", &self.zone_id)
103            .finish()
104    }
105}
106
107/// `PUT /certificates/:uuid` body: a renewal (new chain and key under the same id).
108/// `Debug` does not show the key.
109#[derive(Clone, Serialize)]
110pub struct CertificateRenewal {
111    /// PEM chain, leaf first, up to 64 KiB.
112    pub chain_pem: String,
113    /// PEM private key of the leaf, up to 16 KiB.
114    pub key_pem: String,
115}
116
117impl fmt::Debug for CertificateRenewal {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.debug_struct("CertificateRenewal")
120            .field(
121                "chain_pem",
122                &format_args!("<{} bytes>", self.chain_pem.len()),
123            )
124            .field("key_pem", &format_args!("<redacted>"))
125            .finish()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn debug_redacts_key() {
135        let new = NewCertificate {
136            chain_pem: "chain".into(),
137            key_pem: "very-secret-key".into(),
138            zone_id: Uuid::nil(),
139        };
140        assert!(!format!("{new:?}").contains("very-secret-key"));
141        let renewal = CertificateRenewal {
142            chain_pem: "chain".into(),
143            key_pem: "very-secret-key".into(),
144        };
145        assert!(!format!("{renewal:?}").contains("very-secret-key"));
146    }
147
148    #[test]
149    fn certificate_deserializes() {
150        let cert: Certificate = serde_json::from_value(serde_json::json!({
151            "uuid": "00000000-0000-0000-0000-000000000001",
152            "zone_id": "00000000-0000-0000-0000-000000000002",
153            "is_default": false, "source": "Acme",
154            "names": ["go.example.com"], "subject": "CN=go.example.com", "issuer": "CN=R11",
155            "serial": "01", "fingerprint_sha256": "AB:CD", "valid_from": 1, "valid_to": 2,
156            "expired": false, "key_type": "EC P-256", "chain_length": 2,
157            "created_at": 1, "updated_at": 1
158        }))
159        .unwrap();
160        assert_eq!(cert.source_kind(), Some(CertificateSource::Acme));
161    }
162}