1// Copyright 2020 Matthew Holt
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package acme
16
17import (
18	"context"
19	"crypto"
20	"encoding/base64"
21	"encoding/json"
22	"fmt"
23)
24
25// Account represents a set of metadata associated with an account
26// as defined by the ACME spec §7.1.2:
27// https://tools.ietf.org/html/rfc8555#section-7.1.2
28type Account struct {
29	// status (required, string):  The status of this account.  Possible
30	// values are "valid", "deactivated", and "revoked".  The value
31	// "deactivated" should be used to indicate client-initiated
32	// deactivation whereas "revoked" should be used to indicate server-
33	// initiated deactivation.  See Section 7.1.6.
34	Status string `json:"status"`
35
36	// contact (optional, array of string):  An array of URLs that the
37	// server can use to contact the client for issues related to this
38	// account.  For example, the server may wish to notify the client
39	// about server-initiated revocation or certificate expiration.  For
40	// information on supported URL schemes, see Section 7.3.
41	Contact []string `json:"contact,omitempty"`
42
43	// termsOfServiceAgreed (optional, boolean):  Including this field in a
44	// newAccount request, with a value of true, indicates the client's
45	// agreement with the terms of service.  This field cannot be updated
46	// by the client.
47	TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
48
49	// externalAccountBinding (optional, object):  Including this field in a
50	// newAccount request indicates approval by the holder of an existing
51	// non-ACME account to bind that account to this ACME account.  This
52	// field is not updateable by the client (see Section 7.3.4).
53	//
54	// Use SetExternalAccountBinding() to set this field's value properly.
55	ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
56
57	// orders (required, string):  A URL from which a list of orders
58	// submitted by this account can be fetched via a POST-as-GET
59	// request, as described in Section 7.1.2.1.
60	Orders string `json:"orders"`
61
62	// In response to new-account, "the server returns this account
63	// object in a 201 (Created) response, with the account URL
64	// in a Location header field." §7.3
65	//
66	// We transfer the value from the header to this field for
67	// storage and recall purposes.
68	Location string `json:"location,omitempty"`
69
70	// The private key to the account. Because it is secret, it is
71	// not serialized as JSON and must be stored separately (usually
72	// a PEM-encoded file).
73	PrivateKey crypto.Signer `json:"-"`
74}
75
76// SetExternalAccountBinding sets the ExternalAccountBinding field of the account.
77// It only sets the field value; it does not register the account with the CA. (The
78// client parameter is necessary because the EAB encoding depends on the directory.)
79func (a *Account) SetExternalAccountBinding(ctx context.Context, client *Client, eab EAB) error {
80	if err := client.provision(ctx); err != nil {
81		return err
82	}
83
84	macKey, err := base64.RawURLEncoding.DecodeString(eab.MACKey)
85	if err != nil {
86		return fmt.Errorf("base64-decoding MAC key: %w", err)
87	}
88
89	eabJWS, err := jwsEncodeEAB(a.PrivateKey.Public(), macKey, keyID(eab.KeyID), client.dir.NewAccount)
90	if err != nil {
91		return fmt.Errorf("signing EAB content: %w", err)
92	}
93
94	a.ExternalAccountBinding = eabJWS
95
96	return nil
97}
98
99// NewAccount creates a new account on the ACME server.
100//
101// "A client creates a new account with the server by sending a POST
102// request to the server's newAccount URL." §7.3
103func (c *Client) NewAccount(ctx context.Context, account Account) (Account, error) {
104	if err := c.provision(ctx); err != nil {
105		return account, err
106	}
107	return c.postAccount(ctx, c.dir.NewAccount, accountObject{Account: account})
108}
109
110// GetAccount looks up an account on the ACME server.
111//
112// "If a client wishes to find the URL for an existing account and does
113// not want an account to be created if one does not already exist, then
114// it SHOULD do so by sending a POST request to the newAccount URL with
115// a JWS whose payload has an 'onlyReturnExisting' field set to 'true'."
116// §7.3.1
117func (c *Client) GetAccount(ctx context.Context, account Account) (Account, error) {
118	if err := c.provision(ctx); err != nil {
119		return account, err
120	}
121	return c.postAccount(ctx, c.dir.NewAccount, accountObject{
122		Account:            account,
123		OnlyReturnExisting: true,
124	})
125}
126
127// UpdateAccount updates account information on the ACME server.
128//
129// "If the client wishes to update this information in the future, it
130// sends a POST request with updated information to the account URL.
131// The server MUST ignore any updates to the 'orders' field,
132// 'termsOfServiceAgreed' field (see Section 7.3.3), the 'status' field
133// (except as allowed by Section 7.3.6), or any other fields it does not
134// recognize." §7.3.2
135//
136// This method uses the account.Location value as the account URL.
137func (c *Client) UpdateAccount(ctx context.Context, account Account) (Account, error) {
138	return c.postAccount(ctx, account.Location, accountObject{Account: account})
139}
140
141type keyChangeRequest struct {
142	Account string          `json:"account"`
143	OldKey  json.RawMessage `json:"oldKey"`
144}
145
146// AccountKeyRollover changes an account's associated key.
147//
148// "To change the key associated with an account, the client sends a
149// request to the server containing signatures by both the old and new
150// keys." §7.3.5
151func (c *Client) AccountKeyRollover(ctx context.Context, account Account, newPrivateKey crypto.Signer) (Account, error) {
152	if err := c.provision(ctx); err != nil {
153		return account, err
154	}
155
156	oldPublicKeyJWK, err := jwkEncode(account.PrivateKey.Public())
157	if err != nil {
158		return account, fmt.Errorf("encoding old private key: %v", err)
159	}
160
161	keyChangeReq := keyChangeRequest{
162		Account: account.Location,
163		OldKey:  []byte(oldPublicKeyJWK),
164	}
165
166	innerJWS, err := jwsEncodeJSON(keyChangeReq, newPrivateKey, "", "", c.dir.KeyChange)
167	if err != nil {
168		return account, fmt.Errorf("encoding inner JWS: %v", err)
169	}
170
171	_, err = c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.KeyChange, json.RawMessage(innerJWS), nil)
172	if err != nil {
173		return account, fmt.Errorf("rolling key on server: %w", err)
174	}
175
176	account.PrivateKey = newPrivateKey
177
178	return account, nil
179
180}
181
182func (c *Client) postAccount(ctx context.Context, endpoint string, account accountObject) (Account, error) {
183	// Normally, the account URL is the key ID ("kid")... except when the user
184	// is trying to get the correct account URL. In that case, we must ignore
185	// any existing URL we may have and not set the kid field on the request.
186	// Arguably, this is a user error (spec says "If client wishes to find the
187	// URL for an existing account", so why would the URL already be filled
188	// out?) but it's easy enough to infer their intent and make it work.
189	kid := account.Location
190	if account.OnlyReturnExisting {
191		kid = ""
192	}
193
194	resp, err := c.httpPostJWS(ctx, account.PrivateKey, kid, endpoint, account, &account.Account)
195	if err != nil {
196		return account.Account, err
197	}
198
199	account.Location = resp.Header.Get("Location")
200
201	return account.Account, nil
202}
203
204type accountObject struct {
205	Account
206
207	// If true, newAccount will be read-only, and Account.Location
208	// (which holds the account URL) must be empty.
209	OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
210}
211
212// EAB (External Account Binding) contains information
213// necessary to bind or map an ACME account to some
214// other account known by the CA.
215//
216// External account bindings are "used to associate an
217// ACME account with an existing account in a non-ACME
218// system, such as a CA customer database."
219//
220// "To enable ACME account binding, the CA operating the
221// ACME server needs to provide the ACME client with a
222// MAC key and a key identifier, using some mechanism
223// outside of ACME." §7.3.4
224type EAB struct {
225	// "The key identifier MUST be an ASCII string." §7.3.4
226	KeyID string `json:"key_id"`
227
228	// "The MAC key SHOULD be provided in base64url-encoded
229	// form, to maximize compatibility between non-ACME
230	// provisioning systems and ACME clients." §7.3.4
231	MACKey string `json:"mac_key"`
232}
233
234// Possible status values. From several spec sections:
235// - Account §7.1.2 (valid, deactivated, revoked)
236// - Order §7.1.3 (pending, ready, processing, valid, invalid)
237// - Authorization §7.1.4 (pending, valid, invalid, deactivated, expired, revoked)
238// - Challenge §7.1.5 (pending, processing, valid, invalid)
239// - Status changes §7.1.6
240const (
241	StatusPending     = "pending"
242	StatusProcessing  = "processing"
243	StatusValid       = "valid"
244	StatusInvalid     = "invalid"
245	StatusDeactivated = "deactivated"
246	StatusExpired     = "expired"
247	StatusRevoked     = "revoked"
248	StatusReady       = "ready"
249)
250