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