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 // Handle missing SNI to prevent logging an error below.
165 // At startup, during config initialization, we already adjust the tls config to
166 // inject the listener hostname if there isn't one in the TLS client hello. This is
167 // common for SMTP STARTTLS connections, which often do not care about the
168 // verification of the certificate.
169 if hello.ServerName == "" {
170 log.Debug("tls request without sni servername, rejecting", slog.Any("localaddr", hello.Conn.LocalAddr()), slog.Any("supportedprotos", hello.SupportedProtos))
171 return nil, fmt.Errorf("sni server name required")
172 }
173
174 cert, err := m.GetCertificate(hello)
175 if err != nil {
176 if errors.Is(err, errHostNotAllowed) {
177 log.Debugx("requesting certificate", err, slog.String("host", hello.ServerName))
178 } else {
179 log.Errorx("requesting certificate", err, slog.String("host", hello.ServerName))
180 }
181 }
182 return cert, err
183 }
184
185 acmeTLSConfig := *m.TLSConfig()
186 acmeTLSConfig.GetCertificate = loggingGetCertificate
187
188 tlsConfig := tls.Config{
189 GetCertificate: loggingGetCertificate,
190 }
191
192 a := &Manager{
193 ACMETLSConfig: &acmeTLSConfig,
194 TLSConfig: &tlsConfig,
195 Manager: m,
196 shutdown: shutdown,
197 hosts: map[dns.Domain]struct{}{},
198 }
199 m.HostPolicy = a.HostPolicy
200 return a, nil
201}
202
203// CertAvailable checks whether a non-expired ECDSA certificate is available in the
204// cache for host. No other checks than expiration are done.
205func (m *Manager) CertAvailable(ctx context.Context, log mlog.Log, host dns.Domain) (bool, error) {
206 ck := host.ASCII // Would be "+rsa" for rsa keys.
207 data, err := m.Manager.Cache.Get(ctx, ck)
208 if err != nil && errors.Is(err, autocert.ErrCacheMiss) {
209 return false, nil
210 } else if err != nil {
211 return false, fmt.Errorf("attempt to get certificate from cache: %v", err)
212 }
213
214 // The cached keycert is of the form: private key, leaf certificate, intermediate certificates...
215 privb, rem := pem.Decode(data)
216 if privb == nil {
217 return false, fmt.Errorf("missing private key in cached keycert file")
218 }
219 pubb, _ := pem.Decode(rem)
220 if pubb == nil {
221 return false, fmt.Errorf("missing certificate in cached keycert file")
222 } else if pubb.Type != "CERTIFICATE" {
223 return false, fmt.Errorf("second pem block is %q, expected CERTIFICATE", pubb.Type)
224 }
225 cert, err := x509.ParseCertificate(pubb.Bytes)
226 if err != nil {
227 return false, fmt.Errorf("parsing certificate from cached keycert file: %v", err)
228 }
229 // We assume the certificate has a matching hostname, and is properly CA-signed. We
230 // only check the expiration time.
231 if time.Until(cert.NotBefore) > 0 || time.Since(cert.NotAfter) > 0 {
232 return false, nil
233 }
234 return true, nil
235}
236
237// SetAllowedHostnames sets a new list of allowed hostnames for automatic TLS.
238// After setting the host names, a goroutine is start to check that new host names
239// are fully served by publicIPs (only if non-empty and there is no unspecified
240// address in the list). If no, log an error with a warning that ACME validation
241// may fail.
242func (m *Manager) SetAllowedHostnames(log mlog.Log, resolver dns.Resolver, hostnames map[dns.Domain]struct{}, publicIPs []string, checkHosts bool) {
243 m.Lock()
244 defer m.Unlock()
245
246 // Log as slice, sorted.
247 l := make([]dns.Domain, 0, len(hostnames))
248 for d := range hostnames {
249 l = append(l, d)
250 }
251 sort.Slice(l, func(i, j int) bool {
252 return l[i].Name() < l[j].Name()
253 })
254
255 log.Debug("autotls setting allowed hostnames", slog.Any("hostnames", l), slog.Any("publicips", publicIPs))
256 var added []dns.Domain
257 for h := range hostnames {
258 if _, ok := m.hosts[h]; !ok {
259 added = append(added, h)
260 }
261 }
262 m.hosts = hostnames
263
264 if checkHosts && len(added) > 0 && len(publicIPs) > 0 {
265 for _, ip := range publicIPs {
266 if net.ParseIP(ip).IsUnspecified() {
267 return
268 }
269 }
270 go func() {
271 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
272 defer cancel()
273
274 publicIPstrs := map[string]struct{}{}
275 for _, ip := range publicIPs {
276 publicIPstrs[ip] = struct{}{}
277 }
278
279 log.Debug("checking ips of hosts configured for acme tls cert validation")
280 for _, h := range added {
281 ips, _, err := resolver.LookupIP(ctx, "ip", h.ASCII+".")
282 if err != nil {
283 log.Errorx("warning: acme tls cert validation for host may fail due to dns lookup error", err, slog.Any("host", h))
284 continue
285 }
286 for _, ip := range ips {
287 if _, ok := publicIPstrs[ip.String()]; !ok {
288 log.Error("warning: acme tls cert validation for host is likely to fail because not all its ips are being listened on",
289 slog.Any("hostname", h),
290 slog.Any("listenedips", publicIPs),
291 slog.Any("hostips", ips),
292 slog.Any("missingip", ip))
293 }
294 }
295 }
296 }()
297 }
298}
299
300// Hostnames returns the allowed host names for use with ACME.
301func (m *Manager) Hostnames() []dns.Domain {
302 m.Lock()
303 defer m.Unlock()
304 var l []dns.Domain
305 for h := range m.hosts {
306 l = append(l, h)
307 }
308 return l
309}
310
311var errHostNotAllowed = errors.New("autotls: host not in allowlist")
312
313// HostPolicy decides if a host is allowed for use with ACME, i.e. whether a
314// certificate will be returned if present and/or will be requested if not yet
315// present. Only hosts added with SetAllowedHostnames are allowed. During shutdown,
316// no new connections are allowed.
317func (m *Manager) HostPolicy(ctx context.Context, host string) (rerr error) {
318 log := mlog.New("autotls", nil).WithContext(ctx)
319 defer func() {
320 log.Debugx("autotls hostpolicy result", rerr, slog.String("host", host))
321 }()
322
323 // Don't request new TLS certs when we are shutting down.
324 select {
325 case <-m.shutdown:
326 return fmt.Errorf("shutting down")
327 default:
328 }
329
330 xhost, _, err := net.SplitHostPort(host)
331 if err == nil {
332 // For http-01, host may include a port number.
333 host = xhost
334 }
335
336 d, err := dns.ParseDomain(host)
337 if err != nil {
338 return fmt.Errorf("invalid host: %v", err)
339 }
340
341 m.Lock()
342 defer m.Unlock()
343 if _, ok := m.hosts[d]; !ok {
344 return fmt.Errorf("%w: %q", errHostNotAllowed, d)
345 }
346 return nil
347}
348
349type dirCache autocert.DirCache
350
351func (d dirCache) Delete(ctx context.Context, name string) (rerr error) {
352 log := mlog.New("autotls", nil).WithContext(ctx)
353 defer func() {
354 log.Debugx("dircache delete result", rerr, slog.String("name", name))
355 }()
356 err := autocert.DirCache(d).Delete(ctx, name)
357 if err != nil {
358 log.Errorx("deleting cert from dir cache", err, slog.String("name", name))
359 } else if !strings.HasSuffix(name, "+token") {
360 log.Info("autotls cert delete", slog.String("name", name))
361 }
362 return err
363}
364
365func (d dirCache) Get(ctx context.Context, name string) (rbuf []byte, rerr error) {
366 log := mlog.New("autotls", nil).WithContext(ctx)
367 defer func() {
368 log.Debugx("dircache get result", rerr, slog.String("name", name))
369 }()
370 buf, err := autocert.DirCache(d).Get(ctx, name)
371 if err != nil && errors.Is(err, autocert.ErrCacheMiss) {
372 log.Infox("getting cert from dir cache", err, slog.String("name", name))
373 } else if err != nil {
374 log.Errorx("getting cert from dir cache", err, slog.String("name", name))
375 } else if !strings.HasSuffix(name, "+token") {
376 log.Debug("autotls cert get", slog.String("name", name))
377 }
378 return buf, err
379}
380
381func (d dirCache) Put(ctx context.Context, name string, data []byte) (rerr error) {
382 log := mlog.New("autotls", nil).WithContext(ctx)
383 defer func() {
384 log.Debugx("dircache put result", rerr, slog.String("name", name))
385 }()
386 metricCertput.Inc()
387 err := autocert.DirCache(d).Put(ctx, name, data)
388 if err != nil {
389 log.Errorx("storing cert in dir cache", err, slog.String("name", name))
390 } else if !strings.HasSuffix(name, "+token") {
391 log.Info("autotls cert store", slog.String("name", name))
392 }
393 return err
394}
395