Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
249 lines
9.0 KiB
249 lines
9.0 KiB
// 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" |
|
)
|
|
|