diff options
author | techknowlogick <techknowlogick@gitea.io> | 2021-01-24 18:37:35 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-25 01:37:35 +0200 |
commit | d2ea21d0d8103986b2ce53c17b7b99b1ce6828b0 (patch) | |
tree | 802ea1a787b1f6ef08b18524d3818115a750f0eb /vendor/github.com/mholt | |
parent | bc05ddc0ebd6fdc826ef2beec99304bac60ddd8a (diff) | |
download | gitea-d2ea21d0d8103986b2ce53c17b7b99b1ce6828b0.tar.gz gitea-d2ea21d0d8103986b2ce53c17b7b99b1ce6828b0.zip |
Use caddy's certmagic library for extensible/robust ACME handling (#14177)
* use certmagic for more extensible/robust ACME cert handling
* accept TOS based on config option
Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lauris BH <lauris@nix.lv>
Diffstat (limited to 'vendor/github.com/mholt')
-rw-r--r-- | vendor/github.com/mholt/acmez/.gitignore | 1 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/LICENSE | 201 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/README.md | 59 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/THIRD-PARTY | 37 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/account.go | 249 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/authorization.go | 283 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/certificate.go | 165 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/challenge.go | 133 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/client.go | 240 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/http.go | 394 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/jws.go | 263 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/order.go | 247 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/problem.go | 136 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/client.go | 656 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/go.mod | 8 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/go.sum | 61 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/solver.go | 72 | ||||
-rw-r--r-- | vendor/github.com/mholt/acmez/tlsalpn01.go | 98 |
18 files changed, 3303 insertions, 0 deletions
diff --git a/vendor/github.com/mholt/acmez/.gitignore b/vendor/github.com/mholt/acmez/.gitignore new file mode 100644 index 0000000000..fbd281d14e --- /dev/null +++ b/vendor/github.com/mholt/acmez/.gitignore @@ -0,0 +1 @@ +_gitignore/ diff --git a/vendor/github.com/mholt/acmez/LICENSE b/vendor/github.com/mholt/acmez/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/vendor/github.com/mholt/acmez/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/mholt/acmez/README.md b/vendor/github.com/mholt/acmez/README.md new file mode 100644 index 0000000000..c9c28aaad1 --- /dev/null +++ b/vendor/github.com/mholt/acmez/README.md @@ -0,0 +1,59 @@ +acmez - ACME client library for Go +================================== + +[![godoc](https://pkg.go.dev/badge/github.com/mholt/acmez)](https://pkg.go.dev/github.com/mholt/acmez) + +ACMEz ("ack-measy" or "acme-zee", whichever you prefer) is a fully-compliant [RFC 8555](https://tools.ietf.org/html/rfc8555) (ACME) implementation in pure Go. It is lightweight, has an elegant Go API, and its retry logic is highly robust against external errors. ACMEz is suitable for large-scale enterprise deployments. + +**NOTE:** This module is for _getting_ certificates, not _managing_ certificates. Most users probably want certificate _management_ (keeping certificates renewed) rather than to interface directly with ACME. Developers who want to use certificates in their long-running Go programs should use [CertMagic](https://github.com/caddyserver/certmagic) instead; or, if their program is not written in Go, [Caddy](https://caddyserver.com/) can be used to manage certificates (even without running an HTTP or TLS server). + +This module has two primary packages: + +- **`acmez`** is a high-level wrapper for getting certificates. It implements the ACME order flow described in RFC 8555 including challenge solving using pluggable solvers. +- **`acme`** is a low-level RFC 8555 implementation that provides the fundamental ACME operations, mainly useful if you have advanced or niche requirements. + +In other words, the `acmez` package is **porcelain** while the `acme` package is **plumbing** (to use git's terminology). + + +## Features + +- Simple, elegant Go API +- Thoroughly documented with spec citations +- Robust to external errors +- Structured error values ("problems" as defined in RFC 7807) +- Smart retries (resilient against network and server hiccups) +- Challenge plasticity (randomized challenges, and will retry others if one fails) +- Context cancellation (suitable for high-frequency config changes or reloads) +- Highly flexible and customizable +- External Account Binding (EAB) support +- Tested with multiple ACME CAs (more than just Let's Encrypt) +- Supports niche aspects of RFC 8555 (such as alt cert chains and account key rollover) +- Efficient solving of large SAN lists (e.g. for slow DNS record propagation) +- Utility functions for solving challenges +- Helpers for RFC 8737 (tls-alpn-01 challenge) + +The `acmez` package is "bring-your-own-solver." It provides helper utilities for http-01, dns-01, and tls-alpn-01 challenges, but does not actually solve them for you. You must write an implementation of `acmez.Solver` in order to get certificates. How this is done depends on the environment in which you're using this code. + +This is not a command line utility either. The goal is to not add more external tooling to already-complex infrastructure: ACME and TLS should be built-in to servers rather than tacked on as an afterthought. + + +## Examples + +See the `examples` folder for tutorials on how to use either package. + + +## History + +In 2014, the ISRG was finishing the development of its automated CA infrastructure: the first of its kind to become publicly-trusted, under the name Let's Encrypt, which used a young protocol called ACME to automate domain validation and certificate issuance. + +Meanwhile, a project called [Caddy](https://caddyserver.com) was being developed which would be the first and only web server to use HTTPS _automatically and by default_. To make that possible, another project called lego was commissioned by the Caddy project to become of the first-ever ACME client libraries, and the first client written in Go. It was made by Sebastian Erhart (xenolf), and on day 1 of Let's Encrypt's public beta, Caddy used lego to obtain its first certificate automatically at startup, making Caddy and lego the first-ever integrated ACME client. + +Since then, Caddy has seen use in production longer than any other ACME client integration, and is well-known for being one of the most robust and reliable HTTPS implementations available today. + +A few years later, Caddy's novel auto-HTTPS logic was extracted into a library called [CertMagic](https://github.com/caddyserver/certmagic) to be usable by any Go program. Caddy would continue to use CertMagic, which implemented the certificate _automation and management_ logic on top of the low-level certificate _obtain_ logic that lego provided. + +Soon thereafter, the lego project shifted maintainership and the goals and vision of the project diverged from those of Caddy's use case of managing tens of thousands of certificates per instance. Eventually, [the original Caddy author announced work on a new ACME client library in Go](https://github.com/caddyserver/certmagic/issues/71) that exceeded Caddy's harsh requirements for large-scale enterprise deployments, lean builds, and simple API. This work finally came to fruition in 2020 as ACMEz. + +--- + +(c) 2020 Matthew Holt diff --git a/vendor/github.com/mholt/acmez/THIRD-PARTY b/vendor/github.com/mholt/acmez/THIRD-PARTY new file mode 100644 index 0000000000..876c2ef089 --- /dev/null +++ b/vendor/github.com/mholt/acmez/THIRD-PARTY @@ -0,0 +1,37 @@ +This document contains Third Party Software Notices and/or Additional +Terms and Conditions for licensed third party software components +included within this product. + +== + +https://github.com/golang/crypto/blob/master/acme/jws.go +https://github.com/golang/crypto/blob/master/acme/jws_test.go +(with modifications) + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/mholt/acmez/acme/account.go b/vendor/github.com/mholt/acmez/acme/account.go new file mode 100644 index 0000000000..b103eb25f4 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/account.go @@ -0,0 +1,249 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acme + +import ( + "context" + "crypto" + "encoding/base64" + "encoding/json" + "fmt" +) + +// Account represents a set of metadata associated with an account +// as defined by the ACME spec §7.1.2: +// https://tools.ietf.org/html/rfc8555#section-7.1.2 +type Account struct { + // status (required, string): The status of this account. Possible + // values are "valid", "deactivated", and "revoked". The value + // "deactivated" should be used to indicate client-initiated + // deactivation whereas "revoked" should be used to indicate server- + // initiated deactivation. See Section 7.1.6. + Status string `json:"status"` + + // contact (optional, array of string): An array of URLs that the + // server can use to contact the client for issues related to this + // account. For example, the server may wish to notify the client + // about server-initiated revocation or certificate expiration. For + // information on supported URL schemes, see Section 7.3. + Contact []string `json:"contact,omitempty"` + + // termsOfServiceAgreed (optional, boolean): Including this field in a + // newAccount request, with a value of true, indicates the client's + // agreement with the terms of service. This field cannot be updated + // by the client. + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` + + // externalAccountBinding (optional, object): Including this field in a + // newAccount request indicates approval by the holder of an existing + // non-ACME account to bind that account to this ACME account. This + // field is not updateable by the client (see Section 7.3.4). + // + // Use SetExternalAccountBinding() to set this field's value properly. + ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` + + // orders (required, string): A URL from which a list of orders + // submitted by this account can be fetched via a POST-as-GET + // request, as described in Section 7.1.2.1. + Orders string `json:"orders"` + + // In response to new-account, "the server returns this account + // object in a 201 (Created) response, with the account URL + // in a Location header field." §7.3 + // + // We transfer the value from the header to this field for + // storage and recall purposes. + Location string `json:"location,omitempty"` + + // The private key to the account. Because it is secret, it is + // not serialized as JSON and must be stored separately (usually + // a PEM-encoded file). + PrivateKey crypto.Signer `json:"-"` +} + +// SetExternalAccountBinding sets the ExternalAccountBinding field of the account. +// It only sets the field value; it does not register the account with the CA. (The +// client parameter is necessary because the EAB encoding depends on the directory.) +func (a *Account) SetExternalAccountBinding(ctx context.Context, client *Client, eab EAB) error { + if err := client.provision(ctx); err != nil { + return err + } + + macKey, err := base64.RawURLEncoding.DecodeString(eab.MACKey) + if err != nil { + return fmt.Errorf("base64-decoding MAC key: %w", err) + } + + eabJWS, err := jwsEncodeEAB(a.PrivateKey.Public(), macKey, keyID(eab.KeyID), client.dir.NewAccount) + if err != nil { + return fmt.Errorf("signing EAB content: %w", err) + } + + a.ExternalAccountBinding = eabJWS + + return nil +} + +// NewAccount creates a new account on the ACME server. +// +// "A client creates a new account with the server by sending a POST +// request to the server's newAccount URL." §7.3 +func (c *Client) NewAccount(ctx context.Context, account Account) (Account, error) { + if err := c.provision(ctx); err != nil { + return account, err + } + return c.postAccount(ctx, c.dir.NewAccount, accountObject{Account: account}) +} + +// GetAccount looks up an account on the ACME server. +// +// "If a client wishes to find the URL for an existing account and does +// not want an account to be created if one does not already exist, then +// it SHOULD do so by sending a POST request to the newAccount URL with +// a JWS whose payload has an 'onlyReturnExisting' field set to 'true'." +// §7.3.1 +func (c *Client) GetAccount(ctx context.Context, account Account) (Account, error) { + if err := c.provision(ctx); err != nil { + return account, err + } + return c.postAccount(ctx, c.dir.NewAccount, accountObject{ + Account: account, + OnlyReturnExisting: true, + }) +} + +// UpdateAccount updates account information on the ACME server. +// +// "If the client wishes to update this information in the future, it +// sends a POST request with updated information to the account URL. +// The server MUST ignore any updates to the 'orders' field, +// 'termsOfServiceAgreed' field (see Section 7.3.3), the 'status' field +// (except as allowed by Section 7.3.6), or any other fields it does not +// recognize." §7.3.2 +// +// This method uses the account.Location value as the account URL. +func (c *Client) UpdateAccount(ctx context.Context, account Account) (Account, error) { + return c.postAccount(ctx, account.Location, accountObject{Account: account}) +} + +type keyChangeRequest struct { + Account string `json:"account"` + OldKey json.RawMessage `json:"oldKey"` +} + +// AccountKeyRollover changes an account's associated key. +// +// "To change the key associated with an account, the client sends a +// request to the server containing signatures by both the old and new +// keys." §7.3.5 +func (c *Client) AccountKeyRollover(ctx context.Context, account Account, newPrivateKey crypto.Signer) (Account, error) { + if err := c.provision(ctx); err != nil { + return account, err + } + + oldPublicKeyJWK, err := jwkEncode(account.PrivateKey.Public()) + if err != nil { + return account, fmt.Errorf("encoding old private key: %v", err) + } + + keyChangeReq := keyChangeRequest{ + Account: account.Location, + OldKey: []byte(oldPublicKeyJWK), + } + + innerJWS, err := jwsEncodeJSON(keyChangeReq, newPrivateKey, "", "", c.dir.KeyChange) + if err != nil { + return account, fmt.Errorf("encoding inner JWS: %v", err) + } + + _, err = c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.KeyChange, json.RawMessage(innerJWS), nil) + if err != nil { + return account, fmt.Errorf("rolling key on server: %w", err) + } + + account.PrivateKey = newPrivateKey + + return account, nil + +} + +func (c *Client) postAccount(ctx context.Context, endpoint string, account accountObject) (Account, error) { + // Normally, the account URL is the key ID ("kid")... except when the user + // is trying to get the correct account URL. In that case, we must ignore + // any existing URL we may have and not set the kid field on the request. + // Arguably, this is a user error (spec says "If client wishes to find the + // URL for an existing account", so why would the URL already be filled + // out?) but it's easy enough to infer their intent and make it work. + kid := account.Location + if account.OnlyReturnExisting { + kid = "" + } + + resp, err := c.httpPostJWS(ctx, account.PrivateKey, kid, endpoint, account, &account.Account) + if err != nil { + return account.Account, err + } + + account.Location = resp.Header.Get("Location") + + return account.Account, nil +} + +type accountObject struct { + Account + + // If true, newAccount will be read-only, and Account.Location + // (which holds the account URL) must be empty. + OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` +} + +// EAB (External Account Binding) contains information +// necessary to bind or map an ACME account to some +// other account known by the CA. +// +// External account bindings are "used to associate an +// ACME account with an existing account in a non-ACME +// system, such as a CA customer database." +// +// "To enable ACME account binding, the CA operating the +// ACME server needs to provide the ACME client with a +// MAC key and a key identifier, using some mechanism +// outside of ACME." §7.3.4 +type EAB struct { + // "The key identifier MUST be an ASCII string." §7.3.4 + KeyID string `json:"key_id"` + + // "The MAC key SHOULD be provided in base64url-encoded + // form, to maximize compatibility between non-ACME + // provisioning systems and ACME clients." §7.3.4 + MACKey string `json:"mac_key"` +} + +// Possible status values. From several spec sections: +// - Account §7.1.2 (valid, deactivated, revoked) +// - Order §7.1.3 (pending, ready, processing, valid, invalid) +// - Authorization §7.1.4 (pending, valid, invalid, deactivated, expired, revoked) +// - Challenge §7.1.5 (pending, processing, valid, invalid) +// - Status changes §7.1.6 +const ( + StatusPending = "pending" + StatusProcessing = "processing" + StatusValid = "valid" + StatusInvalid = "invalid" + StatusDeactivated = "deactivated" + StatusExpired = "expired" + StatusRevoked = "revoked" + StatusReady = "ready" +) diff --git a/vendor/github.com/mholt/acmez/acme/authorization.go b/vendor/github.com/mholt/acmez/acme/authorization.go new file mode 100644 index 0000000000..3e69dcc6f6 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/authorization.go @@ -0,0 +1,283 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acme + +import ( + "context" + "fmt" + "time" +) + +// Authorization "represents a server's authorization for +// an account to represent an identifier. In addition to the +// identifier, an authorization includes several metadata fields, such +// as the status of the authorization (e.g., 'pending', 'valid', or +// 'revoked') and which challenges were used to validate possession of +// the identifier." §7.1.4 +type Authorization struct { + // identifier (required, object): The identifier that the account is + // authorized to represent. + Identifier Identifier `json:"identifier"` + + // status (required, string): The status of this authorization. + // Possible values are "pending", "valid", "invalid", "deactivated", + // "expired", and "revoked". See Section 7.1.6. + Status string `json:"status"` + + // expires (optional, string): The timestamp after which the server + // will consider this authorization invalid, encoded in the format + // specified in [RFC3339]. This field is REQUIRED for objects with + // "valid" in the "status" field. + Expires time.Time `json:"expires,omitempty"` + + // challenges (required, array of objects): For pending authorizations, + // the challenges that the client can fulfill in order to prove + // possession of the identifier. For valid authorizations, the + // challenge that was validated. For invalid authorizations, the + // challenge that was attempted and failed. Each array entry is an + // object with parameters required to validate the challenge. A + // client should attempt to fulfill one of these challenges, and a + // server should consider any one of the challenges sufficient to + // make the authorization valid. + Challenges []Challenge `json:"challenges"` + + // wildcard (optional, boolean): This field MUST be present and true + // for authorizations created as a result of a newOrder request + // containing a DNS identifier with a value that was a wildcard + // domain name. For other authorizations, it MUST be absent. + // Wildcard domain names are described in Section 7.1.3. + Wildcard bool `json:"wildcard,omitempty"` + + // "The server allocates a new URL for this authorization and returns a + // 201 (Created) response with the authorization URL in the Location + // header field" §7.4.1 + // + // We transfer the value from the header to this field for storage and + // recall purposes. + Location string `json:"-"` +} + +// IdentifierValue returns the Identifier.Value field, adjusted +// according to the Wildcard field. +func (authz Authorization) IdentifierValue() string { + if authz.Wildcard { + return "*." + authz.Identifier.Value + } + return authz.Identifier.Value +} + +// fillChallengeFields populates extra fields in the challenge structs so that +// challenges can be solved without needing a bunch of unnecessary extra state. +func (authz *Authorization) fillChallengeFields(account Account) error { + accountThumbprint, err := jwkThumbprint(account.PrivateKey.Public()) + if err != nil { + return fmt.Errorf("computing account JWK thumbprint: %v", err) + } + for i := 0; i < len(authz.Challenges); i++ { + authz.Challenges[i].Identifier = authz.Identifier + if authz.Challenges[i].KeyAuthorization == "" { + authz.Challenges[i].KeyAuthorization = authz.Challenges[i].Token + "." + accountThumbprint + } + } + return nil +} + +// NewAuthorization creates a new authorization for an identifier using +// the newAuthz endpoint of the directory, if available. This function +// creates authzs out of the regular order flow. +// +// "Note that because the identifier in a pre-authorization request is +// the exact identifier to be included in the authorization object, pre- +// authorization cannot be used to authorize issuance of certificates +// containing wildcard domain names." §7.4.1 +func (c *Client) NewAuthorization(ctx context.Context, account Account, id Identifier) (Authorization, error) { + if err := c.provision(ctx); err != nil { + return Authorization{}, err + } + if c.dir.NewAuthz == "" { + return Authorization{}, fmt.Errorf("server does not support newAuthz endpoint") + } + + var authz Authorization + resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.NewAuthz, id, &authz) + if err != nil { + return authz, err + } + + authz.Location = resp.Header.Get("Location") + + err = authz.fillChallengeFields(account) + if err != nil { + return authz, err + } + + return authz, nil +} + +// GetAuthorization fetches an authorization object from the server. +// +// "Authorization resources are created by the server in response to +// newOrder or newAuthz requests submitted by an account key holder; +// their URLs are provided to the client in the responses to these +// requests." +// +// "When a client receives an order from the server in reply to a +// newOrder request, it downloads the authorization resources by sending +// POST-as-GET requests to the indicated URLs. If the client initiates +// authorization using a request to the newAuthz resource, it will have +// already received the pending authorization object in the response to +// that request." §7.5 +func (c *Client) GetAuthorization(ctx context.Context, account Account, authzURL string) (Authorization, error) { + if err := c.provision(ctx); err != nil { + return Authorization{}, err + } + + var authz Authorization + _, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, authzURL, nil, &authz) + if err != nil { + return authz, err + } + + authz.Location = authzURL + + err = authz.fillChallengeFields(account) + if err != nil { + return authz, err + } + + return authz, nil +} + +// PollAuthorization polls the authorization resource endpoint until the authorization is +// considered "finalized" which means that it either succeeded, failed, or was abandoned. +// It blocks until that happens or until the configured timeout. +// +// "Usually, the validation process will take some time, so the client +// will need to poll the authorization resource to see when it is +// finalized." +// +// "For challenges where the client can tell when the server +// has validated the challenge (e.g., by seeing an HTTP or DNS request +// from the server), the client SHOULD NOT begin polling until it has +// seen the validation request from the server." §7.5.1 +func (c *Client) PollAuthorization(ctx context.Context, account Account, authz Authorization) (Authorization, error) { + start, interval, maxDuration := time.Now(), c.pollInterval(), c.pollTimeout() + + if authz.Status != "" { + if finalized, err := authzIsFinalized(authz); finalized { + return authz, err + } + } + + for time.Since(start) < maxDuration { + select { + case <-time.After(interval): + case <-ctx.Done(): + return authz, ctx.Err() + } + + // get the latest authz object + resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, authz.Location, nil, &authz) + if err != nil { + return authz, fmt.Errorf("checking authorization status: %w", err) + } + if finalized, err := authzIsFinalized(authz); finalized { + return authz, err + } + + // "The server MUST provide information about its retry state to the + // client via the 'error' field in the challenge and the Retry-After + // HTTP header field in response to requests to the challenge resource." + // §8.2 + interval, err = retryAfter(resp, interval) + if err != nil { + return authz, err + } + } + + return authz, fmt.Errorf("authorization took too long") +} + +// DeactivateAuthorization deactivates an authorization on the server, which is +// a good idea if the authorization is not going to be utilized by the client. +// +// "If a client wishes to relinquish its authorization to issue +// certificates for an identifier, then it may request that the server +// deactivate each authorization associated with it by sending POST +// requests with the static object {"status": "deactivated"} to each +// authorization URL." §7.5.2 +func (c *Client) DeactivateAuthorization(ctx context.Context, account Account, authzURL string) (Authorization, error) { + if err := c.provision(ctx); err != nil { + return Authorization{}, err + } + + if authzURL == "" { + return Authorization{}, fmt.Errorf("empty authz url") + } + + deactivate := struct { + Status string `json:"status"` + }{Status: "deactivated"} + + var authz Authorization + _, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, authzURL, deactivate, &authz) + authz.Location = authzURL + + return authz, err +} + +// authzIsFinalized returns true if the authorization is finished, +// whether successfully or not. If not, an error will be returned. +// Post-valid statuses that make an authz unusable are treated as +// errors. +func authzIsFinalized(authz Authorization) (bool, error) { + switch authz.Status { + case StatusPending: + // "Authorization objects are created in the 'pending' state." §7.1.6 + return false, nil + + case StatusValid: + // "If one of the challenges listed in the authorization transitions + // to the 'valid' state, then the authorization also changes to the + // 'valid' state." §7.1.6 + return true, nil + + case StatusInvalid: + // "If the client attempts to fulfill a challenge and fails, or if + // there is an error while the authorization is still pending, then + // the authorization transitions to the 'invalid' state." §7.1.6 + var firstProblem Problem + for _, chal := range authz.Challenges { + if chal.Error != nil { + firstProblem = *chal.Error + break + } + } + firstProblem.Resource = authz + return true, fmt.Errorf("authorization failed: %w", firstProblem) + + case StatusExpired, StatusDeactivated, StatusRevoked: + // Once the authorization is in the 'valid' state, it can expire + // ('expired'), be deactivated by the client ('deactivated', see + // Section 7.5.2), or revoked by the server ('revoked')." §7.1.6 + return true, fmt.Errorf("authorization %s", authz.Status) + + case "": + return false, fmt.Errorf("status unknown") + + default: + return true, fmt.Errorf("server set unrecognized authorization status: %s", authz.Status) + } +} diff --git a/vendor/github.com/mholt/acmez/acme/certificate.go b/vendor/github.com/mholt/acmez/acme/certificate.go new file mode 100644 index 0000000000..a778280802 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/certificate.go @@ -0,0 +1,165 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acme + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/base64" + "fmt" + "net/http" +) + +// Certificate represents a certificate chain, which we usually refer +// to as "a certificate" because in practice an end-entity certificate +// is seldom useful/practical without a chain. +type Certificate struct { + // The certificate resource URL as provisioned by + // the ACME server. Some ACME servers may split + // the chain into multiple URLs that are Linked + // together, in which case this URL represents the + // starting point. + URL string `json:"url"` + + // The PEM-encoded certificate chain, end-entity first. + ChainPEM []byte `json:"-"` +} + +// GetCertificateChain downloads all available certificate chains originating from +// the given certURL. This is to be done after an order is finalized. +// +// "To download the issued certificate, the client simply sends a POST- +// as-GET request to the certificate URL." +// +// "The server MAY provide one or more link relation header fields +// [RFC8288] with relation 'alternate'. Each such field SHOULD express +// an alternative certificate chain starting with the same end-entity +// certificate. This can be used to express paths to various trust +// anchors. Clients can fetch these alternates and use their own +// heuristics to decide which is optimal." §7.4.2 +func (c *Client) GetCertificateChain(ctx context.Context, account Account, certURL string) ([]Certificate, error) { + if err := c.provision(ctx); err != nil { + return nil, err + } + + var chains []Certificate + + addChain := func(certURL string) (*http.Response, error) { + // can't pool this buffer; bytes escape scope + buf := new(bytes.Buffer) + + // TODO: set the Accept header? ("application/pem-certificate-chain") See end of §7.4.2 + resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, certURL, nil, buf) + if err != nil { + return resp, err + } + contentType := parseMediaType(resp) + + switch contentType { + case "application/pem-certificate-chain": + chains = append(chains, Certificate{ + URL: certURL, + ChainPEM: buf.Bytes(), + }) + default: + return resp, fmt.Errorf("unrecognized Content-Type from server: %s", contentType) + } + + // "For formats that can only express a single certificate, the server SHOULD + // provide one or more "Link: rel="up"" header fields pointing to an + // issuer or issuers so that ACME clients can build a certificate chain + // as defined in TLS (see Section 4.4.2 of [RFC8446])." (end of §7.4.2) + allUp := extractLinks(resp, "up") + for _, upURL := range allUp { + upCerts, err := c.GetCertificateChain(ctx, account, upURL) + if err != nil { + return resp, fmt.Errorf("retrieving next certificate in chain: %s: %w", upURL, err) + } + for _, upCert := range upCerts { + chains[len(chains)-1].ChainPEM = append(chains[len(chains)-1].ChainPEM, upCert.ChainPEM...) + } + } + + return resp, nil + } + + // always add preferred/first certificate chain + resp, err := addChain(certURL) + if err != nil { + return chains, err + } + + // "The server MAY provide one or more link relation header fields + // [RFC8288] with relation 'alternate'. Each such field SHOULD express + // an alternative certificate chain starting with the same end-entity + // certificate. This can be used to express paths to various trust + // anchors. Clients can fetch these alternates and use their own + // heuristics to decide which is optimal." §7.4.2 + alternates := extractLinks(resp, "alternate") + for _, altURL := range alternates { + resp, err = addChain(altURL) + if err != nil { + return nil, fmt.Errorf("retrieving alternate certificate chain at %s: %w", altURL, err) + } + } + + return chains, nil +} + +// RevokeCertificate revokes the given certificate. If the certificate key is not +// provided, then the account key is used instead. See §7.6. +func (c *Client) RevokeCertificate(ctx context.Context, account Account, cert *x509.Certificate, certKey crypto.Signer, reason int) error { + if err := c.provision(ctx); err != nil { + return err + } + + body := struct { + Certificate string `json:"certificate"` + Reason int `json:"reason"` + }{ + Certificate: base64.RawURLEncoding.EncodeToString(cert.Raw), + Reason: reason, + } + + // "Revocation requests are different from other ACME requests in that + // they can be signed with either an account key pair or the key pair in + // the certificate." §7.6 + kid := "" + if certKey == account.PrivateKey { + kid = account.Location + } + + _, err := c.httpPostJWS(ctx, certKey, kid, c.dir.RevokeCert, body, nil) + return err +} + +// Reasons for revoking a certificate, as defined +// by RFC 5280 §5.3.1. +// https://tools.ietf.org/html/rfc5280#section-5.3.1 +const ( + ReasonUnspecified = iota // 0 + ReasonKeyCompromise // 1 + ReasonCACompromise // 2 + ReasonAffiliationChanged // 3 + ReasonSuperseded // 4 + ReasonCessationOfOperation // 5 + ReasonCertificateHold // 6 + _ // 7 (unused) + ReasonRemoveFromCRL // 8 + ReasonPrivilegeWithdrawn // 9 + ReasonAACompromise // 10 +) diff --git a/vendor/github.com/mholt/acmez/acme/challenge.go b/vendor/github.com/mholt/acmez/acme/challenge.go new file mode 100644 index 0000000000..ccb264cf52 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/challenge.go @@ -0,0 +1,133 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acme + +import ( + "context" + "crypto/sha256" + "encoding/base64" +) + +// Challenge holds information about an ACME challenge. +// +// "An ACME challenge object represents a server's offer to validate a +// client's possession of an identifier in a specific way. Unlike the +// other objects listed above, there is not a single standard structure +// for a challenge object. The contents of a challenge object depend on +// the validation method being used. The general structure of challenge +// objects and an initial set of validation methods are described in +// Section 8." §7.1.5 +type Challenge struct { + // "Challenge objects all contain the following basic fields..." §8 + + // type (required, string): The type of challenge encoded in the + // object. + Type string `json:"type"` + + // url (required, string): The URL to which a response can be posted. + URL string `json:"url"` + + // status (required, string): The status of this challenge. Possible + // values are "pending", "processing", "valid", and "invalid" (see + // Section 7.1.6). + Status string `json:"status"` + + // validated (optional, string): The time at which the server validated + // this challenge, encoded in the format specified in [RFC3339]. + // This field is REQUIRED if the "status" field is "valid". + Validated string `json:"validated,omitempty"` + + // error (optional, object): Error that occurred while the server was + // validating the challenge, if any, structured as a problem document + // [RFC7807]. Multiple errors can be indicated by using subproblems + // Section 6.7.1. A challenge object with an error MUST have status + // equal to "invalid". + Error *Problem `json:"error,omitempty"` + + // "All additional fields are specified by the challenge type." §8 + // (We also add our own for convenience.) + + // "The token for a challenge is a string comprised entirely of + // characters in the URL-safe base64 alphabet." §8.1 + // + // Used by the http-01, tls-alpn-01, and dns-01 challenges. + Token string `json:"token,omitempty"` + + // A key authorization is a string that concatenates the token for the + // challenge with a key fingerprint, separated by a "." character (§8.1): + // + // keyAuthorization = token || '.' || base64url(Thumbprint(accountKey)) + // + // This client package automatically assembles and sets this value for you. + KeyAuthorization string `json:"keyAuthorization,omitempty"` + + // We attach the identifier that this challenge is associated with, which + // may be useful information for solving a challenge. It is not part of the + // structure as defined by the spec but is added by us to provide enough + // information to solve the DNS-01 challenge. + Identifier Identifier `json:"identifier,omitempty"` +} + +// HTTP01ResourcePath returns the URI path for solving the http-01 challenge. +// +// "The path at which the resource is provisioned is comprised of the +// fixed prefix '/.well-known/acme-challenge/', followed by the 'token' +// value in the challenge." §8.3 +func (c Challenge) HTTP01ResourcePath() string { + return "/.well-known/acme-challenge/" + c.Token +} + +// DNS01TXTRecordName returns the name of the TXT record to create for +// solving the dns-01 challenge. +// +// "The client constructs the validation domain name by prepending the +// label '_acme-challenge' to the domain name being validated, then +// provisions a TXT record with the digest value under that name." §8.4 +func (c Challenge) DNS01TXTRecordName() string { + return "_acme-challenge." + c.Identifier.Value +} + +// DNS01KeyAuthorization encodes a key authorization value to be used +// in a TXT record for the _acme-challenge DNS record. +// +// "A client fulfills this challenge by constructing a key authorization +// from the 'token' value provided in the challenge and the client's +// account key. The client then computes the SHA-256 digest [FIPS180-4] +// of the key authorization. +// +// The record provisioned to the DNS contains the base64url encoding of +// this digest." §8.4 +func (c Challenge) DNS01KeyAuthorization() string { + h := sha256.Sum256([]byte(c.KeyAuthorization)) + return base64.RawURLEncoding.EncodeToString(h[:]) +} + +// InitiateChallenge "indicates to the server that it is ready for the challenge +// validation by sending an empty JSON body ('{}') carried in a POST request to +// the challenge URL (not the authorization URL)." §7.5.1 +func (c *Client) InitiateChallenge(ctx context.Context, account Account, challenge Challenge) (Challenge, error) { + if err := c.provision(ctx); err != nil { + return Challenge{}, err + } + _, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, challenge.URL, struct{}{}, &challenge) + return challenge, err +} + +// The standard or well-known ACME challenge types. +const ( + ChallengeTypeHTTP01 = "http-01" // RFC 8555 §8.3 + ChallengeTypeDNS01 = "dns-01" // RFC 8555 §8.4 + ChallengeTypeTLSALPN01 = "tls-alpn-01" // RFC 8737 §3 +) diff --git a/vendor/github.com/mholt/acmez/acme/client.go b/vendor/github.com/mholt/acmez/acme/client.go new file mode 100644 index 0000000000..5037905b68 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/client.go @@ -0,0 +1,240 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package acme full implements the ACME protocol specification as +// described in RFC 8555: https://tools.ietf.org/html/rfc8555. +// +// It is designed to work smoothly in large-scale deployments with +// high resilience to errors and intermittent network or server issues, +// with retries built-in at every layer of the HTTP request stack. +// +// NOTE: This is a low-level API. Most users will want the mholt/acmez +// package which is more concerned with configuring challenges and +// implementing the order flow. However, using this package directly +// is recommended for advanced use cases having niche requirements. +// See the examples in the examples/plumbing folder for a tutorial. +package acme + +import ( + "context" + "fmt" + "net/http" + "sync" + "time" + + "go.uber.org/zap" +) + +// Client facilitates ACME client operations as defined by the spec. +// +// Because the client is synchronized for concurrent use, it should +// not be copied. +// +// Many errors that are returned by a Client are likely to be of type +// Problem as long as the ACME server returns a structured error +// response. This package wraps errors that may be of type Problem, +// so you can access the details using the conventional Go pattern: +// +// var problem Problem +// if errors.As(err, &problem) { +// log.Printf("Houston, we have a problem: %+v", problem) +// } +// +// All Problem errors originate from the ACME server. +type Client struct { + // The ACME server's directory endpoint. + Directory string + + // Custom HTTP client. + HTTPClient *http.Client + + // Augmentation of the User-Agent header. Please set + // this so that CAs can troubleshoot bugs more easily. + UserAgent string + + // Delay between poll attempts. Only used if server + // does not supply a Retry-Afer header. Default: 250ms + PollInterval time.Duration + + // Maximum duration for polling. Default: 5m + PollTimeout time.Duration + + // An optional logger. Default: no logs + Logger *zap.Logger + + mu sync.Mutex // protects all unexported fields + dir Directory + nonces *stack +} + +// GetDirectory retrieves the directory configured at c.Directory. It is +// NOT necessary to call this to provision the client. It is only useful +// if you want to access a copy of the directory yourself. +func (c *Client) GetDirectory(ctx context.Context) (Directory, error) { + if err := c.provision(ctx); err != nil { + return Directory{}, err + } + return c.dir, nil +} + +func (c *Client) provision(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.nonces == nil { + c.nonces = new(stack) + } + + err := c.provisionDirectory(ctx) + if err != nil { + return fmt.Errorf("provisioning client: %w", err) + } + + return nil +} + +func (c *Client) provisionDirectory(ctx context.Context) error { + // don't get directory again if we already have it; + // checking any one of the required fields will do + if c.dir.NewNonce != "" { + return nil + } + if c.Directory == "" { + return fmt.Errorf("missing directory URL") + } + // prefer cached version if it's recent enough + directoriesMu.Lock() + defer directoriesMu.Unlock() + if dir, ok := directories[c.Directory]; ok { + if time.Since(dir.retrieved) < 12*time.Hour { + c.dir = dir.Directory + return nil + } + } + _, err := c.httpReq(ctx, http.MethodGet, c.Directory, nil, &c.dir) + if err != nil { + return err + } + directories[c.Directory] = cachedDirectory{c.dir, time.Now()} + return nil +} + +func (c *Client) nonce(ctx context.Context) (string, error) { + nonce := c.nonces.pop() + if nonce != "" { + return nonce, nil + } + + if c.dir.NewNonce == "" { + return "", fmt.Errorf("directory missing newNonce endpoint") + } + + resp, err := c.httpReq(ctx, http.MethodHead, c.dir.NewNonce, nil, nil) + if err != nil { + return "", fmt.Errorf("fetching new nonce from server: %w", err) + } + + return resp.Header.Get(replayNonce), nil +} + +func (c *Client) pollInterval() time.Duration { + if c.PollInterval == 0 { + return defaultPollInterval + } + return c.PollInterval +} + +func (c *Client) pollTimeout() time.Duration { + if c.PollTimeout == 0 { + return defaultPollTimeout + } + return c.PollTimeout +} + +// Directory acts as an index for the ACME server as +// specified in the spec: "In order to help clients +// configure themselves with the right URLs for each +// ACME operation, ACME servers provide a directory +// object." §7.1.1 +type Directory struct { + NewNonce string `json:"newNonce"` + NewAccount string `json:"newAccount"` + NewOrder string `json:"newOrder"` + NewAuthz string `json:"newAuthz,omitempty"` + RevokeCert string `json:"revokeCert"` + KeyChange string `json:"keyChange"` + Meta *DirectoryMeta `json:"meta,omitempty"` +} + +// DirectoryMeta is optional extra data that may be +// included in an ACME server directory. §7.1.1 +type DirectoryMeta struct { + TermsOfService string `json:"termsOfService,omitempty"` + Website string `json:"website,omitempty"` + CAAIdentities []string `json:"caaIdentities,omitempty"` + ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"` +} + +// stack is a simple thread-safe stack. +type stack struct { + stack []string + stackMu sync.Mutex +} + +func (s *stack) push(v string) { + if v == "" { + return + } + s.stackMu.Lock() + defer s.stackMu.Unlock() + if len(s.stack) >= 64 { + return + } + s.stack = append(s.stack, v) +} + +func (s *stack) pop() string { + s.stackMu.Lock() + defer s.stackMu.Unlock() + n := len(s.stack) + if n == 0 { + return "" + } + v := s.stack[n-1] + s.stack = s.stack[:n-1] + return v +} + +// Directories seldom (if ever) change in practice, and +// client structs are often ephemeral, so we can cache +// directories to speed things up a bit for the user. +// Keyed by directory URL. +var ( + directories = make(map[string]cachedDirectory) + directoriesMu sync.Mutex +) + +type cachedDirectory struct { + Directory + retrieved time.Time +} + +// replayNonce is the header field that contains a new +// anti-replay nonce from the server. +const replayNonce = "Replay-Nonce" + +const ( + defaultPollInterval = 250 * time.Millisecond + defaultPollTimeout = 5 * time.Minute +) diff --git a/vendor/github.com/mholt/acmez/acme/http.go b/vendor/github.com/mholt/acmez/acme/http.go new file mode 100644 index 0000000000..83127579e1 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/http.go @@ -0,0 +1,394 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acme + +import ( + "bytes" + "context" + "crypto" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "go.uber.org/zap" +) + +// httpPostJWS performs robust HTTP requests by JWS-encoding the JSON of input. +// If output is specified, the response body is written into it: if the response +// Content-Type is JSON, it will be JSON-decoded into output (which must be a +// pointer); otherwise, if output is an io.Writer, the response body will be +// written to it uninterpreted. In all cases, the returned response value's +// body will have been drained and closed, so there is no need to close it again. +// It automatically retries in the case of network, I/O, or badNonce errors. +func (c *Client) httpPostJWS(ctx context.Context, privateKey crypto.Signer, + kid, endpoint string, input, output interface{}) (*http.Response, error) { + + if err := c.provision(ctx); err != nil { + return nil, err + } + + var resp *http.Response + var err error + + // we can retry on internal server errors just in case it was a hiccup, + // but we probably don't need to retry so many times in that case + internalServerErrors, maxInternalServerErrors := 0, 3 + + // set a hard cap on the number of retries for any other reason + const maxAttempts = 10 + var attempts int + for attempts = 1; attempts <= maxAttempts; attempts++ { + if attempts > 1 { + select { + case <-time.After(250 * time.Millisecond): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + + var nonce string // avoid shadowing err + nonce, err = c.nonce(ctx) + if err != nil { + return nil, err + } + + var encodedPayload []byte // avoid shadowing err + encodedPayload, err = jwsEncodeJSON(input, privateKey, keyID(kid), nonce, endpoint) + if err != nil { + return nil, fmt.Errorf("encoding payload: %v", err) + } + + resp, err = c.httpReq(ctx, http.MethodPost, endpoint, encodedPayload, output) + if err == nil { + return resp, nil + } + + // "When a server rejects a request because its nonce value was + // unacceptable (or not present), it MUST provide HTTP status code 400 + // (Bad Request), and indicate the ACME error type + // 'urn:ietf:params:acme:error:badNonce'. An error response with the + // 'badNonce' error type MUST include a Replay-Nonce header field with a + // fresh nonce that the server will accept in a retry of the original + // query (and possibly in other requests, according to the server's + // nonce scoping policy). On receiving such a response, a client SHOULD + // retry the request using the new nonce." §6.5 + var problem Problem + if errors.As(err, &problem) { + if problem.Type == ProblemTypeBadNonce { + if c.Logger != nil { + c.Logger.Debug("server rejected our nonce; retrying", + zap.String("detail", problem.Detail), + zap.Error(err)) + } + continue + } + } + + // internal server errors *could* just be a hiccup and it may be worth + // trying again, but not nearly so many times as for other reasons + if resp != nil && resp.StatusCode >= 500 { + internalServerErrors++ + if internalServerErrors < maxInternalServerErrors { + continue + } + } + + // for any other error, there's not much we can do automatically + break + } + + return resp, fmt.Errorf("request to %s failed after %d attempts: %v", + endpoint, attempts, err) +} + +// httpReq robustly performs an HTTP request using the given method to the given endpoint, honoring +// the given context's cancellation. The joseJSONPayload is optional; if not nil, it is expected to +// be a JOSE+JSON encoding. The output is also optional; if not nil, the response body will be read +// into output. If the response Content-Type is JSON, it will be JSON-decoded into output, which +// must be a pointer type. If the response is any other Content-Type and if output is a io.Writer, +// it will be written (without interpretation or decoding) to output. In all cases, the returned +// response value will have the body drained and closed, so there is no need to close it again. +// +// If there are any network or I/O errors, the request will be retried as safely and resiliently as +// possible. +func (c *Client) httpReq(ctx context.Context, method, endpoint string, joseJSONPayload []byte, output interface{}) (*http.Response, error) { + // even if the caller doesn't specify an output, we still use a + // buffer to store possible error response (we reset it later) + buf := bufPool.Get().(*bytes.Buffer) + defer bufPool.Put(buf) + + var resp *http.Response + var err error + + // potentially retry the request if there's network, I/O, or server internal errors + const maxAttempts = 3 + for attempt := 0; attempt < maxAttempts; attempt++ { + if attempt > 0 { + // traffic calming ahead + select { + case <-time.After(250 * time.Millisecond): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + + var body io.Reader + if joseJSONPayload != nil { + body = bytes.NewReader(joseJSONPayload) + } + + var req *http.Request + req, err = http.NewRequestWithContext(ctx, method, endpoint, body) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + if len(joseJSONPayload) > 0 { + req.Header.Set("Content-Type", "application/jose+json") + } + + // on first attempt, we need to reset buf since it + // came from the pool; after first attempt, we should + // still reset it because we might be retrying after + // a partial download + buf.Reset() + + var retry bool + resp, retry, err = c.doHTTPRequest(req, buf) + if err != nil { + if retry { + if c.Logger != nil { + c.Logger.Warn("HTTP request failed; retrying", + zap.String("url", req.URL.String()), + zap.Error(err)) + } + continue + } + break + } + + // check for HTTP errors + switch { + case resp.StatusCode >= 200 && resp.StatusCode < 300: // OK + case resp.StatusCode >= 400 && resp.StatusCode < 600: // error + if parseMediaType(resp) == "application/problem+json" { + // "When the server responds with an error status, it SHOULD provide + // additional information using a problem document [RFC7807]." (§6.7) + var problem Problem + err = json.Unmarshal(buf.Bytes(), &problem) + if err != nil { + return resp, fmt.Errorf("HTTP %d: JSON-decoding problem details: %w (raw='%s')", + resp.StatusCode, err, buf.String()) + } + if resp.StatusCode >= 500 && joseJSONPayload == nil { + // a 5xx status is probably safe to retry on even after a + // request that had no I/O errors; it could be that the + // server just had a hiccup... so try again, but only if + // there is no request body, because we can't replay a + // request that has an anti-replay nonce, obviously + err = problem + continue + } + return resp, problem + } + return resp, fmt.Errorf("HTTP %d: %s", resp.StatusCode, buf.String()) + default: // what even is this + return resp, fmt.Errorf("unexpected status code: HTTP %d", resp.StatusCode) + } + + // do not retry if we got this far (success) + break + } + if err != nil { + return resp, err + } + + // if expecting a body, finally decode it + if output != nil { + contentType := parseMediaType(resp) + switch contentType { + case "application/json": + // unmarshal JSON + err = json.Unmarshal(buf.Bytes(), output) + if err != nil { + return resp, fmt.Errorf("JSON-decoding response body: %w", err) + } + + default: + // don't interpret anything else here; just hope + // it's a Writer and copy the bytes + w, ok := output.(io.Writer) + if !ok { + return resp, fmt.Errorf("response Content-Type is %s but target container is not io.Writer: %T", contentType, output) + } + _, err = io.Copy(w, buf) + if err != nil { + return resp, err + } + } + } + + return resp, nil +} + +// doHTTPRequest performs an HTTP request at most one time. It returns the response +// (with drained and closed body), having drained any request body into buf. If +// retry == true is returned, then the request should be safe to retry in the case +// of an error. However, in some cases a retry may be recommended even if part of +// the response body has been read and written into buf. Thus, the buffer may have +// been partially written to and should be reset before being reused. +// +// This method remembers any nonce returned by the server. +func (c *Client) doHTTPRequest(req *http.Request, buf *bytes.Buffer) (resp *http.Response, retry bool, err error) { + req.Header.Set("User-Agent", c.userAgent()) + + resp, err = c.httpClient().Do(req) + if err != nil { + return resp, true, fmt.Errorf("performing request: %w", err) + } + defer resp.Body.Close() + + if c.Logger != nil { + c.Logger.Debug("http request", + zap.String("method", req.Method), + zap.String("url", req.URL.String()), + zap.Reflect("headers", req.Header), + zap.Int("status_code", resp.StatusCode), + zap.Reflect("response_headers", resp.Header)) + } + + // "The server MUST include a Replay-Nonce header field + // in every successful response to a POST request and + // SHOULD provide it in error responses as well." §6.5 + // + // "Before sending a POST request to the server, an ACME + // client needs to have a fresh anti-replay nonce to put + // in the 'nonce' header of the JWS. In most cases, the + // client will have gotten a nonce from a previous + // request." §7.2 + // + // So basically, we need to remember the nonces we get + // and use them at the next opportunity. + c.nonces.push(resp.Header.Get(replayNonce)) + + // drain the response body, even if we aren't keeping it + // (this allows us to reuse the connection and also read + // any error information) + _, err = io.Copy(buf, resp.Body) + if err != nil { + // this is likely a network or I/O error, but is it worth retrying? + // technically the request has already completed, it was just our + // download of the response that failed; so we probably should not + // retry if the request succeeded... however, if there was an HTTP + // error, it likely didn't count against any server-enforced rate + // limits, and we DO want to know the error information, so it should + // be safe to retry the request in those cases AS LONG AS there is + // no request body, which in the context of ACME likely includes an + // anti-replay nonce, which obviously we can't reuse + retry = resp.StatusCode >= 400 && req.Body == nil + return resp, retry, fmt.Errorf("reading response body: %w", err) + } + + return resp, false, nil +} + +func (c *Client) httpClient() *http.Client { + if c.HTTPClient == nil { + return http.DefaultClient + } + return c.HTTPClient +} + +func (c *Client) userAgent() string { + ua := fmt.Sprintf("acmez (%s; %s)", runtime.GOOS, runtime.GOARCH) + if c.UserAgent != "" { + ua = c.UserAgent + " " + ua + } + return ua +} + +// extractLinks extracts the URL from the Link header with the +// designated relation rel. It may return more than value +// if there are multiple matching Link values. +// +// Originally by Isaac: https://github.com/eggsampler/acme +// and has been modified to support multiple matching Links. +func extractLinks(resp *http.Response, rel string) []string { + if resp == nil { + return nil + } + var links []string + for _, l := range resp.Header["Link"] { + matches := linkRegex.FindAllStringSubmatch(l, -1) + for _, m := range matches { + if len(m) != 3 { + continue + } + if m[2] == rel { + links = append(links, m[1]) + } + } + } + return links +} + +// parseMediaType returns only the media type from the +// Content-Type header of resp. +func parseMediaType(resp *http.Response) string { + if resp == nil { + return "" + } + ct := resp.Header.Get("Content-Type") + sep := strings.Index(ct, ";") + if sep < 0 { + return ct + } + return strings.TrimSpace(ct[:sep]) +} + +// retryAfter returns a duration from the response's Retry-After +// header field, if it exists. It can return an error if the +// header contains an invalid value. If there is no error but +// there is no Retry-After header provided, then the fallback +// duration is returned instead. +func retryAfter(resp *http.Response, fallback time.Duration) (time.Duration, error) { + if resp == nil { + return fallback, nil + } + raSeconds := resp.Header.Get("Retry-After") + if raSeconds == "" { + return fallback, nil + } + ra, err := strconv.Atoi(raSeconds) + if err != nil || ra < 0 { + return 0, fmt.Errorf("response had invalid Retry-After header: %s", raSeconds) + } + return time.Duration(ra) * time.Second, nil +} + +var bufPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +var linkRegex = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`) diff --git a/vendor/github.com/mholt/acmez/acme/jws.go b/vendor/github.com/mholt/acmez/acme/jws.go new file mode 100644 index 0000000000..bdbb4573eb --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/jws.go @@ -0,0 +1,263 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// --- ORIGINAL LICENSE --- +// +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the THIRD-PARTY file. +// +// (This file has been modified from its original contents.) +// (And it has dragons. Don't wake the dragons.) + +package acme + +import ( + "crypto" + "crypto/ecdsa" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + _ "crypto/sha512" // need for EC keys + "encoding/base64" + "encoding/json" + "fmt" + "math/big" +) + +var errUnsupportedKey = fmt.Errorf("unknown key type; only RSA and ECDSA are supported") + +// keyID is the account identity provided by a CA during registration. +type keyID string + +// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID. +// See jwsEncodeJSON for details. +const noKeyID = keyID("") + +// // noPayload indicates jwsEncodeJSON will encode zero-length octet string +// // in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make +// // authenticated GET requests via POSTing with an empty payload. +// // See https://tools.ietf.org/html/rfc8555#section-6.3 for more details. +// const noPayload = "" + +// jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4. +func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url string) ([]byte, error) { + // §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm" + alg, sha := "HS256", crypto.SHA256 + + // §7.3.4: "The 'nonce' field MUST NOT be present" + phead, err := jwsHead(alg, "", url, kid, nil) + if err != nil { + return nil, err + } + + encodedKey, err := jwkEncode(accountKey) + if err != nil { + return nil, err + } + payload := base64.RawURLEncoding.EncodeToString([]byte(encodedKey)) + + payloadToSign := []byte(phead + "." + payload) + + h := hmac.New(sha256.New, hmacKey) + h.Write(payloadToSign) + sig := h.Sum(nil) + + return jwsFinal(sha, sig, phead, payload) +} + +// jwsEncodeJSON signs claimset using provided key and a nonce. +// The result is serialized in JSON format containing either kid or jwk +// fields based on the provided keyID value. +// +// If kid is non-empty, its quoted value is inserted in the protected head +// as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted +// as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive. +// +// See https://tools.ietf.org/html/rfc7515#section-7. +// +// If nonce is empty, it will not be encoded into the header. +func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) { + alg, sha := jwsHasher(key.Public()) + if alg == "" || !sha.Available() { + return nil, errUnsupportedKey + } + + phead, err := jwsHead(alg, nonce, url, kid, key) + if err != nil { + return nil, err + } + + var payload string + if claimset != nil { + cs, err := json.Marshal(claimset) + if err != nil { + return nil, err + } + payload = base64.RawURLEncoding.EncodeToString(cs) + } + + payloadToSign := []byte(phead + "." + payload) + hash := sha.New() + _, _ = hash.Write(payloadToSign) + digest := hash.Sum(nil) + + sig, err := jwsSign(key, sha, digest) + if err != nil { + return nil, err + } + + return jwsFinal(sha, sig, phead, payload) +} + +// jwkEncode encodes public part of an RSA or ECDSA key into a JWK. +// The result is also suitable for creating a JWK thumbprint. +// https://tools.ietf.org/html/rfc7517 +func jwkEncode(pub crypto.PublicKey) (string, error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + n := pub.N + e := big.NewInt(int64(pub.E)) + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, + base64.RawURLEncoding.EncodeToString(e.Bytes()), + base64.RawURLEncoding.EncodeToString(n.Bytes()), + ), nil + case *ecdsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.2.1 + p := pub.Curve.Params() + n := p.BitSize / 8 + if p.BitSize%8 != 0 { + n++ + } + x := pub.X.Bytes() + if n > len(x) { + x = append(make([]byte, n-len(x)), x...) + } + y := pub.Y.Bytes() + if n > len(y) { + y = append(make([]byte, n-len(y)), y...) + } + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, + p.Name, + base64.RawURLEncoding.EncodeToString(x), + base64.RawURLEncoding.EncodeToString(y), + ), nil + } + return "", errUnsupportedKey +} + +// jwsHead constructs the protected JWS header for the given fields. +// Since jwk and kid are mutually-exclusive, the jwk will be encoded +// only if kid is empty. If nonce is empty, it will not be encoded. +func jwsHead(alg, nonce, url string, kid keyID, key crypto.Signer) (string, error) { + phead := fmt.Sprintf(`{"alg":%q`, alg) + if kid == noKeyID { + jwk, err := jwkEncode(key.Public()) + if err != nil { + return "", err + } + phead += fmt.Sprintf(`,"jwk":%s`, jwk) + } else { + phead += fmt.Sprintf(`,"kid":%q`, kid) + } + if nonce != "" { + phead += fmt.Sprintf(`,"nonce":%q`, nonce) + } + phead += fmt.Sprintf(`,"url":%q}`, url) + phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) + return phead, nil +} + +// jwsFinal constructs the final JWS object. +func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) { + enc := struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` + }{ + Protected: phead, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(sig), + } + result, err := json.Marshal(&enc) + if err != nil { + return nil, err + } + return result, nil +} + +// jwsSign signs the digest using the given key. +// The hash is unused for ECDSA keys. +// +// Note: non-stdlib crypto.Signer implementations are expected to return +// the signature in the format as specified in RFC7518. +// See https://tools.ietf.org/html/rfc7518 for more details. +func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) { + if key, ok := key.(*ecdsa.PrivateKey); ok { + // The key.Sign method of ecdsa returns ASN1-encoded signature. + // So, we use the package Sign function instead + // to get R and S values directly and format the result accordingly. + r, s, err := ecdsa.Sign(rand.Reader, key, digest) + if err != nil { + return nil, err + } + rb, sb := r.Bytes(), s.Bytes() + size := key.Params().BitSize / 8 + if size%8 > 0 { + size++ + } + sig := make([]byte, size*2) + copy(sig[size-len(rb):], rb) + copy(sig[size*2-len(sb):], sb) + return sig, nil + } + return key.Sign(rand.Reader, digest, hash) +} + +// jwsHasher indicates suitable JWS algorithm name and a hash function +// to use for signing a digest with the provided key. +// It returns ("", 0) if the key is not supported. +func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) { + switch pub := pub.(type) { + case *rsa.PublicKey: + return "RS256", crypto.SHA256 + case *ecdsa.PublicKey: + switch pub.Params().Name { + case "P-256": + return "ES256", crypto.SHA256 + case "P-384": + return "ES384", crypto.SHA384 + case "P-521": + return "ES512", crypto.SHA512 + } + } + return "", 0 +} + +// jwkThumbprint creates a JWK thumbprint out of pub +// as specified in https://tools.ietf.org/html/rfc7638. +func jwkThumbprint(pub crypto.PublicKey) (string, error) { + jwk, err := jwkEncode(pub) + if err != nil { + return "", err + } + b := sha256.Sum256([]byte(jwk)) + return base64.RawURLEncoding.EncodeToString(b[:]), nil +} diff --git a/vendor/github.com/mholt/acmez/acme/order.go b/vendor/github.com/mholt/acmez/acme/order.go new file mode 100644 index 0000000000..579bb3a47b --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/order.go @@ -0,0 +1,247 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acme + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "time" +) + +// Order is an object that "represents a client's request for a certificate +// and is used to track the progress of that order through to issuance. +// Thus, the object contains information about the requested +// certificate, the authorizations that the server requires the client +// to complete, and any certificates that have resulted from this order." +// §7.1.3 +type Order struct { + // status (required, string): The status of this order. Possible + // values are "pending", "ready", "processing", "valid", and + // "invalid". See Section 7.1.6. + Status string `json:"status"` + + // expires (optional, string): The timestamp after which the server + // will consider this order invalid, encoded in the format specified + // in [RFC3339]. This field is REQUIRED for objects with "pending" + // or "valid" in the status field. + Expires time.Time `json:"expires,omitempty"` + + // identifiers (required, array of object): An array of identifier + // objects that the order pertains to. + Identifiers []Identifier `json:"identifiers"` + + // notBefore (optional, string): The requested value of the notBefore + // field in the certificate, in the date format defined in [RFC3339]. + NotBefore *time.Time `json:"notBefore,omitempty"` + + // notAfter (optional, string): The requested value of the notAfter + // field in the certificate, in the date format defined in [RFC3339]. + NotAfter *time.Time `json:"notAfter,omitempty"` + + // error (optional, object): The error that occurred while processing + // the order, if any. This field is structured as a problem document + // [RFC7807]. + Error *Problem `json:"error,omitempty"` + + // authorizations (required, array of string): For pending orders, the + // authorizations that the client needs to complete before the + // requested certificate can be issued (see Section 7.5), including + // unexpired authorizations that the client has completed in the past + // for identifiers specified in the order. The authorizations + // required are dictated by server policy; there may not be a 1:1 + // relationship between the order identifiers and the authorizations + // required. For final orders (in the "valid" or "invalid" state), + // the authorizations that were completed. Each entry is a URL from + // which an authorization can be fetched with a POST-as-GET request. + Authorizations []string `json:"authorizations"` + + // finalize (required, string): A URL that a CSR must be POSTed to once + // all of the order's authorizations are satisfied to finalize the + // order. The result of a successful finalization will be the + // population of the certificate URL for the order. + Finalize string `json:"finalize"` + + // certificate (optional, string): A URL for the certificate that has + // been issued in response to this order. + Certificate string `json:"certificate"` + + // Similar to new-account, the server returns a 201 response with + // the URL to the order object in the Location header. + // + // We transfer the value from the header to this field for + // storage and recall purposes. + Location string `json:"-"` +} + +// Identifier is used in order and authorization (authz) objects. +type Identifier struct { + // type (required, string): The type of identifier. This document + // defines the "dns" identifier type. See the registry defined in + // Section 9.7.7 for any others. + Type string `json:"type"` + + // value (required, string): The identifier itself. + Value string `json:"value"` +} + +// NewOrder creates a new order with the server. +// +// "The client begins the certificate issuance process by sending a POST +// request to the server's newOrder resource." §7.4 +func (c *Client) NewOrder(ctx context.Context, account Account, order Order) (Order, error) { + if err := c.provision(ctx); err != nil { + return order, err + } + resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.NewOrder, order, &order) + if err != nil { + return order, err + } + order.Location = resp.Header.Get("Location") + return order, nil +} + +// FinalizeOrder finalizes the order with the server and polls under the server has +// updated the order status. The CSR must be in ASN.1 DER-encoded format. If this +// succeeds, the certificate is ready to download once this returns. +// +// "Once the client believes it has fulfilled the server's requirements, +// it should send a POST request to the order resource's finalize URL." §7.4 +func (c *Client) FinalizeOrder(ctx context.Context, account Account, order Order, csrASN1DER []byte) (Order, error) { + if err := c.provision(ctx); err != nil { + return order, err + } + + body := struct { + // csr (required, string): A CSR encoding the parameters for the + // certificate being requested [RFC2986]. The CSR is sent in the + // base64url-encoded version of the DER format. (Note: Because this + // field uses base64url, and does not include headers, it is + // different from PEM.) §7.4 + CSR string `json:"csr"` + }{ + CSR: base64.RawURLEncoding.EncodeToString(csrASN1DER), + } + + resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Finalize, body, &order) + if err != nil { + // "A request to finalize an order will result in error if the order is + // not in the 'ready' state. In such cases, the server MUST return a + // 403 (Forbidden) error with a problem document of type + // 'orderNotReady'. The client should then send a POST-as-GET request + // to the order resource to obtain its current state. The status of the + // order will indicate what action the client should take (see below)." + // §7.4 + var problem Problem + if errors.As(err, &problem) { + if problem.Type != ProblemTypeOrderNotReady { + return order, err + } + } else { + return order, err + } + } + + // unlike with accounts and authorizations, the spec isn't clear on whether + // the server MUST set this on finalizing the order, but their example shows a + // Location header, so I guess if it's set in the response, we should keep it + if newLocation := resp.Header.Get("Location"); newLocation != "" { + order.Location = newLocation + } + + if finished, err := orderIsFinished(order); finished { + return order, err + } + + // TODO: "The elements of the "authorizations" and "identifiers" arrays are + // immutable once set. If a client observes a change + // in the contents of either array, then it SHOULD consider the order + // invalid." + + maxDuration := c.pollTimeout() + start := time.Now() + for time.Since(start) < maxDuration { + // querying an order is expensive on the server-side, so we + // shouldn't do it too frequently; honor server preference + interval, err := retryAfter(resp, c.pollInterval()) + if err != nil { + return order, err + } + select { + case <-time.After(interval): + case <-ctx.Done(): + return order, ctx.Err() + } + + resp, err = c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Location, nil, &order) + if err != nil { + return order, fmt.Errorf("polling order status: %w", err) + } + + // (same reasoning as above) + if newLocation := resp.Header.Get("Location"); newLocation != "" { + order.Location = newLocation + } + + if finished, err := orderIsFinished(order); finished { + return order, err + } + } + + return order, fmt.Errorf("order took too long") +} + +// orderIsFinished returns true if the order processing is complete, +// regardless of success or failure. If this function returns true, +// polling an order status should stop. If there is an error with the +// order, an error will be returned. This function should be called +// only after a request to finalize an order. See §7.4. +func orderIsFinished(order Order) (bool, error) { + switch order.Status { + case StatusInvalid: + // "invalid": The certificate will not be issued. Consider this + // order process abandoned. + return true, fmt.Errorf("final order is invalid: %w", order.Error) + + case StatusPending: + // "pending": The server does not believe that the client has + // fulfilled the requirements. Check the "authorizations" array for + // entries that are still pending. + return true, fmt.Errorf("order pending, authorizations remaining: %v", order.Authorizations) + + case StatusReady: + // "ready": The server agrees that the requirements have been + // fulfilled, and is awaiting finalization. Submit a finalization + // request. + // (we did just submit a finalization request, so this is an error) + return true, fmt.Errorf("unexpected state: %s - order already finalized", order.Status) + + case StatusProcessing: + // "processing": The certificate is being issued. Send a GET request + // after the time given in the "Retry-After" header field of the + // response, if any. + return false, nil + + case StatusValid: + // "valid": The server has issued the certificate and provisioned its + // URL to the "certificate" field of the order. Download the + // certificate. + return true, nil + + default: + return true, fmt.Errorf("unrecognized order status: %s", order.Status) + } +} diff --git a/vendor/github.com/mholt/acmez/acme/problem.go b/vendor/github.com/mholt/acmez/acme/problem.go new file mode 100644 index 0000000000..98fdb00958 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/problem.go @@ -0,0 +1,136 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acme + +import "fmt" + +// Problem carries the details of an error from HTTP APIs as +// defined in RFC 7807: https://tools.ietf.org/html/rfc7807 +// and as extended by RFC 8555 §6.7: +// https://tools.ietf.org/html/rfc8555#section-6.7 +type Problem struct { + // "type" (string) - A URI reference [RFC3986] that identifies the + // problem type. This specification encourages that, when + // dereferenced, it provide human-readable documentation for the + // problem type (e.g., using HTML [W3C.REC-html5-20141028]). When + // this member is not present, its value is assumed to be + // "about:blank". §3.1 + Type string `json:"type"` + + // "title" (string) - A short, human-readable summary of the problem + // type. It SHOULD NOT change from occurrence to occurrence of the + // problem, except for purposes of localization (e.g., using + // proactive content negotiation; see [RFC7231], Section 3.4). §3.1 + Title string `json:"title,omitempty"` + + // "status" (number) - The HTTP status code ([RFC7231], Section 6) + // generated by the origin server for this occurrence of the problem. + // §3.1 + Status int `json:"status,omitempty"` + + // "detail" (string) - A human-readable explanation specific to this + // occurrence of the problem. §3.1 + // + // RFC 8555 §6.7: "Clients SHOULD display the 'detail' field of all + // errors." + Detail string `json:"detail,omitempty"` + + // "instance" (string) - A URI reference that identifies the specific + // occurrence of the problem. It may or may not yield further + // information if dereferenced. §3.1 + Instance string `json:"instance,omitempty"` + + // "Sometimes a CA may need to return multiple errors in response to a + // request. Additionally, the CA may need to attribute errors to + // specific identifiers. For instance, a newOrder request may contain + // multiple identifiers for which the CA cannot issue certificates. In + // this situation, an ACME problem document MAY contain the + // 'subproblems' field, containing a JSON array of problem documents." + // RFC 8555 §6.7.1 + Subproblems []Subproblem `json:"subproblems,omitempty"` + + // For convenience, we've added this field to associate with a value + // that is related to or caused the problem. It is not part of the + // spec, but, if a challenge fails for example, we can associate the + // error with the problematic authz object by setting this field. + // Challenge failures will have this set to an Authorization type. + Resource interface{} `json:"-"` +} + +func (p Problem) Error() string { + // TODO: 7.3.3: Handle changes to Terms of Service (notice it uses the Instance field and Link header) + + // RFC 8555 §6.7: "Clients SHOULD display the 'detail' field of all errors." + s := fmt.Sprintf("HTTP %d %s - %s", p.Status, p.Type, p.Detail) + if len(p.Subproblems) > 0 { + for _, v := range p.Subproblems { + s += fmt.Sprintf(", problem %q: %s", v.Type, v.Detail) + } + } + if p.Instance != "" { + s += ", url: " + p.Instance + } + return s +} + +// Subproblem describes a more specific error in a problem according to +// RFC 8555 §6.7.1: "An ACME problem document MAY contain the +// 'subproblems' field, containing a JSON array of problem documents, +// each of which MAY contain an 'identifier' field." +type Subproblem struct { + Problem + + // "If present, the 'identifier' field MUST contain an ACME + // identifier (Section 9.7.7)." §6.7.1 + Identifier Identifier `json:"identifier,omitempty"` +} + +// Standard token values for the "type" field of problems, as defined +// in RFC 8555 §6.7: https://tools.ietf.org/html/rfc8555#section-6.7 +// +// "To facilitate automatic response to errors, this document defines the +// following standard tokens for use in the 'type' field (within the +// ACME URN namespace 'urn:ietf:params:acme:error:') ... This list is not +// exhaustive. The server MAY return errors whose 'type' field is set to +// a URI other than those defined above." +const ( + // The ACME error URN prefix. + ProblemTypeNamespace = "urn:ietf:params:acme:error:" + + ProblemTypeAccountDoesNotExist = ProblemTypeNamespace + "accountDoesNotExist" + ProblemTypeAlreadyRevoked = ProblemTypeNamespace + "alreadyRevoked" + ProblemTypeBadCSR = ProblemTypeNamespace + "badCSR" + ProblemTypeBadNonce = ProblemTypeNamespace + "badNonce" + ProblemTypeBadPublicKey = ProblemTypeNamespace + "badPublicKey" + ProblemTypeBadRevocationReason = ProblemTypeNamespace + "badRevocationReason" + ProblemTypeBadSignatureAlgorithm = ProblemTypeNamespace + "badSignatureAlgorithm" + ProblemTypeCAA = ProblemTypeNamespace + "caa" + ProblemTypeCompound = ProblemTypeNamespace + "compound" + ProblemTypeConnection = ProblemTypeNamespace + "connection" + ProblemTypeDNS = ProblemTypeNamespace + "dns" + ProblemTypeExternalAccountRequired = ProblemTypeNamespace + "externalAccountRequired" + ProblemTypeIncorrectResponse = ProblemTypeNamespace + "incorrectResponse" + ProblemTypeInvalidContact = ProblemTypeNamespace + "invalidContact" + ProblemTypeMalformed = ProblemTypeNamespace + "malformed" + ProblemTypeOrderNotReady = ProblemTypeNamespace + "orderNotReady" + ProblemTypeRateLimited = ProblemTypeNamespace + "rateLimited" + ProblemTypeRejectedIdentifier = ProblemTypeNamespace + "rejectedIdentifier" + ProblemTypeServerInternal = ProblemTypeNamespace + "serverInternal" + ProblemTypeTLS = ProblemTypeNamespace + "tls" + ProblemTypeUnauthorized = ProblemTypeNamespace + "unauthorized" + ProblemTypeUnsupportedContact = ProblemTypeNamespace + "unsupportedContact" + ProblemTypeUnsupportedIdentifier = ProblemTypeNamespace + "unsupportedIdentifier" + ProblemTypeUserActionRequired = ProblemTypeNamespace + "userActionRequired" +) diff --git a/vendor/github.com/mholt/acmez/client.go b/vendor/github.com/mholt/acmez/client.go new file mode 100644 index 0000000000..4cad9c5e57 --- /dev/null +++ b/vendor/github.com/mholt/acmez/client.go @@ -0,0 +1,656 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package acmez implements the higher-level flow of the ACME specification, +// RFC 8555: https://tools.ietf.org/html/rfc8555, specifically the sequence +// in Section 7.1 (page 21). +// +// It makes it easy to obtain certificates with various challenge types +// using pluggable challenge solvers, and provides some handy utilities for +// implementing solvers and using the certificates. It DOES NOT manage +// certificates, it only gets them from the ACME server. +// +// NOTE: This package's main function is to get a certificate, not manage it. +// Most users will want to *manage* certificates over the lifetime of a +// long-running program such as a HTTPS or TLS server, and should use CertMagic +// instead: https://github.com/caddyserver/certmagic. +package acmez + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/x509" + "errors" + "fmt" + weakrand "math/rand" + "net" + "net/url" + "sort" + "strings" + "sync" + "time" + + "github.com/mholt/acmez/acme" + "go.uber.org/zap" + "golang.org/x/net/idna" +) + +func init() { + weakrand.Seed(time.Now().UnixNano()) +} + +// Client is a high-level API for ACME operations. It wraps +// a lower-level ACME client with useful functions to make +// common flows easier, especially for the issuance of +// certificates. +type Client struct { + *acme.Client + + // Map of solvers keyed by name of the challenge type. + ChallengeSolvers map[string]Solver + + // An optional logger. Default: no logs + Logger *zap.Logger +} + +// ObtainCertificateUsingCSR obtains all resulting certificate chains using the given CSR, which +// must be completely and properly filled out (particularly its DNSNames and Raw fields - this +// usually involves creating a template CSR, then calling x509.CreateCertificateRequest, then +// x509.ParseCertificateRequest on the output). The Subject CommonName is NOT considered. +// +// It implements every single part of the ACME flow described in RFC 8555 §7.1 with the exception +// of "Create account" because this method signature does not have a way to return the udpated +// account object. The account's status MUST be "valid" in order to succeed. +// +// As far as SANs go, this method currently only supports DNSNames on the csr. +func (c *Client) ObtainCertificateUsingCSR(ctx context.Context, account acme.Account, csr *x509.CertificateRequest) ([]acme.Certificate, error) { + if account.Status != acme.StatusValid { + return nil, fmt.Errorf("account status is not valid: %s", account.Status) + } + if csr == nil { + return nil, fmt.Errorf("missing CSR") + } + + var ids []acme.Identifier + for _, name := range csr.DNSNames { + // "The domain name MUST be encoded in the form in which it would appear + // in a certificate. That is, it MUST be encoded according to the rules + // in Section 7 of [RFC5280]." §7.1.4 + normalizedName, err := idna.ToASCII(name) + if err != nil { + return nil, fmt.Errorf("converting identifier '%s' to ASCII: %v", name, err) + } + + ids = append(ids, acme.Identifier{ + Type: "dns", + Value: normalizedName, + }) + } + if len(ids) == 0 { + return nil, fmt.Errorf("no identifiers found") + } + + order := acme.Order{Identifiers: ids} + var err error + + // remember which challenge types failed for which identifiers + // so we can retry with other challenge types + failedChallengeTypes := make(failedChallengeMap) + + const maxAttempts = 3 // hard cap on number of retries for good measure + for attempt := 1; attempt <= maxAttempts; attempt++ { + if attempt > 1 { + select { + case <-time.After(1 * time.Second): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + + // create order for a new certificate + order, err = c.Client.NewOrder(ctx, account, order) + if err != nil { + return nil, fmt.Errorf("creating new order: %w", err) + } + + // solve one challenge for each authz on the order + err = c.solveChallenges(ctx, account, order, failedChallengeTypes) + + // yay, we win! + if err == nil { + break + } + + // for some errors, we can retry with different challenge types + var problem acme.Problem + if errors.As(err, &problem) { + authz := problem.Resource.(acme.Authorization) + if c.Logger != nil { + c.Logger.Error("validating authorization", + zap.String("identifier", authz.IdentifierValue()), + zap.Error(err), + zap.String("order", order.Location), + zap.Int("attempt", attempt), + zap.Int("max_attempts", maxAttempts)) + } + err = fmt.Errorf("solving challenge: %s: %w", authz.IdentifierValue(), err) + if errors.As(err, &retryableErr{}) { + continue + } + return nil, err + } + + return nil, fmt.Errorf("solving challenges: %w (order=%s)", err, order.Location) + } + + if c.Logger != nil { + c.Logger.Info("validations succeeded; finalizing order", zap.String("order", order.Location)) + } + + // finalize the order, which requests the CA to issue us a certificate + order, err = c.Client.FinalizeOrder(ctx, account, order, csr.Raw) + if err != nil { + return nil, fmt.Errorf("finalizing order %s: %w", order.Location, err) + } + + // finally, download the certificate + certChains, err := c.Client.GetCertificateChain(ctx, account, order.Certificate) + if err != nil { + return nil, fmt.Errorf("downloading certificate chain from %s: %w (order=%s)", + order.Certificate, err, order.Location) + } + + if c.Logger != nil { + if len(certChains) == 0 { + c.Logger.Info("no certificate chains offered by server") + } else { + c.Logger.Info("successfully downloaded available certificate chains", + zap.Int("count", len(certChains)), + zap.String("first_url", certChains[0].URL)) + } + } + + return certChains, nil +} + +// ObtainCertificate is the same as ObtainCertificateUsingCSR, except it is a slight wrapper +// that generates the CSR for you. Doing so requires the private key you will be using for +// the certificate (different from the account private key). It obtains a certificate for +// the given SANs (domain names) using the provided account. +func (c *Client) ObtainCertificate(ctx context.Context, account acme.Account, certPrivateKey crypto.Signer, sans []string) ([]acme.Certificate, error) { + if len(sans) == 0 { + return nil, fmt.Errorf("no DNS names provided: %v", sans) + } + if certPrivateKey == nil { + return nil, fmt.Errorf("missing certificate private key") + } + + csrTemplate := new(x509.CertificateRequest) + for _, name := range sans { + if ip := net.ParseIP(name); ip != nil { + csrTemplate.IPAddresses = append(csrTemplate.IPAddresses, ip) + } else if strings.Contains(name, "@") { + csrTemplate.EmailAddresses = append(csrTemplate.EmailAddresses, name) + } else if u, err := url.Parse(name); err == nil && strings.Contains(name, "/") { + csrTemplate.URIs = append(csrTemplate.URIs, u) + } else { + csrTemplate.DNSNames = append(csrTemplate.DNSNames, name) + } + } + + // to properly fill out the CSR, we need to create it, then parse it + csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, certPrivateKey) + if err != nil { + return nil, fmt.Errorf("generating CSR: %v", err) + } + csr, err := x509.ParseCertificateRequest(csrDER) + if err != nil { + return nil, fmt.Errorf("parsing generated CSR: %v", err) + } + + return c.ObtainCertificateUsingCSR(ctx, account, csr) +} + +// getAuthzObjects constructs stateful authorization objects for each authz on the order. +// It includes all authorizations regardless of their status so that they can be +// deactivated at the end if necessary. Be sure to check authz status before operating +// on the authz; not all will be "pending" - some authorizations might already be valid. +func (c *Client) getAuthzObjects(ctx context.Context, account acme.Account, order acme.Order, + failedChallengeTypes failedChallengeMap) ([]*authzState, error) { + var authzStates []*authzState + var err error + + // start by allowing each authz's solver to present for its challenge + for _, authzURL := range order.Authorizations { + authz := &authzState{account: account} + authz.Authorization, err = c.Client.GetAuthorization(ctx, account, authzURL) + if err != nil { + return nil, fmt.Errorf("getting authorization at %s: %w", authzURL, err) + } + + // add all offered challenge types to our memory if they + // arent't there already; we use this for statistics to + // choose the most successful challenge type over time; + // if initial fill, randomize challenge order + preferredChallengesMu.Lock() + preferredWasEmpty := len(preferredChallenges) == 0 + for _, chal := range authz.Challenges { + preferredChallenges.addUnique(chal.Type) + } + if preferredWasEmpty { + weakrand.Shuffle(len(preferredChallenges), func(i, j int) { + preferredChallenges[i], preferredChallenges[j] = + preferredChallenges[j], preferredChallenges[i] + }) + } + preferredChallengesMu.Unlock() + + // copy over any challenges that are not known to have already + // failed, making them candidates for solving for this authz + failedChallengeTypes.enqueueUnfailedChallenges(authz) + + authzStates = append(authzStates, authz) + } + + // sort authzs so that challenges which require waiting go first; no point + // in getting authorizations quickly while others will take a long time + sort.SliceStable(authzStates, func(i, j int) bool { + _, iIsWaiter := authzStates[i].currentSolver.(Waiter) + _, jIsWaiter := authzStates[j].currentSolver.(Waiter) + // "if i is a waiter, and j is not a waiter, then i is less than j" + return iIsWaiter && !jIsWaiter + }) + + return authzStates, nil +} + +func (c *Client) solveChallenges(ctx context.Context, account acme.Account, order acme.Order, failedChallengeTypes failedChallengeMap) error { + authzStates, err := c.getAuthzObjects(ctx, account, order, failedChallengeTypes) + if err != nil { + return err + } + + // when the function returns, make sure we clean up any and all resources + defer func() { + // always clean up any remaining challenge solvers + for _, authz := range authzStates { + if authz.currentSolver == nil { + // happens when authz state ended on a challenge we have no + // solver for or if we have already cleaned up this solver + continue + } + if err := authz.currentSolver.CleanUp(ctx, authz.currentChallenge); err != nil { + if c.Logger != nil { + c.Logger.Error("cleaning up solver", + zap.String("identifier", authz.IdentifierValue()), + zap.String("challenge_type", authz.currentChallenge.Type), + zap.Error(err)) + } + } + } + + if err == nil { + return + } + + // if this function returns with an error, make sure to deactivate + // all pending or valid authorization objects so they don't "leak" + // See: https://github.com/go-acme/lego/issues/383 and https://github.com/go-acme/lego/issues/353 + for _, authz := range authzStates { + if authz.Status != acme.StatusPending && authz.Status != acme.StatusValid { + continue + } + updatedAuthz, err := c.Client.DeactivateAuthorization(ctx, account, authz.Location) + if err != nil { + if c.Logger != nil { + c.Logger.Error("deactivating authorization", + zap.String("identifier", authz.IdentifierValue()), + zap.String("authz", authz.Location), + zap.Error(err)) + } + } + authz.Authorization = updatedAuthz + } + }() + + // present for all challenges first; this allows them all to begin any + // slow tasks up front if necessary before we start polling/waiting + for _, authz := range authzStates { + // see §7.1.6 for state transitions + if authz.Status != acme.StatusPending && authz.Status != acme.StatusValid { + return fmt.Errorf("authz %s has unexpected status; order will fail: %s", authz.Location, authz.Status) + } + if authz.Status == acme.StatusValid { + continue + } + + err = c.presentForNextChallenge(ctx, authz) + if err != nil { + return err + } + } + + // now that all solvers have had the opportunity to present, tell + // the server to begin the selected challenge for each authz + for _, authz := range authzStates { + err = c.initiateCurrentChallenge(ctx, authz) + if err != nil { + return err + } + } + + // poll each authz to wait for completion of all challenges + for _, authz := range authzStates { + err = c.pollAuthorization(ctx, account, authz, failedChallengeTypes) + if err != nil { + return err + } + } + + return nil +} + +func (c *Client) presentForNextChallenge(ctx context.Context, authz *authzState) error { + if authz.Status != acme.StatusPending { + if authz.Status == acme.StatusValid && c.Logger != nil { + c.Logger.Info("authorization already valid", + zap.String("identifier", authz.IdentifierValue()), + zap.String("authz_url", authz.Location), + zap.Time("expires", authz.Expires)) + } + return nil + } + + err := c.nextChallenge(authz) + if err != nil { + return err + } + + if c.Logger != nil { + c.Logger.Info("trying to solve challenge", + zap.String("identifier", authz.IdentifierValue()), + zap.String("challenge_type", authz.currentChallenge.Type), + zap.String("ca", c.Directory)) + } + + err = authz.currentSolver.Present(ctx, authz.currentChallenge) + if err != nil { + return fmt.Errorf("presenting for challenge: %w", err) + } + + return nil +} + +func (c *Client) initiateCurrentChallenge(ctx context.Context, authz *authzState) error { + if authz.Status != acme.StatusPending { + return nil + } + + // by now, all challenges should have had an opportunity to present, so + // if this solver needs more time to finish presenting, wait on it now + // (yes, this does block the initiation of the other challenges, but + // that's probably OK, since we can't finalize the order until the slow + // challenges are done too) + if waiter, ok := authz.currentSolver.(Waiter); ok { + err := waiter.Wait(ctx, authz.currentChallenge) + if err != nil { + return fmt.Errorf("waiting for solver %T to be ready: %w", authz.currentSolver, err) + } + } + + // tell the server to initiate the challenge + var err error + authz.currentChallenge, err = c.Client.InitiateChallenge(ctx, authz.account, authz.currentChallenge) + if err != nil { + return fmt.Errorf("initiating challenge with server: %w", err) + } + + if c.Logger != nil { + c.Logger.Debug("challenge accepted", + zap.String("identifier", authz.IdentifierValue()), + zap.String("challenge_type", authz.currentChallenge.Type)) + } + + return nil +} + +// nextChallenge sets the next challenge (and associated solver) on +// authz; it returns an error if there is no compatible challenge. +func (c *Client) nextChallenge(authz *authzState) error { + preferredChallengesMu.Lock() + defer preferredChallengesMu.Unlock() + + // find the most-preferred challenge that is also in the list of + // remaining challenges, then make sure we have a solver for it + for _, prefChalType := range preferredChallenges { + for i, remainingChal := range authz.remainingChallenges { + if remainingChal.Type != prefChalType.typeName { + continue + } + authz.currentChallenge = remainingChal + authz.currentSolver = c.ChallengeSolvers[authz.currentChallenge.Type] + if authz.currentSolver != nil { + authz.remainingChallenges = append(authz.remainingChallenges[:i], authz.remainingChallenges[i+1:]...) + return nil + } + if c.Logger != nil { + c.Logger.Debug("no solver configured", zap.String("challenge_type", remainingChal.Type)) + } + break + } + } + return fmt.Errorf("%s: no solvers available for remaining challenges (configured=%v offered=%v remaining=%v)", + authz.IdentifierValue(), c.enabledChallengeTypes(), authz.listOfferedChallenges(), authz.listRemainingChallenges()) +} + +func (c *Client) pollAuthorization(ctx context.Context, account acme.Account, authz *authzState, failedChallengeTypes failedChallengeMap) error { + // In §7.5.1, the spec says: + // + // "For challenges where the client can tell when the server has + // validated the challenge (e.g., by seeing an HTTP or DNS request + // from the server), the client SHOULD NOT begin polling until it has + // seen the validation request from the server." + // + // However, in practice, this is difficult in the general case because + // we would need to design some relatively-nuanced concurrency and hope + // that the solver implementations also get their side right -- and the + // fact that it's even possible only sometimes makes it harder, because + // each solver needs a way to signal whether we should wait for its + // approval. So no, I've decided not to implement that recommendation + // in this particular library, but any implementations that use the lower + // ACME API directly are welcome and encouraged to do so where possible. + var err error + authz.Authorization, err = c.Client.PollAuthorization(ctx, account, authz.Authorization) + + // if a challenge was attempted (i.e. did not start valid)... + if authz.currentSolver != nil { + // increment the statistics on this challenge type before handling error + preferredChallengesMu.Lock() + preferredChallenges.increment(authz.currentChallenge.Type, err == nil) + preferredChallengesMu.Unlock() + + // always clean up the challenge solver after polling, regardless of error + cleanupErr := authz.currentSolver.CleanUp(ctx, authz.currentChallenge) + if cleanupErr != nil && c.Logger != nil { + c.Logger.Error("cleaning up solver", + zap.String("identifier", authz.IdentifierValue()), + zap.String("challenge_type", authz.currentChallenge.Type), + zap.Error(err)) + } + authz.currentSolver = nil // avoid cleaning it up again later + } + + // finally, handle any error from validating the authz + if err != nil { + var problem acme.Problem + if errors.As(err, &problem) { + if c.Logger != nil { + c.Logger.Error("challenge failed", + zap.String("identifier", authz.IdentifierValue()), + zap.String("challenge_type", authz.currentChallenge.Type), + zap.Int("status_code", problem.Status), + zap.String("problem_type", problem.Type), + zap.String("error", problem.Detail)) + } + + failedChallengeTypes.rememberFailedChallenge(authz) + + switch problem.Type { + case acme.ProblemTypeConnection, + acme.ProblemTypeDNS, + acme.ProblemTypeServerInternal, + acme.ProblemTypeUnauthorized, + acme.ProblemTypeTLS: + // this error might be recoverable with another challenge type + return retryableErr{err} + } + } + return fmt.Errorf("[%s] %w", authz.Authorization.IdentifierValue(), err) + } + return nil +} + +func (c *Client) enabledChallengeTypes() []string { + enabledChallenges := make([]string, 0, len(c.ChallengeSolvers)) + for name, val := range c.ChallengeSolvers { + if val != nil { + enabledChallenges = append(enabledChallenges, name) + } + } + return enabledChallenges +} + +type authzState struct { + acme.Authorization + account acme.Account + currentChallenge acme.Challenge + currentSolver Solver + remainingChallenges []acme.Challenge +} + +func (authz authzState) listOfferedChallenges() []string { + return challengeTypeNames(authz.Challenges) +} + +func (authz authzState) listRemainingChallenges() []string { + return challengeTypeNames(authz.remainingChallenges) +} + +func challengeTypeNames(challengeList []acme.Challenge) []string { + names := make([]string, 0, len(challengeList)) + for _, chal := range challengeList { + names = append(names, chal.Type) + } + return names +} + +// TODO: possibly configurable policy? converge to most successful (current) vs. completely random + +// challengeHistory is a memory of how successful a challenge type is. +type challengeHistory struct { + typeName string + successes, total int +} + +func (ch challengeHistory) successRatio() float64 { + if ch.total == 0 { + return 1.0 + } + return float64(ch.successes) / float64(ch.total) +} + +// failedChallengeMap keeps track of failed challenge types per identifier. +type failedChallengeMap map[string][]string + +func (fcm failedChallengeMap) rememberFailedChallenge(authz *authzState) { + idKey := fcm.idKey(authz) + fcm[idKey] = append(fcm[idKey], authz.currentChallenge.Type) +} + +// enqueueUnfailedChallenges enqueues each challenge offered in authz if it +// is not known to have failed for the authz's identifier already. +func (fcm failedChallengeMap) enqueueUnfailedChallenges(authz *authzState) { + idKey := fcm.idKey(authz) + for _, chal := range authz.Challenges { + if !contains(fcm[idKey], chal.Type) { + authz.remainingChallenges = append(authz.remainingChallenges, chal) + } + } +} + +func (fcm failedChallengeMap) idKey(authz *authzState) string { + return authz.Identifier.Type + authz.IdentifierValue() +} + +// challengeTypes is a list of challenges we've seen and/or +// used previously. It sorts from most successful to least +// successful, such that most successful challenges are first. +type challengeTypes []challengeHistory + +// Len is part of sort.Interface. +func (ct challengeTypes) Len() int { return len(ct) } + +// Swap is part of sort.Interface. +func (ct challengeTypes) Swap(i, j int) { ct[i], ct[j] = ct[j], ct[i] } + +// Less is part of sort.Interface. It sorts challenge +// types from highest success ratio to lowest. +func (ct challengeTypes) Less(i, j int) bool { + return ct[i].successRatio() > ct[j].successRatio() +} + +func (ct *challengeTypes) addUnique(challengeType string) { + for _, c := range *ct { + if c.typeName == challengeType { + return + } + } + *ct = append(*ct, challengeHistory{typeName: challengeType}) +} + +func (ct challengeTypes) increment(challengeType string, successful bool) { + defer sort.Stable(ct) // keep most successful challenges in front + for i, c := range ct { + if c.typeName == challengeType { + ct[i].total++ + if successful { + ct[i].successes++ + } + return + } + } +} + +func contains(haystack []string, needle string) bool { + for _, s := range haystack { + if s == needle { + return true + } + } + return false +} + +// retryableErr wraps an error that indicates the caller should retry +// the operation; specifically with a different challenge type. +type retryableErr struct{ error } + +func (re retryableErr) Unwrap() error { return re.error } + +// Keep a list of challenges we've seen offered by servers, +// and prefer keep an ordered list of +var ( + preferredChallenges challengeTypes + preferredChallengesMu sync.Mutex +) diff --git a/vendor/github.com/mholt/acmez/go.mod b/vendor/github.com/mholt/acmez/go.mod new file mode 100644 index 0000000000..a0495e531a --- /dev/null +++ b/vendor/github.com/mholt/acmez/go.mod @@ -0,0 +1,8 @@ +module github.com/mholt/acmez + +go 1.14 + +require ( + go.uber.org/zap v1.15.0 + golang.org/x/net v0.0.0-20200707034311-ab3426394381 +) diff --git a/vendor/github.com/mholt/acmez/go.sum b/vendor/github.com/mholt/acmez/go.sum new file mode 100644 index 0000000000..929a2dd57c --- /dev/null +++ b/vendor/github.com/mholt/acmez/go.sum @@ -0,0 +1,61 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= +go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/vendor/github.com/mholt/acmez/solver.go b/vendor/github.com/mholt/acmez/solver.go new file mode 100644 index 0000000000..8e77b27b37 --- /dev/null +++ b/vendor/github.com/mholt/acmez/solver.go @@ -0,0 +1,72 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acmez + +import ( + "context" + + "github.com/mholt/acmez/acme" +) + +// Solver is a type that can solve ACME challenges. All +// implementations MUST honor context cancellation. +type Solver interface { + // Present is called just before a challenge is initiated. + // The implementation MUST prepare anything that is necessary + // for completing the challenge; for example, provisioning + // an HTTP resource, TLS certificate, or a DNS record. + // + // It MUST return quickly. If presenting the challenge token + // will take time, then the implementation MUST do the + // minimum amount of work required in this method, and + // SHOULD additionally implement the Waiter interface. + // For example, a DNS challenge solver might make a quick + // HTTP request to a provider's API to create a new DNS + // record, but it might be several minutes or hours before + // the DNS record propagates. The API request should be + // done in Present(), and waiting for propagation should + // be done in Wait(). + Present(context.Context, acme.Challenge) error + + // CleanUp is called after a challenge is finished, whether + // successful or not. It MUST free/remove any resources it + // allocated/created during Present. It SHOULD NOT require + // that Present ran successfully. It MUST return quickly. + CleanUp(context.Context, acme.Challenge) error +} + +// Waiter is an optional interface for Solvers to implement. Its +// primary purpose is to help ensure the challenge can be solved +// before the server gives up trying to verify the challenge. +// +// If implemented, it will be called after Present() but just +// before the challenge is initiated with the server. It blocks +// until the challenge is ready to be solved. (For example, +// waiting on a DNS record to propagate.) This allows challenges +// to succeed that would normally fail because they take too long +// to set up (i.e. the ACME server would give up polling DNS or +// the client would timeout its polling). By separating Present() +// from Wait(), it allows the slow part of all solvers to begin +// up front, rather than waiting on each solver one at a time. +// +// It MUST NOT do anything exclusive of Present() that is required +// for the challenge to succeed. In other words, if Present() is +// called but Wait() is not, then the challenge should still be able +// to succeed assuming infinite time. +// +// Implementations MUST honor context cancellation. +type Waiter interface { + Wait(context.Context, acme.Challenge) error +} diff --git a/vendor/github.com/mholt/acmez/tlsalpn01.go b/vendor/github.com/mholt/acmez/tlsalpn01.go new file mode 100644 index 0000000000..a6b920b547 --- /dev/null +++ b/vendor/github.com/mholt/acmez/tlsalpn01.go @@ -0,0 +1,98 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acmez + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "math/big" + "time" + + "github.com/mholt/acmez/acme" +) + +// TLSALPN01ChallengeCert creates a certificate that can be used for +// handshakes while solving the tls-alpn-01 challenge. See RFC 8737 §3. +func TLSALPN01ChallengeCert(challenge acme.Challenge) (*tls.Certificate, error) { + keyAuthSum := sha256.Sum256([]byte(challenge.KeyAuthorization)) + keyAuthSumASN1, err := asn1.Marshal(keyAuthSum[:sha256.Size]) + if err != nil { + return nil, err + } + + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + challengeKeyASN1, err := x509.MarshalECPrivateKey(certKey) + if err != nil { + return nil, err + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{CommonName: "ACME challenge"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour * 365), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{challenge.Identifier.Value}, + + // add key authentication digest as the acmeValidation-v1 extension + // (marked as critical such that it won't be used by non-ACME software). + // Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-3 + ExtraExtensions: []pkix.Extension{ + { + Id: idPEACMEIdentifierV1, + Critical: true, + Value: keyAuthSumASN1, + }, + }, + } + challengeCertDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &certKey.PublicKey, certKey) + if err != nil { + return nil, err + } + + challengeCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: challengeCertDER}) + challengeKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: challengeKeyASN1}) + + cert, err := tls.X509KeyPair(challengeCertPEM, challengeKeyPEM) + if err != nil { + return nil, err + } + + return &cert, nil +} + +// ACMETLS1Protocol is the ALPN value for the TLS-ALPN challenge +// handshake. See RFC 8737 §6.2. +const ACMETLS1Protocol = "acme-tls/1" + +// idPEACMEIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. +// See RFC 8737 §6.1. https://www.rfc-editor.org/rfc/rfc8737.html#section-6.1 +var idPEACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} |