mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-30 22:11:07 +00:00 
			
		
		
		
	* use certmagic for more extensible/robust ACME cert handling * accept TOS based on config option Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv>
		
			
				
	
	
		
			249 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			249 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // 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"
 | |
| )
 |