Skip to content
File

Blob: acme/manager.go

go194 lines
1package acme
2 
3import (
4 "context"
5 "crypto/tls"
6 "crypto/x509"
7 "fmt"
8 "slices"
9 "sync/atomic"
10 "time"
11 
12 "go.miragespace.co/specter/spec/chord"
13 "go.miragespace.co/specter/spec/cipher"
14 "go.miragespace.co/specter/spec/tun"
15 
16 "github.com/caddyserver/certmagic"
17 "github.com/mholt/acmez/v3"
18 "go.uber.org/zap"
19)
20 
21var (
22 ErrInvalid = fmt.Errorf("acme: invalid hostname")
23)
24 
25var _ cipher.CertProvider = (*Manager)(nil)
26 
27type ManagerConfig struct {
28 Logger *zap.Logger
29 KV chord.KV
30 DNSSolver acmez.Solver
31 ManagedDomains []string
32 CA string
33 Email string
34 
35 // used when testing
36 testTrustedRoots *x509.CertPool
37}
38 
39type Manager struct {
40 managedConfig *certmagic.Config
41 dynamicConfig *certmagic.Config
42 managed []string
43 chordStorage *ChordStorage
44 onHandshake atomic.Pointer[cipher.OnHandshakeFunc]
45 ManagerConfig
46}
47 
48func NewManager(cfg ManagerConfig) (*Manager, error) {
49 kvStore, err := NewChordStorage(
50 cfg.Logger.With(zap.String("component", "acme_storage")),
51 cfg.KV,
52 StorageConfig{
53 RetryInterval: time.Second * 3,
54 LeaseTTL: time.Minute,
55 })
56 if err != nil {
57 return nil, err
58 }
59 
60 isDev := cfg.CA != certmagic.LetsEncryptProductionCA
61 manager := &Manager{
62 chordStorage: kvStore,
63 ManagerConfig: cfg,
64 }
65 
66 manager.OnHandshake(noopHandshakFunc)
67 
68 managedConfig := certmagic.Config{
69 Storage: kvStore,
70 DefaultServerName: cfg.ManagedDomains[0],
71 Logger: cfg.Logger.With(zap.String("component", "acme_managed")),
72 }
73 managedIssuer := certmagic.NewACMEIssuer(&managedConfig, certmagic.ACMEIssuer{
74 CA: cfg.CA,
75 Email: cfg.Email,
76 Agreed: true,
77 Logger: cfg.Logger.With(zap.String("component", "acme_managed_issuer")),
78 DNS01Solver: cfg.DNSSolver,
79 DisableHTTPChallenge: true,
80 DisableTLSALPNChallenge: true,
81 TrustedRoots: cfg.testTrustedRoots,
82 })
83 managedConfig.Issuers = []certmagic.Issuer{managedIssuer}
84 
85 dynamicConfig := certmagic.Config{
86 Storage: kvStore,
87 DefaultServerName: cfg.ManagedDomains[0],
88 Logger: cfg.Logger.With(zap.String("component", "acme_dynamic")),
89 OnDemand: &certmagic.OnDemandConfig{
90 DecisionFunc: manager.check,
91 },
92 }
93 dynamicIssuer := certmagic.NewACMEIssuer(&dynamicConfig, certmagic.ACMEIssuer{
94 CA: cfg.CA,
95 Email: cfg.Email,
96 Agreed: true,
97 Logger: cfg.Logger.With(zap.String("component", "acme_dynamic_issuer")),
98 DNS01Solver: cfg.DNSSolver,
99 DisableHTTPChallenge: true,
100 DisableTLSALPNChallenge: true,
101 TrustedRoots: cfg.testTrustedRoots,
102 })
103 dynamicConfig.Issuers = []certmagic.Issuer{dynamicIssuer}
104 
105 if isDev {
106 managedConfig.OCSP = certmagic.OCSPConfig{
107 DisableStapling: true,
108 }
109 dynamicConfig.OCSP = certmagic.OCSPConfig{
110 DisableStapling: true,
111 }
112 }
113 
114 cache := certmagic.NewCache(certmagic.CacheOptions{
115 Logger: cfg.Logger.With(zap.String("component", "acme_cache")),
116 GetConfigForCert: manager.getConfig,
117 })
118 
119 manager.managedConfig = certmagic.New(cache, managedConfig)
120 manager.dynamicConfig = certmagic.New(cache, dynamicConfig)
121 manager.managed = make([]string, len(cfg.ManagedDomains))
122 copy(manager.managed, cfg.ManagedDomains)
123 for _, d := range cfg.ManagedDomains {
124 manager.managed = append(manager.managed, "*."+d)
125 }
126 
127 return manager, nil
128}
129 
130func (m *Manager) check(ctx context.Context, name string) error {
131 m.dynamicConfig.Logger.Debug("Dynamic certificate request", zap.String("name", name))
132 
133 callCtx, cancel := context.WithTimeout(ctx, time.Second*5)
134 defer cancel()
135 
136 _, err := tun.FindCustomHostname(callCtx, m.KV, name)
137 
138 return err
139}
140 
141func (m *Manager) isManaged(subject string) bool {
142 for _, d := range m.managed {
143 if certmagic.MatchWildcard(subject, d) {
144 return true
145 }
146 }
147 return false
148}
149 
150func (m *Manager) getConfig(c certmagic.Certificate) (*certmagic.Config, error) {
151 if slices.ContainsFunc(c.Names, m.isManaged) {
152 return m.managedConfig, nil
153 }
154 return m.dynamicConfig, nil
155}
156 
157func (m *Manager) GetCertificate(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
158 if chi.Context() != nil {
159 return m.GetCertificateWithContext(chi.Context(), chi)
160 }
161 return m.GetCertificateWithContext(context.Background(), chi)
162}
163 
164func (m *Manager) GetCertificateWithContext(ctx context.Context, chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
165 sni := chi.ServerName
166 if sni == "" {
167 return nil, ErrInvalid
168 }
169 
170 acmeHostname.Add(sni, 1)
171 
172 onHandshake := m.onHandshake.Load()
173 (*onHandshake)(sni)
174 
175 if m.isManaged(sni) {
176 return m.managedConfig.GetCertificateWithContext(ctx, chi)
177 } else {
178 return m.dynamicConfig.GetCertificateWithContext(ctx, chi)
179 }
180}
181 
182func (m *Manager) Initialize(ctx context.Context) error {
183 return m.managedConfig.ManageAsync(ctx, m.managed)
184}
185 
186func (m *Manager) OnHandshake(fn cipher.OnHandshakeFunc) {
187 if fn == nil {
188 return
189 }
190 m.onHandshake.Store(&fn)
191}
192 
193func noopHandshakFunc(_ string) {}