@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 779 lines 24.4 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddytls
16
17 import (
18 "context"
19 "crypto/x509"
20 "encoding/json"
21 "fmt"
22 "net/http"
23 "net/url"
24 "os"
25 "strconv"
26 "strings"
27 "time"
28
29 "github.com/caddyserver/certmagic"
30 "github.com/caddyserver/zerossl"
31 "github.com/mholt/acmez/v3/acme"
32 "go.uber.org/zap"
33 "go.uber.org/zap/zapcore"
34
35 "github.com/caddyserver/caddy/v2"
36 "github.com/caddyserver/caddy/v2/caddyconfig"
37 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
38 )
39
40 func init() {
41 caddy.RegisterModule(ACMEIssuer{})
42 }
43
44 // ACMEIssuer manages certificates using the ACME protocol (RFC 8555).
45 type ACMEIssuer struct {
46 // The URL to the CA's ACME directory endpoint. Default:
47 // https://acme-v02.api.letsencrypt.org/directory
48 CA string `json:"ca,omitempty"`
49
50 // The URL to the test CA's ACME directory endpoint.
51 // This endpoint is only used during retries if there
52 // is a failure using the primary CA. Default:
53 // https://acme-staging-v02.api.letsencrypt.org/directory
54 TestCA string `json:"test_ca,omitempty"`
55
56 // Your email address, so the CA can contact you if necessary.
57 // Not required, but strongly recommended to provide one so
58 // you can be reached if there is a problem. Your email is
59 // not sent to any Caddy mothership or used for any purpose
60 // other than ACME transactions.
61 Email string `json:"email,omitempty"`
62
63 // Optionally select an ACME profile to use for certificate
64 // orders. Must be a profile name offered by the ACME server,
65 // which are listed at its directory endpoint.
66 //
67 // EXPERIMENTAL: Subject to change.
68 // See https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/
69 Profile string `json:"profile,omitempty"`
70
71 // If you have an existing account with the ACME server, put
72 // the private key here in PEM format. The ACME client will
73 // look up your account information with this key first before
74 // trying to create a new one. You can use placeholders here,
75 // for example if you have it in an environment variable.
76 AccountKey string `json:"account_key,omitempty"`
77
78 // If using an ACME CA that requires an external account
79 // binding, specify the CA-provided credentials here.
80 ExternalAccount *acme.EAB `json:"external_account,omitempty"`
81
82 // Time to wait before timing out an ACME operation.
83 // Default: 0 (no timeout)
84 ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"`
85
86 // Configures the various ACME challenge types.
87 Challenges *ChallengesConfig `json:"challenges,omitempty"`
88
89 // An array of files of CA certificates to accept when connecting to the
90 // ACME CA. Generally, you should only use this if the ACME CA endpoint
91 // is internal or for development/testing purposes.
92 TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"`
93
94 // Preferences for selecting alternate certificate chains, if offered
95 // by the CA. By default, the first offered chain will be selected.
96 // If configured, the chains may be sorted and the first matching chain
97 // will be selected.
98 PreferredChains *ChainPreference `json:"preferred_chains,omitempty"`
99
100 // The validity period to ask the CA to issue a certificate for.
101 // Default: 0 (CA chooses lifetime).
102 // This value is used to compute the "notAfter" field of the ACME order;
103 // therefore the system must have a reasonably synchronized clock.
104 // NOTE: Not all CAs support this. Check with your CA's ACME
105 // documentation to see if this is allowed and what values may
106 // be used. EXPERIMENTAL: Subject to change.
107 CertificateLifetime caddy.Duration `json:"certificate_lifetime,omitempty"`
108
109 // Forward proxy module
110 NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"`
111
112 rootPool *x509.CertPool
113 logger *zap.Logger
114 httpProxy func(*http.Request) (*url.URL, error)
115
116 template certmagic.ACMEIssuer // set at Provision
117 magic *certmagic.Config // set at PreCheck
118 issuer *certmagic.ACMEIssuer // set at PreCheck; result of template + magic
119 }
120
121 // CaddyModule returns the Caddy module information.
122 func (ACMEIssuer) CaddyModule() caddy.ModuleInfo {
123 return caddy.ModuleInfo{
124 ID: "tls.issuance.acme",
125 New: func() caddy.Module { return new(ACMEIssuer) },
126 }
127 }
128
129 // Provision sets up iss.
130 func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
131 iss.logger = ctx.Logger()
132
133 repl := caddy.NewReplacer()
134
135 // expand email address, if non-empty
136 if iss.Email != "" {
137 email, err := repl.ReplaceOrErr(iss.Email, true, true)
138 if err != nil {
139 return fmt.Errorf("expanding email address '%s': %v", iss.Email, err)
140 }
141 iss.Email = email
142 }
143
144 // expand CA endpoint, if non-empty
145 if iss.CA != "" {
146 ca, err := repl.ReplaceOrErr(iss.CA, true, true)
147 if err != nil {
148 return fmt.Errorf("expanding CA endpoint '%s': %v", iss.CA, err)
149 }
150 iss.CA = ca
151 }
152
153 // expand TestCA endpoint, if non-empty
154 if iss.TestCA != "" {
155 testca, err := repl.ReplaceOrErr(iss.TestCA, true, true)
156 if err != nil {
157 return fmt.Errorf("expanding TestCA endpoint '%s': %v", iss.TestCA, err)
158 }
159 iss.TestCA = testca
160 }
161
162 // expand EAB credentials, if non-empty
163 if iss.ExternalAccount != nil {
164 if iss.ExternalAccount.KeyID != "" {
165 keyID, err := repl.ReplaceOrErr(iss.ExternalAccount.KeyID, true, true)
166 if err != nil {
167 return fmt.Errorf("expanding EAB key ID '%s': %v", iss.ExternalAccount.KeyID, err)
168 }
169 iss.ExternalAccount.KeyID = keyID
170 }
171 if iss.ExternalAccount.MACKey != "" {
172 macKey, err := repl.ReplaceOrErr(iss.ExternalAccount.MACKey, true, true)
173 if err != nil {
174 return fmt.Errorf("expanding EAB MAC key (redacted): %v", err)
175 }
176 iss.ExternalAccount.MACKey = macKey
177 }
178 }
179
180 // expand account key, if non-empty
181 if iss.AccountKey != "" {
182 accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true)
183 if err != nil {
184 return fmt.Errorf("expanding account key PEM '%s': %v", iss.AccountKey, err)
185 }
186 iss.AccountKey = accountKey
187 }
188
189 // expand DNS override domain, if non-empty
190 if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.OverrideDomain != "" {
191 overrideDomain, err := repl.ReplaceOrErr(iss.Challenges.DNS.OverrideDomain, true, true)
192 if err != nil {
193 return fmt.Errorf("expanding DNS override domain '%s': %v", iss.Challenges.DNS.OverrideDomain, err)
194 }
195 iss.Challenges.DNS.OverrideDomain = overrideDomain
196 }
197
198 // DNS challenge provider, if not already established
199 if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.solver == nil {
200 var prov certmagic.DNSProvider
201 if iss.Challenges.DNS.ProviderRaw != nil {
202 // a challenge provider has been locally configured - use it
203 val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw")
204 if err != nil {
205 return fmt.Errorf("loading DNS provider module: %v", err)
206 }
207 prov = val.(certmagic.DNSProvider)
208 } else if tlsAppIface, err := ctx.AppIfConfigured("tls"); err == nil {
209 // no locally configured DNS challenge provider, but if there is
210 // a global DNS module configured with the TLS app, use that
211 tlsApp := tlsAppIface.(*TLS)
212 if tlsApp.dns != nil {
213 prov = tlsApp.dns.(certmagic.DNSProvider)
214 }
215 }
216 if prov == nil {
217 return fmt.Errorf("DNS challenge enabled, but no DNS provider configured")
218 }
219 iss.Challenges.DNS.solver = &certmagic.DNS01Solver{
220 DNSManager: certmagic.DNSManager{
221 DNSProvider: prov,
222 TTL: time.Duration(iss.Challenges.DNS.TTL),
223 PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
224 PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
225 Resolvers: iss.Challenges.DNS.Resolvers,
226 OverrideDomain: iss.Challenges.DNS.OverrideDomain,
227 Logger: iss.logger.Named("dns_manager"),
228 },
229 }
230 }
231
232 // add any custom CAs to trust store
233 if len(iss.TrustedRootsPEMFiles) > 0 {
234 iss.rootPool = x509.NewCertPool()
235 for _, pemFile := range iss.TrustedRootsPEMFiles {
236 pemData, err := os.ReadFile(pemFile)
237 if err != nil {
238 return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err)
239 }
240 if !iss.rootPool.AppendCertsFromPEM(pemData) {
241 return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err)
242 }
243 }
244 }
245
246 var err error
247 iss.template, err = iss.makeIssuerTemplate(ctx)
248 if err != nil {
249 return err
250 }
251
252 return nil
253 }
254
255 func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssuer, error) {
256 template := certmagic.ACMEIssuer{
257 CA: iss.CA,
258 TestCA: iss.TestCA,
259 Email: iss.Email,
260 Profile: iss.Profile,
261 AccountKeyPEM: iss.AccountKey,
262 CertObtainTimeout: time.Duration(iss.ACMETimeout),
263 TrustedRoots: iss.rootPool,
264 ExternalAccount: iss.ExternalAccount,
265 NotAfter: time.Duration(iss.CertificateLifetime),
266 Logger: iss.logger,
267 }
268
269 // LoadModule clears NetworkProxyRaw, so keep the proxy for when the
270 // issuer is provisioned again (e.g. by the HTTP app's auto-HTTPS)
271 if len(iss.NetworkProxyRaw) != 0 {
272 proxyMod, err := ctx.LoadModule(iss, "NetworkProxyRaw")
273 if err != nil {
274 return template, fmt.Errorf("failed to load network_proxy module: %v", err)
275 }
276 if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok {
277 iss.httpProxy = m.ProxyFunc()
278 } else {
279 return template, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``")
280 }
281 }
282 template.HTTPProxy = iss.httpProxy
283
284 if iss.Challenges != nil {
285 if iss.Challenges.HTTP != nil {
286 template.DisableHTTPChallenge = iss.Challenges.HTTP.Disabled
287 template.AltHTTPPort = iss.Challenges.HTTP.AlternatePort
288 }
289 if iss.Challenges.TLSALPN != nil {
290 template.DisableTLSALPNChallenge = iss.Challenges.TLSALPN.Disabled
291 template.AltTLSALPNPort = iss.Challenges.TLSALPN.AlternatePort
292 }
293 if iss.Challenges.DNS != nil {
294 template.DNS01Solver = iss.Challenges.DNS.solver
295 }
296 template.ListenHost = iss.Challenges.BindHost
297 if iss.Challenges.Distributed != nil {
298 template.DisableDistributedSolvers = !*iss.Challenges.Distributed
299 }
300 }
301
302 if iss.PreferredChains != nil {
303 template.PreferredChains = certmagic.ChainPreference{
304 Smallest: iss.PreferredChains.Smallest,
305 AnyCommonName: iss.PreferredChains.AnyCommonName,
306 RootCommonName: iss.PreferredChains.RootCommonName,
307 }
308 }
309
310 // ZeroSSL requires EAB, but we can generate that automatically (requires an email address be configured)
311 if strings.HasPrefix(iss.CA, "https://acme.zerossl.com/") {
312 template.NewAccountFunc = func(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
313 if acmeIss.ExternalAccount != nil {
314 return acct, nil
315 }
316 var err error
317 acmeIss.ExternalAccount, acct, err = iss.generateZeroSSLEABCredentials(ctx, acct)
318 return acct, err
319 }
320 }
321
322 return template, nil
323 }
324
325 // SetConfig sets the associated certmagic config for this issuer.
326 // This is required because ACME needs values from the config in
327 // order to solve the challenges during issuance. This implements
328 // the ConfigSetter interface.
329 func (iss *ACMEIssuer) SetConfig(cfg *certmagic.Config) {
330 iss.magic = cfg
331 iss.issuer = certmagic.NewACMEIssuer(cfg, iss.template)
332 }
333
334 // PreCheck implements the certmagic.PreChecker interface.
335 func (iss *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
336 return iss.issuer.PreCheck(ctx, names, interactive)
337 }
338
339 // Issue obtains a certificate for the given csr.
340 func (iss *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
341 return iss.issuer.Issue(ctx, csr)
342 }
343
344 // IssuerKey returns the unique issuer key for the configured CA endpoint.
345 func (iss *ACMEIssuer) IssuerKey() string {
346 return iss.issuer.IssuerKey()
347 }
348
349 // Revoke revokes the given certificate.
350 func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error {
351 return iss.issuer.Revoke(ctx, cert, reason)
352 }
353
354 // GetACMEIssuer returns iss. This is useful when other types embed ACMEIssuer, because
355 // type-asserting them to *ACMEIssuer will fail, but type-asserting them to an interface
356 // with only this method will succeed, and will still allow the embedded ACMEIssuer
357 // to be accessed and manipulated.
358 func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss }
359
360 // GetRenewalInfo wraps the underlying GetRenewalInfo method and satisfies
361 // the CertMagic interface for ARI support.
362 func (iss *ACMEIssuer) GetRenewalInfo(ctx context.Context, cert certmagic.Certificate) (acme.RenewalInfo, error) {
363 return iss.issuer.GetRenewalInfo(ctx, cert)
364 }
365
366 // generateZeroSSLEABCredentials generates ZeroSSL EAB credentials for the primary contact email
367 // on the issuer. It should only be usedif the CA endpoint is ZeroSSL. An email address is required.
368 func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) {
369 if strings.TrimSpace(iss.Email) == "" {
370 return nil, acme.Account{}, fmt.Errorf("your email address is required to use ZeroSSL's ACME endpoint")
371 }
372
373 if len(acct.Contact) == 0 {
374 // we borrow the email from config or the default email, so ensure it's saved with the account
375 acct.Contact = []string{"mailto:" + iss.Email}
376 }
377
378 endpoint := zerossl.BaseURL + "/acme/eab-credentials-email"
379 form := url.Values{"email": []string{iss.Email}}
380 body := strings.NewReader(form.Encode())
381
382 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
383 if err != nil {
384 return nil, acct, fmt.Errorf("forming request: %v", err)
385 }
386 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
387 req.Header.Set("User-Agent", certmagic.UserAgent)
388
389 resp, err := http.DefaultClient.Do(req) //nolint:gosec // no SSRF since URL is from trusted config
390 if err != nil {
391 return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err)
392 }
393 defer resp.Body.Close()
394
395 var result struct {
396 Success bool `json:"success"`
397 Error struct {
398 Code int `json:"code"`
399 Type string `json:"type"`
400 } `json:"error"`
401 EABKID string `json:"eab_kid"`
402 EABHMACKey string `json:"eab_hmac_key"`
403 }
404 err = json.NewDecoder(resp.Body).Decode(&result)
405 if err != nil {
406 return nil, acct, fmt.Errorf("decoding API response: %v", err)
407 }
408 if result.Error.Code != 0 {
409 // do this check first because ZeroSSL's API returns 200 on errors
410 return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)",
411 resp.StatusCode, result.Error.Type, result.Error.Code)
412 }
413 if resp.StatusCode != http.StatusOK {
414 return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
415 }
416
417 if c := iss.logger.Check(zapcore.InfoLevel, "generated EAB credentials"); c != nil {
418 c.Write(zap.String("key_id", result.EABKID))
419 }
420
421 return &acme.EAB{
422 KeyID: result.EABKID,
423 MACKey: result.EABHMACKey,
424 }, acct, nil
425 }
426
427 // UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
428 //
429 // ... acme [<directory_url>] {
430 // dir <directory_url>
431 // test_dir <test_directory_url>
432 // email <email>
433 // profile <profile_name>
434 // timeout <duration>
435 // disable_http_challenge
436 // disable_tlsalpn_challenge
437 // alt_http_port <port>
438 // alt_tlsalpn_port <port>
439 // eab <key_id> <mac_key>
440 // trusted_roots <pem_files...>
441 // dns <provider_name> [<options>]
442 // propagation_delay <duration>
443 // propagation_timeout <duration>
444 // resolvers <dns_servers...>
445 // dns_ttl <duration>
446 // dns_challenge_override_domain <domain>
447 // preferred_chains [smallest] {
448 // root_common_name <common_names...>
449 // any_common_name <common_names...>
450 // }
451 // }
452 func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
453 d.Next() // consume issuer name
454
455 if d.NextArg() {
456 iss.CA = d.Val()
457 if d.NextArg() {
458 return d.ArgErr()
459 }
460 }
461
462 for d.NextBlock(0) {
463 switch d.Val() {
464 case "lifetime":
465 var lifetimeStr string
466 if !d.AllArgs(&lifetimeStr) {
467 return d.ArgErr()
468 }
469 lifetime, err := caddy.ParseDuration(lifetimeStr)
470 if err != nil {
471 return d.Errf("invalid lifetime %s: %v", lifetimeStr, err)
472 }
473 if lifetime < 0 {
474 return d.Errf("lifetime must be >= 0: %s", lifetime)
475 }
476 iss.CertificateLifetime = caddy.Duration(lifetime)
477
478 case "dir":
479 if iss.CA != "" {
480 return d.Errf("directory is already specified: %s", iss.CA)
481 }
482 if !d.AllArgs(&iss.CA) {
483 return d.ArgErr()
484 }
485
486 case "test_dir":
487 if !d.AllArgs(&iss.TestCA) {
488 return d.ArgErr()
489 }
490
491 case "email":
492 if !d.AllArgs(&iss.Email) {
493 return d.ArgErr()
494 }
495
496 case "profile":
497 if !d.AllArgs(&iss.Profile) {
498 return d.ArgErr()
499 }
500
501 case "timeout":
502 var timeoutStr string
503 if !d.AllArgs(&timeoutStr) {
504 return d.ArgErr()
505 }
506 timeout, err := caddy.ParseDuration(timeoutStr)
507 if err != nil {
508 return d.Errf("invalid timeout duration %s: %v", timeoutStr, err)
509 }
510 iss.ACMETimeout = caddy.Duration(timeout)
511
512 case "disable_http_challenge":
513 if d.NextArg() {
514 return d.ArgErr()
515 }
516 if iss.Challenges == nil {
517 iss.Challenges = new(ChallengesConfig)
518 }
519 if iss.Challenges.HTTP == nil {
520 iss.Challenges.HTTP = new(HTTPChallengeConfig)
521 }
522 iss.Challenges.HTTP.Disabled = true
523
524 case "disable_tlsalpn_challenge":
525 if d.NextArg() {
526 return d.ArgErr()
527 }
528 if iss.Challenges == nil {
529 iss.Challenges = new(ChallengesConfig)
530 }
531 if iss.Challenges.TLSALPN == nil {
532 iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig)
533 }
534 iss.Challenges.TLSALPN.Disabled = true
535
536 case "distributed":
537 if !d.NextArg() {
538 return d.ArgErr()
539 }
540 if d.Val() != "false" {
541 return d.Errf("only accepted value is 'false'")
542 }
543 if iss.Challenges == nil {
544 iss.Challenges = new(ChallengesConfig)
545 }
546 if iss.Challenges.Distributed == nil {
547 iss.Challenges.Distributed = new(bool)
548 }
549
550 case "alt_http_port":
551 if !d.NextArg() {
552 return d.ArgErr()
553 }
554 port, err := strconv.Atoi(d.Val())
555 if err != nil {
556 return d.Errf("invalid port %s: %v", d.Val(), err)
557 }
558 if iss.Challenges == nil {
559 iss.Challenges = new(ChallengesConfig)
560 }
561 if iss.Challenges.HTTP == nil {
562 iss.Challenges.HTTP = new(HTTPChallengeConfig)
563 }
564 iss.Challenges.HTTP.AlternatePort = port
565
566 case "alt_tlsalpn_port":
567 if !d.NextArg() {
568 return d.ArgErr()
569 }
570 port, err := strconv.Atoi(d.Val())
571 if err != nil {
572 return d.Errf("invalid port %s: %v", d.Val(), err)
573 }
574 if iss.Challenges == nil {
575 iss.Challenges = new(ChallengesConfig)
576 }
577 if iss.Challenges.TLSALPN == nil {
578 iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig)
579 }
580 iss.Challenges.TLSALPN.AlternatePort = port
581
582 case "eab":
583 iss.ExternalAccount = new(acme.EAB)
584 if !d.AllArgs(&iss.ExternalAccount.KeyID, &iss.ExternalAccount.MACKey) {
585 return d.ArgErr()
586 }
587
588 case "trusted_roots":
589 iss.TrustedRootsPEMFiles = d.RemainingArgs()
590
591 case "dns":
592 if iss.Challenges == nil {
593 iss.Challenges = new(ChallengesConfig)
594 }
595 if iss.Challenges.DNS == nil {
596 iss.Challenges.DNS = new(DNSChallengeConfig)
597 }
598 if d.NextArg() {
599 provName := d.Val()
600 unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName)
601 if err != nil {
602 return err
603 }
604 iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil)
605 }
606
607 case "propagation_delay":
608 if !d.NextArg() {
609 return d.ArgErr()
610 }
611 delayStr := d.Val()
612 delay, err := caddy.ParseDuration(delayStr)
613 if err != nil {
614 return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err)
615 }
616 if iss.Challenges == nil {
617 iss.Challenges = new(ChallengesConfig)
618 }
619 if iss.Challenges.DNS == nil {
620 iss.Challenges.DNS = new(DNSChallengeConfig)
621 }
622 iss.Challenges.DNS.PropagationDelay = caddy.Duration(delay)
623
624 case "propagation_timeout":
625 if !d.NextArg() {
626 return d.ArgErr()
627 }
628 timeoutStr := d.Val()
629 var timeout time.Duration
630 if timeoutStr == "-1" {
631 timeout = time.Duration(-1)
632 } else {
633 var err error
634 timeout, err = caddy.ParseDuration(timeoutStr)
635 if err != nil {
636 return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err)
637 }
638 }
639 if iss.Challenges == nil {
640 iss.Challenges = new(ChallengesConfig)
641 }
642 if iss.Challenges.DNS == nil {
643 iss.Challenges.DNS = new(DNSChallengeConfig)
644 }
645 iss.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout)
646
647 case "resolvers":
648 if iss.Challenges == nil {
649 iss.Challenges = new(ChallengesConfig)
650 }
651 if iss.Challenges.DNS == nil {
652 iss.Challenges.DNS = new(DNSChallengeConfig)
653 }
654 iss.Challenges.DNS.Resolvers = d.RemainingArgs()
655 if len(iss.Challenges.DNS.Resolvers) == 0 {
656 return d.ArgErr()
657 }
658
659 case "dns_ttl":
660 if !d.NextArg() {
661 return d.ArgErr()
662 }
663 ttlStr := d.Val()
664 ttl, err := caddy.ParseDuration(ttlStr)
665 if err != nil {
666 return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err)
667 }
668 if iss.Challenges == nil {
669 iss.Challenges = new(ChallengesConfig)
670 }
671 if iss.Challenges.DNS == nil {
672 iss.Challenges.DNS = new(DNSChallengeConfig)
673 }
674 iss.Challenges.DNS.TTL = caddy.Duration(ttl)
675
676 case "dns_challenge_override_domain":
677 arg := d.RemainingArgs()
678 if len(arg) != 1 {
679 return d.ArgErr()
680 }
681 if iss.Challenges == nil {
682 iss.Challenges = new(ChallengesConfig)
683 }
684 if iss.Challenges.DNS == nil {
685 iss.Challenges.DNS = new(DNSChallengeConfig)
686 }
687 iss.Challenges.DNS.OverrideDomain = arg[0]
688
689 case "preferred_chains":
690 chainPref, err := ParseCaddyfilePreferredChainsOptions(d)
691 if err != nil {
692 return err
693 }
694 iss.PreferredChains = chainPref
695
696 default:
697 return d.Errf("unrecognized ACME issuer property: %s", d.Val())
698 }
699 }
700 return nil
701 }
702
703 func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPreference, error) {
704 chainPref := new(ChainPreference)
705 if d.NextArg() {
706 smallestOpt := d.Val()
707 if smallestOpt == "smallest" {
708 trueBool := true
709 chainPref.Smallest = &trueBool
710 if d.NextArg() { // Only one argument allowed
711 return nil, d.ArgErr()
712 }
713 if d.NextBlock(d.Nesting()) { // Don't allow other options when smallest == true
714 return nil, d.Err("No more options are accepted when using the 'smallest' option")
715 }
716 } else { // Smallest option should always be 'smallest' or unset
717 return nil, d.Errf("Invalid argument '%s'", smallestOpt)
718 }
719 }
720 for nesting := d.Nesting(); d.NextBlock(nesting); {
721 switch d.Val() {
722 case "root_common_name":
723 rootCommonNameOpt := d.RemainingArgs()
724 chainPref.RootCommonName = append(chainPref.RootCommonName, rootCommonNameOpt...)
725 if rootCommonNameOpt == nil {
726 return nil, d.ArgErr()
727 }
728 if chainPref.AnyCommonName != nil {
729 return nil, d.Err("Can't set root_common_name when any_common_name is already set")
730 }
731
732 case "any_common_name":
733 anyCommonNameOpt := d.RemainingArgs()
734 chainPref.AnyCommonName = append(chainPref.AnyCommonName, anyCommonNameOpt...)
735 if anyCommonNameOpt == nil {
736 return nil, d.ArgErr()
737 }
738 if chainPref.RootCommonName != nil {
739 return nil, d.Err("Can't set any_common_name when root_common_name is already set")
740 }
741
742 default:
743 return nil, d.Errf("Received unrecognized parameter '%s'", d.Val())
744 }
745 }
746
747 if chainPref.Smallest == nil && chainPref.RootCommonName == nil && chainPref.AnyCommonName == nil {
748 return nil, d.Err("No options for preferred_chains received")
749 }
750
751 return chainPref, nil
752 }
753
754 // ChainPreference describes the client's preferred certificate chain,
755 // useful if the CA offers alternate chains. The first matching chain
756 // will be selected.
757 type ChainPreference struct {
758 // Prefer chains with the fewest number of bytes.
759 Smallest *bool `json:"smallest,omitempty"`
760
761 // Select first chain having a root with one of
762 // these common names.
763 RootCommonName []string `json:"root_common_name,omitempty"`
764
765 // Select first chain that has any issuer with one
766 // of these common names.
767 AnyCommonName []string `json:"any_common_name,omitempty"`
768 }
769
770 // Interface guards
771 var (
772 _ certmagic.PreChecker = (*ACMEIssuer)(nil)
773 _ certmagic.Issuer = (*ACMEIssuer)(nil)
774 _ certmagic.Revoker = (*ACMEIssuer)(nil)
775 _ certmagic.RenewalInfoGetter = (*ACMEIssuer)(nil)
776 _ caddy.Provisioner = (*ACMEIssuer)(nil)
777 _ ConfigSetter = (*ACMEIssuer)(nil)
778 _ caddyfile.Unmarshaler = (*ACMEIssuer)(nil)
779 )