Skip to main content

didbot_tls/
source.rs

1//! Which authority a zone's certificate comes from.
2//!
3//! A server always serves TLS. Where the certificate comes from is decided
4//! by the zone hostname alone, and decided here: a zone under `.localhost`
5//! has no public DNS, so no ACME challenge can complete for it and it is
6//! served from the local authority in [`crate::local`]; every other zone is
7//! served from ACME. The two never overlap, because this is the only place
8//! that chooses between them.
9
10use didbot_identity::did::is_loopback_host;
11
12/// Where a zone's certificate is issued.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CertificateSource {
15    /// A public authority, over ACME DNS-01. See [`crate::acme`].
16    Acme,
17    /// This machine's own authority, kept in the data directory. See
18    /// [`crate::local`].
19    Local,
20}
21
22impl CertificateSource {
23    /// The source `zone` is served from.
24    pub fn for_zone(zone: &str) -> Self {
25        if is_loopback_host(zone) {
26            Self::Local
27        } else {
28            Self::Acme
29        }
30    }
31
32    /// The spelling a caller passes on a command line or writes in a config
33    /// file.
34    pub fn as_str(self) -> &'static str {
35        match self {
36            Self::Acme => "acme",
37            Self::Local => "local",
38        }
39    }
40
41    /// Why `zone` cannot be served from this source, or `None` if it can.
42    ///
43    /// One sentence per direction, both naming the source the zone does
44    /// take, because a refusal that does not say what to pass instead makes
45    /// the reader guess.
46    pub fn refusal_for(self, zone: &str) -> Option<String> {
47        let allowed = Self::for_zone(zone);
48        if allowed == self {
49            return None;
50        }
51        Some(match self {
52            Self::Acme => format!(
53                "--tls acme needs a real zone; --zone {zone} is under .localhost, which no \
54                 DNS-01 challenge can complete against. Pass --tls local, which is what a \
55                 .localhost zone is served from."
56            ),
57            Self::Local => format!(
58                "--tls local serves a certificate only this machine trusts; --zone {zone} is a \
59                 real zone, so pass --tls acme and let a public authority issue for it."
60            ),
61        })
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn each_zone_has_exactly_one_source() {
71        for zone in [
72            "agents.localhost",
73            "localhost",
74            "opzone.localhost",
75            "127.0.0.1",
76        ] {
77            assert_eq!(CertificateSource::for_zone(zone), CertificateSource::Local);
78            assert!(CertificateSource::Local.refusal_for(zone).is_none());
79            assert!(CertificateSource::Acme.refusal_for(zone).is_some());
80        }
81        for zone in ["pds.did.bot", "agents.example.com", "localhost.example"] {
82            assert_eq!(CertificateSource::for_zone(zone), CertificateSource::Acme);
83            assert!(CertificateSource::Acme.refusal_for(zone).is_none());
84            assert!(CertificateSource::Local.refusal_for(zone).is_some());
85        }
86    }
87}