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