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
| 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 | ) |