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