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