1// Package autotls automatically configures TLS (for SMTP, IMAP, HTTP) by
2// requesting certificates with ACME, typically from Let's Encrypt.
3package autotls
4
5// We do tls-alpn-01, and also http-01. For DNS we would need a third party tool
6// with an API that can make the DNS changes, as we don't want to link in dozens of
7// bespoke API's for DNS record manipulation into mox.
8
9import (
10 "bytes"
11 "context"
12 "crypto"
13 "crypto/ecdsa"
14 "crypto/elliptic"
15 cryptorand "crypto/rand"
16 "crypto/rsa"
17 "crypto/tls"
18 "crypto/x509"
19 "encoding/pem"
20 "errors"
21 "fmt"
22 "io"
23 "log/slog"
24 "net"
25 "os"
26 "path/filepath"
27 "sort"
28 "strings"
29 "sync"
30 "time"
31
32 "golang.org/x/crypto/acme"
33
34 "github.com/prometheus/client_golang/prometheus"
35 "github.com/prometheus/client_golang/prometheus/promauto"
36
37 "github.com/mjl-/autocert"
38
39 "github.com/mjl-/mox/dns"
40 "github.com/mjl-/mox/mlog"
41 "github.com/mjl-/mox/moxvar"
42)
43
44var (
45 metricMissingServerName = promauto.NewCounter(
46 prometheus.CounterOpts{
47 Name: "mox_autotls_missing_servername_total",
48 Help: "Number of failed TLS connection attempts with missing SNI where no fallback hostname was configured.",
49 },
50 )
51 metricUnknownServerName = promauto.NewCounter(
52 prometheus.CounterOpts{
53 Name: "mox_autotls_unknown_servername_total",
54 Help: "Number of failed TLS connection attempts with an unrecognized SNI name where no fallback hostname was configured.",
55 },
56 )
57 metricCertRequestErrors = promauto.NewCounter(
58 prometheus.CounterOpts{
59 Name: "mox_autotls_cert_request_errors_total",
60 Help: "Number of errors trying to retrieve a certificate for a hostname, possibly ACME verification errors.",
61 },
62 )
63 metricCertput = promauto.NewCounter(
64 prometheus.CounterOpts{
65 Name: "mox_autotls_certput_total",
66 Help: "Number of certificate store puts.",
67 },
68 )
69)
70
71// Manager is in charge of a single ACME identity, and automatically requests
72// certificates for allowlisted hosts.
73type Manager struct {
74 ACMETLSConfig *tls.Config // For serving HTTPS on port 443, which is required for certificate requests to succeed.
75 Manager *autocert.Manager
76
77 shutdown <-chan struct{}
78
79 sync.Mutex
80 hosts map[dns.Domain]struct{}
81}
82
83// Load returns an initialized autotls manager for "name" (used for the ACME key
84// file and requested certs and their keys). All files are stored within acmeDir.
85//
86// contactEmail must be a valid email address to which notifications about ACME can
87// be sent. directoryURL is the ACME starting point.
88//
89// eabKeyID and eabKey are for external account binding when making a new account,
90// which some ACME providers require.
91//
92// getPrivateKey is called to get the private key for the host and key type. It
93// can be used to deliver a specific (e.g. always the same) private key for a
94// host, or a newly generated key.
95//
96// When shutdown is closed, no new TLS connections can be created.
97func Load(name, acmeDir, contactEmail, directoryURL string, eabKeyID string, eabKey []byte, getPrivateKey func(host string, keyType autocert.KeyType) (crypto.Signer, error), shutdown <-chan struct{}) (*Manager, error) {
98 if directoryURL == "" {
99 return nil, fmt.Errorf("empty ACME directory URL")
100 }
101 if contactEmail == "" {
102 return nil, fmt.Errorf("empty contact email")
103 }
104
105 // Load identity key if it exists. Otherwise, create a new key.
106 p := filepath.Join(acmeDir, name+".key")
107 var key crypto.Signer
108 f, err := os.Open(p)
109 if f != nil {
110 defer f.Close()
111 }
112 if err != nil && os.IsNotExist(err) {
113 key, err = ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
114 if err != nil {
115 return nil, fmt.Errorf("generating ecdsa identity key: %s", err)
116 }
117 der, err := x509.MarshalPKCS8PrivateKey(key)
118 if err != nil {
119 return nil, fmt.Errorf("marshal identity key: %s", err)
120 }
121 block := &pem.Block{
122 Type: "PRIVATE KEY",
123 Headers: map[string]string{
124 "Note": fmt.Sprintf("PEM PKCS8 ECDSA private key generated for ACME provider %s by mox", name),
125 },
126 Bytes: der,
127 }
128 b := &bytes.Buffer{}
129 if err := pem.Encode(b, block); err != nil {
130 return nil, fmt.Errorf("pem encode: %s", err)
131 } else if err := os.WriteFile(p, b.Bytes(), 0660); err != nil {
132 return nil, fmt.Errorf("writing identity key: %s", err)
133 }
134 } else if err != nil {
135 return nil, fmt.Errorf("open identity key file: %s", err)
136 } else {
137 var privKey any
138 if buf, err := io.ReadAll(f); err != nil {
139 return nil, fmt.Errorf("reading identity key: %s", err)
140 } else if p, _ := pem.Decode(buf); p == nil {
141 return nil, fmt.Errorf("no pem data")
142 } else if p.Type != "PRIVATE KEY" {
143 return nil, fmt.Errorf("got PEM block %q, expected \"PRIVATE KEY\"", p.Type)
144 } else if privKey, err = x509.ParsePKCS8PrivateKey(p.Bytes); err != nil {
145 return nil, fmt.Errorf("parsing PKCS8 private key: %s", err)
146 }
147 switch k := privKey.(type) {
148 case *ecdsa.PrivateKey:
149 key = k
150 case *rsa.PrivateKey:
151 key = k
152 default:
153 return nil, fmt.Errorf("unsupported private key type %T", key)
154 }
155 }
156
157 m := &autocert.Manager{
158 Cache: dirCache(filepath.Join(acmeDir, "keycerts", name)),
159 Prompt: autocert.AcceptTOS,
160 Email: contactEmail,
161 Client: &acme.Client{
162 DirectoryURL: directoryURL,
163 Key: key,
164 UserAgent: "mox/" + moxvar.Version,
165 },
166 GetPrivateKey: getPrivateKey,
167 // HostPolicy set below.
168 }
169 // If external account binding key is provided, use it for registering a new account.
170 // todo: ideally the key and its id are provided temporarily by the admin when registering a new account. but we don't do that interactive setup yet. in the future, an interactive setup/quickstart would ask for the key once to register a new acme account.
171 if eabKeyID != "" {
172 m.ExternalAccountBinding = &acme.ExternalAccountBinding{
173 KID: eabKeyID,
174 Key: eabKey,
175 }
176 }
177
178 a := &Manager{
179 Manager: m,
180 shutdown: shutdown,
181 hosts: map[dns.Domain]struct{}{},
182 }
183 m.HostPolicy = a.HostPolicy
184 acmeTLSConfig := *m.TLSConfig()
185 acmeTLSConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
186 return a.loggingGetCertificate(hello, dns.Domain{}, false, false)
187 }
188 a.ACMETLSConfig = &acmeTLSConfig
189 return a, nil
190}
191
192// loggingGetCertificate is a helper to implement crypto/tls.Config.GetCertificate,
193// optionally falling back to a certificate for fallbackHostname in case SNI is
194// absent or for an unknown hostname.
195func (m *Manager) loggingGetCertificate(hello *tls.ClientHelloInfo, fallbackHostname dns.Domain, fallbackNoSNI, fallbackUnknownSNI bool) (*tls.Certificate, error) {
196 log := mlog.New("autotls", nil).WithContext(hello.Context())
197
198 // If we can't find a certificate (depending on fallback parameters), we return a
199 // nil certificate and nil error, which crypto/tls turns into a TLS alert
200 // "unrecognized name", which can be interpreted by clients as a hint that they are
201 // using the wrong hostname, or a certificate is missing.
202
203 if hello.ServerName == "" && fallbackNoSNI {
204 hello.ServerName = fallbackHostname.ASCII
205 }
206
207 // Handle missing SNI to prevent logging an error below.
208 if hello.ServerName == "" {
209 metricMissingServerName.Inc()
210 log.Debug("tls request without sni servername, rejecting", slog.Any("localaddr", hello.Conn.LocalAddr()), slog.Any("supportedprotos", hello.SupportedProtos))
211 return nil, nil
212 }
213
214 cert, err := m.Manager.GetCertificate(hello)
215 if err != nil && errors.Is(err, errHostNotAllowed) {
216 if !fallbackUnknownSNI {
217 metricUnknownServerName.Inc()
218 log.Debugx("requesting certificate", err, slog.String("host", hello.ServerName))
219 return nil, nil
220 }
221
222 log.Debug("certificate for unknown hostname, using fallback hostname", slog.String("host", hello.ServerName))
223 hello.ServerName = fallbackHostname.ASCII
224 cert, err = m.Manager.GetCertificate(hello)
225 if err != nil {
226 metricCertRequestErrors.Inc()
227 log.Errorx("requesting certificate for fallback hostname", err, slog.String("host", hello.ServerName))
228 } else {
229 log.Debug("using certificate for fallback hostname", slog.String("host", hello.ServerName))
230 }
231 return cert, err
232 } else if err != nil {
233 metricCertRequestErrors.Inc()
234 log.Errorx("requesting certificate", err, slog.String("host", hello.ServerName))
235 }
236 return cert, err
237}
238
239// TLSConfig returns a TLS server config that optionally returns a certificate for
240// fallbackHostname if no SNI was done, or for an unknown hostname.
241//
242// If fallbackNoSNI is set, TLS connections without SNI will use a certificate for
243// fallbackHostname. Otherwise, connections without SNI will fail with a message
244// that no TLS certificate is available.
245//
246// If fallbackUnknownSNI is set, TLS connections with an SNI hostname that is not
247// allowlisted will instead use a certificate for fallbackHostname. Otherwise, such
248// TLS connections will fail.
249func (m *Manager) TLSConfig(fallbackHostname dns.Domain, fallbackNoSNI, fallbackUnknownSNI bool) *tls.Config {
250 return &tls.Config{
251 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
252 return m.loggingGetCertificate(hello, fallbackHostname, fallbackNoSNI, fallbackUnknownSNI)
253 },
254 }
255}
256
257// CertAvailable checks whether a non-expired ECDSA certificate is available in the
258// cache for host. No other checks than expiration are done.
259func (m *Manager) CertAvailable(ctx context.Context, log mlog.Log, host dns.Domain) (bool, error) {
260 ck := host.ASCII // Would be "+rsa" for rsa keys.
261 data, err := m.Manager.Cache.Get(ctx, ck)
262 if err != nil && errors.Is(err, autocert.ErrCacheMiss) {
263 return false, nil
264 } else if err != nil {
265 return false, fmt.Errorf("attempt to get certificate from cache: %v", err)
266 }
267
268 // The cached keycert is of the form: private key, leaf certificate, intermediate certificates...
269 privb, rem := pem.Decode(data)
270 if privb == nil {
271 return false, fmt.Errorf("missing private key in cached keycert file")
272 }
273 pubb, _ := pem.Decode(rem)
274 if pubb == nil {
275 return false, fmt.Errorf("missing certificate in cached keycert file")
276 } else if pubb.Type != "CERTIFICATE" {
277 return false, fmt.Errorf("second pem block is %q, expected CERTIFICATE", pubb.Type)
278 }
279 cert, err := x509.ParseCertificate(pubb.Bytes)
280 if err != nil {
281 return false, fmt.Errorf("parsing certificate from cached keycert file: %v", err)
282 }
283 // We assume the certificate has a matching hostname, and is properly CA-signed. We
284 // only check the expiration time.
285 if time.Until(cert.NotBefore) > 0 || time.Since(cert.NotAfter) > 0 {
286 return false, nil
287 }
288 return true, nil
289}
290
291// SetAllowedHostnames sets a new list of allowed hostnames for automatic TLS.
292// After setting the host names, a goroutine is start to check that new host names
293// are fully served by publicIPs (only if non-empty and there is no unspecified
294// address in the list). If no, log an error with a warning that ACME validation
295// may fail.
296func (m *Manager) SetAllowedHostnames(log mlog.Log, resolver dns.Resolver, hostnames map[dns.Domain]struct{}, publicIPs []string, checkHosts bool) {
297 m.Lock()
298 defer m.Unlock()
299
300 // Log as slice, sorted.
301 l := make([]dns.Domain, 0, len(hostnames))
302 for d := range hostnames {
303 l = append(l, d)
304 }
305 sort.Slice(l, func(i, j int) bool {
306 return l[i].Name() < l[j].Name()
307 })
308
309 log.Debug("autotls setting allowed hostnames", slog.Any("hostnames", l), slog.Any("publicips", publicIPs))
310 var added []dns.Domain
311 for h := range hostnames {
312 if _, ok := m.hosts[h]; !ok {
313 added = append(added, h)
314 }
315 }
316 m.hosts = hostnames
317
318 if checkHosts && len(added) > 0 && len(publicIPs) > 0 {
319 for _, ip := range publicIPs {
320 if net.ParseIP(ip).IsUnspecified() {
321 return
322 }
323 }
324 go func() {
325 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
326 defer cancel()
327
328 publicIPstrs := map[string]struct{}{}
329 for _, ip := range publicIPs {
330 publicIPstrs[ip] = struct{}{}
331 }
332
333 log.Debug("checking ips of hosts configured for acme tls cert validation")
334 for _, h := range added {
335 ips, _, err := resolver.LookupIP(ctx, "ip", h.ASCII+".")
336 if err != nil {
337 log.Warnx("acme tls cert validation for host may fail due to dns lookup error", err, slog.Any("host", h))
338 continue
339 }
340 for _, ip := range ips {
341 if _, ok := publicIPstrs[ip.String()]; !ok {
342 log.Warn("acme tls cert validation for host is likely to fail because not all its ips are being listened on",
343 slog.Any("hostname", h),
344 slog.Any("listenedips", publicIPs),
345 slog.Any("hostips", ips),
346 slog.Any("missingip", ip))
347 }
348 }
349 }
350 }()
351 }
352}
353
354// Hostnames returns the allowed host names for use with ACME.
355func (m *Manager) Hostnames() []dns.Domain {
356 m.Lock()
357 defer m.Unlock()
358 var l []dns.Domain
359 for h := range m.hosts {
360 l = append(l, h)
361 }
362 return l
363}
364
365var errHostNotAllowed = errors.New("autotls: host not in allowlist")
366
367// HostPolicy decides if a host is allowed for use with ACME, i.e. whether a
368// certificate will be returned if present and/or will be requested if not yet
369// present. Only hosts added with SetAllowedHostnames are allowed. During shutdown,
370// no new connections are allowed.
371func (m *Manager) HostPolicy(ctx context.Context, host string) (rerr error) {
372 log := mlog.New("autotls", nil).WithContext(ctx)
373 defer func() {
374 log.Debugx("autotls hostpolicy result", rerr, slog.String("host", host))
375 }()
376
377 // Don't request new TLS certs when we are shutting down.
378 select {
379 case <-m.shutdown:
380 return fmt.Errorf("shutting down")
381 default:
382 }
383
384 xhost, _, err := net.SplitHostPort(host)
385 if err == nil {
386 // For http-01, host may include a port number.
387 host = xhost
388 }
389
390 d, err := dns.ParseDomain(host)
391 if err != nil {
392 return fmt.Errorf("invalid host: %v", err)
393 }
394
395 m.Lock()
396 defer m.Unlock()
397 if _, ok := m.hosts[d]; !ok {
398 return fmt.Errorf("%w: %q", errHostNotAllowed, d)
399 }
400 return nil
401}
402
403type dirCache autocert.DirCache
404
405func (d dirCache) Delete(ctx context.Context, name string) (rerr error) {
406 log := mlog.New("autotls", nil).WithContext(ctx)
407 defer func() {
408 log.Debugx("dircache delete result", rerr, slog.String("name", name))
409 }()
410 err := autocert.DirCache(d).Delete(ctx, name)
411 if err != nil {
412 log.Errorx("deleting cert from dir cache", err, slog.String("name", name))
413 } else if !strings.HasSuffix(name, "+token") {
414 log.Info("autotls cert delete", slog.String("name", name))
415 }
416 return err
417}
418
419func (d dirCache) Get(ctx context.Context, name string) (rbuf []byte, rerr error) {
420 log := mlog.New("autotls", nil).WithContext(ctx)
421 defer func() {
422 log.Debugx("dircache get result", rerr, slog.String("name", name))
423 }()
424 buf, err := autocert.DirCache(d).Get(ctx, name)
425 if err != nil && errors.Is(err, autocert.ErrCacheMiss) {
426 log.Infox("getting cert from dir cache", err, slog.String("name", name))
427 } else if err != nil {
428 log.Errorx("getting cert from dir cache", err, slog.String("name", name))
429 } else if !strings.HasSuffix(name, "+token") {
430 log.Debug("autotls cert get", slog.String("name", name))
431 }
432 return buf, err
433}
434
435func (d dirCache) Put(ctx context.Context, name string, data []byte) (rerr error) {
436 log := mlog.New("autotls", nil).WithContext(ctx)
437 defer func() {
438 log.Debugx("dircache put result", rerr, slog.String("name", name))
439 }()
440 metricCertput.Inc()
441 err := autocert.DirCache(d).Put(ctx, name, data)
442 if err != nil {
443 log.Errorx("storing cert in dir cache", err, slog.String("name", name))
444 } else if !strings.HasSuffix(name, "+token") {
445 log.Info("autotls cert store", slog.String("name", name))
446 }
447 return err
448}
449