vault/builtin/logical/pki/path_acme_account.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

483 lines
16 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package pki
import (
"fmt"
"net/http"
"path"
"strings"
"time"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
func uuidNameRegex(name string) string {
return fmt.Sprintf("(?P<%s>[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}?)", name)
}
func pathAcmeNewAccount(b *backend, baseUrl string, opts acmeWrapperOpts) *framework.Path {
return patternAcmeNewAccount(b, baseUrl+"/new-account", opts)
}
func pathAcmeUpdateAccount(b *backend, baseUrl string, opts acmeWrapperOpts) *framework.Path {
return patternAcmeNewAccount(b, baseUrl+"/account/"+uuidNameRegex("kid"), opts)
}
func addFieldsForACMEPath(fields map[string]*framework.FieldSchema, pattern string) map[string]*framework.FieldSchema {
if strings.Contains(pattern, framework.GenericNameRegex("role")) {
fields["role"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `The desired role for the acme request`,
Required: true,
}
}
if strings.Contains(pattern, framework.GenericNameRegex(issuerRefParam)) {
fields[issuerRefParam] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `Reference to an existing issuer name or issuer id`,
Required: true,
}
}
if strings.Contains(pattern, framework.GenericNameRegex("policy")) {
fields["policy"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `The policy name to pass through to the CIEPS service`,
Required: true,
}
}
return fields
}
func addFieldsForACMERequest(fields map[string]*framework.FieldSchema) map[string]*framework.FieldSchema {
fields["protected"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: "ACME request 'protected' value",
Required: false,
}
fields["payload"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: "ACME request 'payload' value",
Required: false,
}
fields["signature"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: "ACME request 'signature' value",
Required: false,
}
return fields
}
func addFieldsForACMEKidRequest(fields map[string]*framework.FieldSchema, pattern string) map[string]*framework.FieldSchema {
if strings.Contains(pattern, uuidNameRegex("kid")) {
fields["kid"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `The key identifier provided by the CA`,
Required: true,
}
}
return fields
}
func patternAcmeNewAccount(b *backend, pattern string, opts acmeWrapperOpts) *framework.Path {
fields := map[string]*framework.FieldSchema{}
addFieldsForACMEPath(fields, pattern)
addFieldsForACMERequest(fields)
addFieldsForACMEKidRequest(fields, pattern)
return &framework.Path{
Pattern: pattern,
Fields: fields,
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.acmeParsedWrapper(opts, b.acmeNewAccountHandler),
ForwardPerformanceSecondary: false,
ForwardPerformanceStandby: true,
},
},
HelpSynopsis: pathAcmeHelpSync,
HelpDescription: pathAcmeHelpDesc,
}
}
func (b *backend) acmeNewAccountHandler(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData, userCtx *jwsCtx, data map[string]interface{}) (*logical.Response, error) {
// Parameters
var ok bool
var onlyReturnExisting bool
var contacts []string
var termsOfServiceAgreed bool
var status string
var eabData map[string]interface{}
rawContact, present := data["contact"]
if present {
listContact, ok := rawContact.([]interface{})
if !ok {
return nil, fmt.Errorf("invalid type (%T) for field 'contact': %w", rawContact, ErrMalformed)
}
for index, singleContact := range listContact {
contact, ok := singleContact.(string)
if !ok {
return nil, fmt.Errorf("invalid type (%T) for field 'contact' item %d: %w", singleContact, index, ErrMalformed)
}
contacts = append(contacts, contact)
}
}
rawTermsOfServiceAgreed, present := data["termsOfServiceAgreed"]
if present {
termsOfServiceAgreed, ok = rawTermsOfServiceAgreed.(bool)
if !ok {
return nil, fmt.Errorf("invalid type (%T) for field 'termsOfServiceAgreed': %w", rawTermsOfServiceAgreed, ErrMalformed)
}
}
rawOnlyReturnExisting, present := data["onlyReturnExisting"]
if present {
onlyReturnExisting, ok = rawOnlyReturnExisting.(bool)
if !ok {
return nil, fmt.Errorf("invalid type (%T) for field 'onlyReturnExisting': %w", rawOnlyReturnExisting, ErrMalformed)
}
}
// Per RFC 8555 7.3.6 Account deactivation, we will handle it within our update API.
rawStatus, present := data["status"]
if present {
status, ok = rawStatus.(string)
if !ok {
return nil, fmt.Errorf("invalid type (%T) for field 'onlyReturnExisting': %w", rawOnlyReturnExisting, ErrMalformed)
}
}
if eabDataRaw, ok := data["externalAccountBinding"]; ok {
eabData, ok = eabDataRaw.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%w: externalAccountBinding field was unparseable", ErrMalformed)
}
}
// We have two paths here: search or create.
if onlyReturnExisting {
return b.acmeAccountSearchHandler(acmeCtx, userCtx)
}
// Pass through the /new-account API calls to this specific handler as its requirements are different
// from the account update handler.
if strings.HasSuffix(r.Path, "/new-account") {
return b.acmeNewAccountCreateHandler(acmeCtx, userCtx, contacts, termsOfServiceAgreed, r, eabData)
}
return b.acmeNewAccountUpdateHandler(acmeCtx, userCtx, contacts, status, eabData)
}
func formatNewAccountResponse(acmeCtx *acmeContext, acct *acmeAccount, eabData map[string]interface{}) *logical.Response {
resp := formatAccountResponse(acmeCtx, acct)
// Per RFC 8555 Section 7.1.2. Account Objects
// 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
if acct.Eab != nil && len(eabData) != 0 {
resp.Data["externalAccountBinding"] = eabData
}
return resp
}
func formatAccountResponse(acmeCtx *acmeContext, acct *acmeAccount) *logical.Response {
location := acmeCtx.baseUrl.String() + "account/" + acct.KeyId
resp := &logical.Response{
Data: map[string]interface{}{
"status": acct.Status,
"orders": location + "/orders",
},
Headers: map[string][]string{
"Location": {location},
},
}
if len(acct.Contact) > 0 {
resp.Data["contact"] = acct.Contact
}
return resp
}
func (b *backend) acmeAccountSearchHandler(acmeCtx *acmeContext, userCtx *jwsCtx) (*logical.Response, error) {
thumbprint, err := userCtx.GetKeyThumbprint()
if err != nil {
return nil, fmt.Errorf("failed generating thumbprint for key: %w", err)
}
account, err := b.acmeState.LoadAccountByKey(acmeCtx, thumbprint)
if err != nil {
return nil, fmt.Errorf("failed to load account by thumbprint: %w", err)
}
if account != nil {
if err = acmeCtx.eabPolicy.EnforceForExistingAccount(account); err != nil {
return nil, err
}
return formatAccountResponse(acmeCtx, account), nil
}
// Per RFC 8555 Section 7.3.1. Finding an Account URL Given a Key:
//
// > If a client sends such a request and an account does not exist,
// > then the server MUST return an error response with status code
// > 400 (Bad Request) and type "urn:ietf:params:acme:error:accountDoesNotExist".
return nil, fmt.Errorf("An account with this key does not exist: %w", ErrAccountDoesNotExist)
}
func (b *backend) acmeNewAccountCreateHandler(acmeCtx *acmeContext, userCtx *jwsCtx, contact []string, termsOfServiceAgreed bool, r *logical.Request, eabData map[string]interface{}) (*logical.Response, error) {
if userCtx.Existing {
return nil, fmt.Errorf("cannot submit to newAccount with 'kid': %w", ErrMalformed)
}
// If the account already exists, return the existing one.
thumbprint, err := userCtx.GetKeyThumbprint()
if err != nil {
return nil, fmt.Errorf("failed generating thumbprint for key: %w", err)
}
accountByKey, err := b.acmeState.LoadAccountByKey(acmeCtx, thumbprint)
if err != nil {
return nil, fmt.Errorf("failed to load account by thumbprint: %w", err)
}
if accountByKey != nil {
if err = acmeCtx.eabPolicy.EnforceForExistingAccount(accountByKey); err != nil {
return nil, err
}
return formatAccountResponse(acmeCtx, accountByKey), nil
}
var eab *eabType
if len(eabData) != 0 {
eab, err = verifyEabPayload(b.acmeState, acmeCtx, userCtx, r.Path, eabData)
if err != nil {
return nil, err
}
}
// Verify against our EAB policy
if err = acmeCtx.eabPolicy.EnforceForNewAccount(eab); err != nil {
return nil, err
}
// TODO: Limit this only when ToS are required or set by the operator, since we don't have a
// ToS URL in the directory at the moment, we can not enforce this.
//if !termsOfServiceAgreed {
// return nil, fmt.Errorf("terms of service not agreed to: %w", ErrUserActionRequired)
//}
if eab != nil {
// We delete the EAB to prevent future re-use after associating it with an account, worst
// case if we fail creating the account we simply nuked the EAB which they can create another
// and retry
wasDeleted, err := b.acmeState.DeleteEab(acmeCtx.sc, eab.KeyID)
if err != nil {
return nil, fmt.Errorf("failed to delete eab reference: %w", err)
}
if !wasDeleted {
// Something consumed our EAB before we did bail...
return nil, fmt.Errorf("eab was already used: %w", ErrUnauthorized)
}
}
b.acmeAccountLock.RLock() // Prevents Account Creation and Tidy Interfering
defer b.acmeAccountLock.RUnlock()
accountByKid, err := b.acmeState.CreateAccount(acmeCtx, userCtx, contact, termsOfServiceAgreed, eab)
if err != nil {
if eab != nil {
return nil, fmt.Errorf("failed to create account: %w; the EAB key used for this request has been deleted as a result of this operation; fetch a new EAB key before retrying", err)
}
return nil, fmt.Errorf("failed to create account: %w", err)
}
resp := formatNewAccountResponse(acmeCtx, accountByKid, eabData)
// Per RFC 8555 Section 7.3. Account Management:
//
// > The server returns this account object in a 201 (Created) response,
// > with the account URL in a Location header field.
resp.Data[logical.HTTPStatusCode] = http.StatusCreated
return resp, nil
}
func (b *backend) acmeNewAccountUpdateHandler(acmeCtx *acmeContext, userCtx *jwsCtx, contact []string, status string, eabData map[string]interface{}) (*logical.Response, error) {
if !userCtx.Existing {
return nil, fmt.Errorf("cannot submit to account updates without a 'kid': %w", ErrMalformed)
}
if len(eabData) != 0 {
return nil, fmt.Errorf("%w: not allowed to update EAB data in accounts", ErrMalformed)
}
account, err := b.acmeState.LoadAccount(acmeCtx, userCtx.Kid)
if err != nil {
return nil, fmt.Errorf("error loading account: %w", err)
}
if err = acmeCtx.eabPolicy.EnforceForExistingAccount(account); err != nil {
return nil, err
}
// Per RFC 8555 7.3.6 Account deactivation, if we were previously deactivated, we should return
// unauthorized. There is no way to reactivate any accounts per ACME RFC.
if account.Status != AccountStatusValid {
// Treating "revoked" and "deactivated" as the same here.
return nil, ErrUnauthorized
}
shouldUpdate := false
// Check to see if we should update, we don't really care about ordering
if !strutil.EquivalentSlices(account.Contact, contact) {
shouldUpdate = true
account.Contact = contact
}
// Check to process account de-activation status was requested.
// 7.3.6. Account Deactivation
if string(AccountStatusDeactivated) == status {
shouldUpdate = true
// TODO: This should cancel any ongoing operations (do not revoke certs),
// perhaps we should delete this account here?
account.Status = AccountStatusDeactivated
account.AccountRevokedDate = time.Now()
}
if shouldUpdate {
err = b.acmeState.UpdateAccount(acmeCtx.sc, account)
if err != nil {
return nil, fmt.Errorf("failed to update account: %w", err)
}
}
resp := formatAccountResponse(acmeCtx, account)
return resp, nil
}
func (b *backend) tidyAcmeAccountByThumbprint(as *acmeState, sc *storageContext, keyThumbprint string, certTidyBuffer, accountTidyBuffer time.Duration) error {
thumbprintEntry, err := sc.Storage.Get(sc.Context, path.Join(acmeThumbprintPrefix, keyThumbprint))
if err != nil {
return fmt.Errorf("error retrieving thumbprint entry %v, unable to find corresponding account entry: %w", keyThumbprint, err)
}
if thumbprintEntry == nil {
return fmt.Errorf("empty thumbprint entry %v, unable to find corresponding account entry", keyThumbprint)
}
var thumbprint acmeThumbprint
err = thumbprintEntry.DecodeJSON(&thumbprint)
if err != nil {
return fmt.Errorf("unable to decode thumbprint entry %v to find account entry: %w", keyThumbprint, err)
}
if len(thumbprint.Kid) == 0 {
return fmt.Errorf("unable to find account entry: empty kid within thumbprint entry: %s", keyThumbprint)
}
// Now Get the Account:
accountEntry, err := sc.Storage.Get(sc.Context, acmeAccountPrefix+thumbprint.Kid)
if err != nil {
return err
}
if accountEntry == nil {
// We delete the Thumbprint Associated with the Account, and we are done
err = sc.Storage.Delete(sc.Context, path.Join(acmeThumbprintPrefix, keyThumbprint))
if err != nil {
return err
}
b.tidyStatusIncDeletedAcmeAccountCount()
return nil
}
var account acmeAccount
err = accountEntry.DecodeJSON(&account)
if err != nil {
return err
}
account.KeyId = thumbprint.Kid
// Tidy Orders On the Account
orderIds, err := as.ListOrderIds(sc, thumbprint.Kid)
if err != nil {
return err
}
allOrdersTidied := true
maxCertExpiryUpdated := false
for _, orderId := range orderIds {
wasTidied, orderExpiry, err := b.acmeTidyOrder(sc, thumbprint.Kid, getOrderPath(thumbprint.Kid, orderId), certTidyBuffer)
if err != nil {
return err
}
if !wasTidied {
allOrdersTidied = false
}
if !orderExpiry.IsZero() && account.MaxCertExpiry.Before(orderExpiry) {
account.MaxCertExpiry = orderExpiry
maxCertExpiryUpdated = true
}
}
now := time.Now()
if allOrdersTidied &&
now.After(account.AccountCreatedDate.Add(accountTidyBuffer)) &&
now.After(account.MaxCertExpiry.Add(accountTidyBuffer)) {
// Tidy this account
// If it is Revoked or Deactivated:
if (account.Status == AccountStatusRevoked || account.Status == AccountStatusDeactivated) && now.After(account.AccountRevokedDate.Add(accountTidyBuffer)) {
// We Delete the Account Associated with this Thumbprint:
err = sc.Storage.Delete(sc.Context, path.Join(acmeAccountPrefix, thumbprint.Kid))
if err != nil {
return err
}
// Now we delete the Thumbprint Associated with the Account:
err = sc.Storage.Delete(sc.Context, path.Join(acmeThumbprintPrefix, keyThumbprint))
if err != nil {
return err
}
b.tidyStatusIncDeletedAcmeAccountCount()
} else if account.Status == AccountStatusValid {
// Revoke This Account
account.AccountRevokedDate = now
account.Status = AccountStatusRevoked
err := as.UpdateAccount(sc, &account)
if err != nil {
return err
}
b.tidyStatusIncRevAcmeAccountCount()
}
}
// Only update the account if we modified the max cert expiry values and the account is still valid,
// to prevent us from adding back a deleted account or not re-writing the revoked account that was
// already written above.
if maxCertExpiryUpdated && account.Status == AccountStatusValid {
// Update our expiry time we previously setup.
err := as.UpdateAccount(sc, &account)
if err != nil {
return err
}
}
return nil
}