mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-05-31 21:00:02 +02:00
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>
This commit is contained in:
parent
bc05ddc0eb
commit
d2ea21d0d8
437 changed files with 56286 additions and 4270 deletions
249
vendor/github.com/mholt/acmez/acme/account.go
generated
vendored
Normal file
249
vendor/github.com/mholt/acmez/acme/account.go
generated
vendored
Normal file
|
@ -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"
|
||||
)
|
283
vendor/github.com/mholt/acmez/acme/authorization.go
generated
vendored
Normal file
283
vendor/github.com/mholt/acmez/acme/authorization.go
generated
vendored
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
165
vendor/github.com/mholt/acmez/acme/certificate.go
generated
vendored
Normal file
165
vendor/github.com/mholt/acmez/acme/certificate.go
generated
vendored
Normal file
|
@ -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
|
||||
)
|
133
vendor/github.com/mholt/acmez/acme/challenge.go
generated
vendored
Normal file
133
vendor/github.com/mholt/acmez/acme/challenge.go
generated
vendored
Normal file
|
@ -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
|
||||
)
|
240
vendor/github.com/mholt/acmez/acme/client.go
generated
vendored
Normal file
240
vendor/github.com/mholt/acmez/acme/client.go
generated
vendored
Normal file
|
@ -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
|
||||
)
|
394
vendor/github.com/mholt/acmez/acme/http.go
generated
vendored
Normal file
394
vendor/github.com/mholt/acmez/acme/http.go
generated
vendored
Normal file
|
@ -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="(.+?)"`)
|
263
vendor/github.com/mholt/acmez/acme/jws.go
generated
vendored
Normal file
263
vendor/github.com/mholt/acmez/acme/jws.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
247
vendor/github.com/mholt/acmez/acme/order.go
generated
vendored
Normal file
247
vendor/github.com/mholt/acmez/acme/order.go
generated
vendored
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
136
vendor/github.com/mholt/acmez/acme/problem.go
generated
vendored
Normal file
136
vendor/github.com/mholt/acmez/acme/problem.go
generated
vendored
Normal file
|
@ -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"
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue