-
-
Notifications
You must be signed in to change notification settings - Fork 289
/
acmemanager.go
348 lines (308 loc) · 11 KB
/
acmemanager.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
package certmagic
import (
"context"
"crypto/x509"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/go-acme/lego/v3/acme"
"github.com/go-acme/lego/v3/certificate"
"github.com/go-acme/lego/v3/challenge"
"github.com/go-acme/lego/v3/challenge/dns01"
)
// ACMEManager gets certificates using ACME. It implements the PreChecker,
// Issuer, and Revoker interfaces.
//
// It is NOT VALID to use an ACMEManager without calling NewACMEManager().
// It fills in default values from DefaultACME as well as setting up
// internal state that is necessary for valid use. Always call
// NewACMEManager() to get a valid ACMEManager value.
type ACMEManager struct {
// The endpoint of the directory for the ACME
// CA we are to use
CA string
// TestCA is the endpoint of the directory for
// an ACME CA to use to test domain validation,
// but any certs obtained from this CA are
// discarded
TestCA string
// The email address to use when creating or
// selecting an existing ACME server account
Email string
// Set to true if agreed to the CA's
// subscriber agreement
Agreed bool
// Disable all HTTP challenges
DisableHTTPChallenge bool
// Disable all TLS-ALPN challenges
DisableTLSALPNChallenge bool
// The host (ONLY the host, not port) to listen
// on if necessary to start a listener to solve
// an ACME challenge
ListenHost string
// The alternate port to use for the ACME HTTP
// challenge; if non-empty, this port will be
// used instead of HTTPChallengePort to spin up
// a listener for the HTTP challenge
AltHTTPPort int
// The alternate port to use for the ACME
// TLS-ALPN challenge; the system must forward
// TLSALPNChallengePort to this port for
// challenge to succeed
AltTLSALPNPort int
// The DNS provider to use when solving the
// ACME DNS challenge
DNSProvider challenge.Provider
// The ChallengeOption struct to provide
// custom precheck or name resolution options
// for DNS challenge validation and execution
DNSChallengeOption dns01.ChallengeOption
// TrustedRoots specifies a pool of root CA
// certificates to trust when communicating
// over a network to a peer.
TrustedRoots *x509.CertPool
// The maximum amount of time to allow for
// obtaining a certificate. If empty, the
// default from the underlying lego lib is
// used. If set, it must not be too low so
// as to cancel orders too early, running
// the risk of rate limiting.
CertObtainTimeout time.Duration
config *Config
}
// NewACMEManager constructs a valid ACMEManager based on a template
// configuration; any empty values will be filled in by defaults in
// DefaultACME. The associated config is also required.
//
// Typically, you'll create the Config first, then call NewACMEManager(),
// then assign the return value to the Issuer/Revoker fields of the Config.
func NewACMEManager(cfg *Config, template ACMEManager) *ACMEManager {
if cfg == nil {
panic("cannot make valid ACMEManager without an associated CertMagic config")
}
if template.CA == "" {
template.CA = DefaultACME.CA
}
if template.TestCA == "" {
template.TestCA = DefaultACME.TestCA
}
if template.Email == "" {
template.Email = DefaultACME.Email
}
if !template.Agreed {
template.Agreed = DefaultACME.Agreed
}
if !template.DisableHTTPChallenge {
template.DisableHTTPChallenge = DefaultACME.DisableHTTPChallenge
}
if !template.DisableTLSALPNChallenge {
template.DisableTLSALPNChallenge = DefaultACME.DisableTLSALPNChallenge
}
if template.ListenHost == "" {
template.ListenHost = DefaultACME.ListenHost
}
if template.AltHTTPPort == 0 {
template.AltHTTPPort = DefaultACME.AltHTTPPort
}
if template.AltTLSALPNPort == 0 {
template.AltTLSALPNPort = DefaultACME.AltTLSALPNPort
}
if template.DNSProvider == nil {
template.DNSProvider = DefaultACME.DNSProvider
}
if template.DNSChallengeOption == nil {
template.DNSChallengeOption = DefaultACME.DNSChallengeOption
}
if template.TrustedRoots == nil {
template.TrustedRoots = DefaultACME.TrustedRoots
}
if template.CertObtainTimeout == 0 {
template.CertObtainTimeout = DefaultACME.CertObtainTimeout
}
template.config = cfg
return &template
}
// IssuerKey returns the unique issuer key for the
// confgured CA endpoint.
func (am *ACMEManager) IssuerKey() string {
return am.issuerKey(am.CA)
}
func (am *ACMEManager) issuerKey(ca string) string {
key := ca
if caURL, err := url.Parse(key); err == nil {
key = caURL.Host
if caURL.Path != "" {
// keep the path, but make sure it's a single
// component (i.e. no forward slashes, and for
// good measure, no backward slashes either)
const hyphen = "-"
repl := strings.NewReplacer(
"/", hyphen,
"\\", hyphen,
)
path := strings.Trim(repl.Replace(caURL.Path), hyphen)
if path != "" {
key += hyphen + path
}
}
}
return key
}
// PreCheck performs a few simple checks before obtaining or
// renewing a certificate with ACME, and returns whether this
// batch is eligible for certificates if using Let's Encrypt.
// It also ensures that an email address is available.
func (am *ACMEManager) PreCheck(names []string, interactive bool) error {
letsEncrypt := strings.Contains(am.CA, "api.letsencrypt.org")
if letsEncrypt {
for _, name := range names {
if !SubjectQualifiesForPublicCert(name) {
return fmt.Errorf("subject does not qualify for a Let's Encrypt certificate: %s", name)
}
}
}
return am.getEmail(interactive)
}
// Issue implements the Issuer interface. It obtains a certificate for the given csr using
// the ACME configuration am.
func (am *ACMEManager) Issue(ctx context.Context, csr *x509.CertificateRequest) (*IssuedCertificate, error) {
if am.config == nil {
panic("missing config pointer (must use NewACMEManager)")
}
var isRetry bool
if attempts, ok := ctx.Value(AttemptsCtxKey).(*int); ok {
isRetry = *attempts > 0
}
cert, usedTestCA, err := am.doIssue(ctx, csr, isRetry)
if err != nil {
return nil, err
}
// important to note that usedTestCA is not necessarily the same as isRetry
// (usedTestCA can be true if the main CA and the test CA happen to be the same)
if isRetry && usedTestCA && am.CA != am.TestCA {
// succeeded with testing endpoint, so try again with production endpoint
// (only if the production endpoint is different from the testing endpoint)
// TODO: This logic is imperfect and could benefit from some refinement.
// The two CA endpoints likely have different states, which could cause one
// to succeed and the other to fail, even if it's not a validation error.
// Two common cases would be:
// 1) Rate limiter state. This is more likely to cause prod to fail while
// staging succeeds, since prod usually has tighter rate limits. Thus, if
// initial attempt failed in prod due to rate limit, first retry (on staging)
// might succeed, and then trying prod again right way would probably still
// fail; normally this would terminate retries but the right thing to do in
// this case is to back off and retry again later. We could refine this logic
// to stick with the production endpoint on retries unless the error changes.
// 2) Cached authorizations state. If a domain validates successfully with
// one endpoint, but then the other endpoint is used, it might fail, e.g. if
// DNS was just changed or is still propagating. In this case, the second CA
// should continue to be retried with backoff, without switching back to the
// other endpoint. This is more likely to happen if a user is testing with
// the staging CA as the main CA, then changes their configuration once they
// think they are ready for the production endpoint.
cert, _, err = am.doIssue(ctx, csr, false)
if err != nil {
// succeeded with test CA but failed just now with the production CA;
// either we are observing differing internal states of each CA that will
// work out with time, or there is a bug/misconfiguration somewhere
// externally; it is hard to tell which! one easy cue is whether the
// error is specifically a 429 (Too Many Requests); if so, we should
// probably keep retrying
var acmeErr acme.ProblemDetails
if errors.As(err, &acmeErr) {
if acmeErr.HTTPStatus == http.StatusTooManyRequests {
// DON'T abort retries; the test CA succeeded (even
// if it's cached, it recently succeeded!) so we just
// need to keep trying (with backoff) until this CA's
// rate limits expire...
// TODO: as mentioned in comment above, we would benefit
// by pinning the main CA at this point instead of
// needlessly retrying with the test CA first each time
return nil, err
}
}
return nil, ErrNoRetry{err}
}
}
return cert, err
}
func (am *ACMEManager) doIssue(ctx context.Context, csr *x509.CertificateRequest, useTestCA bool) (*IssuedCertificate, bool, error) {
client, err := am.newACMEClientWithRetry(useTestCA)
if err != nil {
return nil, false, err
}
usingTestCA := client.usingTestCA()
nameSet := namesFromCSR(csr)
if !useTestCA {
if err := client.throttle(ctx, nameSet); err != nil {
return nil, usingTestCA, err
}
}
certRes, err := client.tryAllEnabledChallenges(ctx, csr)
if err != nil {
return nil, usingTestCA, fmt.Errorf("%v %w", nameSet, err)
}
ic := &IssuedCertificate{
Certificate: certRes.Certificate,
Metadata: certRes,
}
return ic, usingTestCA, nil
}
func (c *acmeClient) tryAllEnabledChallenges(ctx context.Context, csr *x509.CertificateRequest) (*certificate.Resource, error) {
// start with all enabled challenges
challenges := c.initialChallenges()
if len(challenges) == 0 {
return nil, fmt.Errorf("no challenge types enabled")
}
// try while a challenge type is still available
var cert *certificate.Resource
var err error
for len(challenges) > 0 {
var chosenChallenge challenge.Type
chosenChallenge, challenges = c.nextChallenge(challenges)
cert, err = c.acmeClient.Certificate.ObtainForCSR(*csr, true)
if err == nil {
return cert, nil
}
log.Printf("[ERROR] %s (challenge=%s remaining=%v)", err, chosenChallenge, challenges)
time.Sleep(2 * time.Second)
}
return cert, err
}
// Revoke implements the Revoker interface. It revokes the given certificate.
func (am *ACMEManager) Revoke(ctx context.Context, cert CertificateResource) error {
client, err := am.newACMEClient(false, false)
if err != nil {
return err
}
meta := cert.IssuerData.(map[string]interface{})
cr := certificate.Resource{
Domain: meta["domain"].(string),
CertURL: meta["certUrl"].(string),
CertStableURL: meta["certStableURL"].(string),
}
return client.revoke(ctx, cr)
}
// DefaultACME specifies the default settings
// to use for ACMEManagers.
var DefaultACME = ACMEManager{
CA: LetsEncryptProductionCA,
TestCA: LetsEncryptStagingCA,
}
// Some well-known CA endpoints available to use.
const (
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
)
// prefixACME is the storage key prefix used for ACME-specific assets.
const prefixACME = "acme"
// Interface guards
var (
_ PreChecker = (*ACMEManager)(nil)
_ Issuer = (*ACMEManager)(nil)
_ Revoker = (*ACMEManager)(nil)
)