forked from Shiloh/githaven
d2ea21d0d8
* 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>
284 lines
10 KiB
Go
Vendored
284 lines
10 KiB
Go
Vendored
// 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)
|
|
}
|
|
}
|