mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +00:00
Add External Account Binding support to ACME (#20523)
* Add Vault APIS to create, list, delete ACME EAB keys - Add Vault authenticated APIs to create, list and delete ACME EAB keys. - Add supporting tests for all new apis * Add require_eab to acme configuration * Add EAB support to ACME * Add EAB support to ACME * PR feedback 1 - Address missing err return within DeleteEab - Move verifyEabPayload to acme_jws.go no code changes in this PR - Update error message returned for error on account storage with EAB. * PR feedback 2 - Verify JWK signature payload after signature verification * Introduce an ACME eab_policy in configuration - Instead of a boolean on/off for require_eab, introduce named policies for ACME behaviour enforcing eab. - The default policy of always-required, will force new accounts to have an EAB, and all operations in the future, will make sure the account has an EAB associated with it. - Two other policies, not-required will allow any anonymous users to use ACME within PKI and 'new-account-required' will enforce new accounts going forward to require an EAB, but existing accounts will still be allowed to use ACME if they don't have an EAB associated with the account. - Having 'always-required' as a policy, will override the environment variable to disable public acme as well. * Add missing go-docs to new tests. * Add valid eab_policy values in error message.
This commit is contained in:
69
builtin/logical/pki/acme_eab_policy.go
Normal file
69
builtin/logical/pki/acme_eab_policy.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EabPolicyName string
|
||||
|
||||
const (
|
||||
eabPolicyNotRequired EabPolicyName = "not-required"
|
||||
eabPolicyNewAccountRequired EabPolicyName = "new-account-required"
|
||||
eabPolicyAlwaysRequired EabPolicyName = "always-required"
|
||||
)
|
||||
|
||||
func getEabPolicyByString(name string) (EabPolicy, error) {
|
||||
lcName := strings.TrimSpace(strings.ToLower(name))
|
||||
switch lcName {
|
||||
case string(eabPolicyNotRequired):
|
||||
return getEabPolicyByName(eabPolicyNotRequired), nil
|
||||
case string(eabPolicyNewAccountRequired):
|
||||
return getEabPolicyByName(eabPolicyNewAccountRequired), nil
|
||||
case string(eabPolicyAlwaysRequired):
|
||||
return getEabPolicyByName(eabPolicyAlwaysRequired), nil
|
||||
default:
|
||||
return getEabPolicyByName(eabPolicyAlwaysRequired), fmt.Errorf("unknown eab policy name: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func getEabPolicyByName(name EabPolicyName) EabPolicy {
|
||||
return EabPolicy{Name: name}
|
||||
}
|
||||
|
||||
type EabPolicy struct {
|
||||
Name EabPolicyName
|
||||
}
|
||||
|
||||
// EnforceForNewAccount for new account creations, should we require an EAB.
|
||||
func (ep EabPolicy) EnforceForNewAccount(eabData *eabType) error {
|
||||
if (ep.Name == eabPolicyAlwaysRequired || ep.Name == eabPolicyNewAccountRequired) && eabData == nil {
|
||||
return ErrExternalAccountRequired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnforceForExistingAccount for all operations within ACME, does the account being used require an EAB attached to it.
|
||||
func (ep EabPolicy) EnforceForExistingAccount(account *acmeAccount) error {
|
||||
if ep.Name == eabPolicyAlwaysRequired && account.Eab == nil {
|
||||
return ErrExternalAccountRequired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExternalAccountRequired for new accounts incoming does is an EAB required
|
||||
func (ep EabPolicy) IsExternalAccountRequired() bool {
|
||||
return ep.Name == eabPolicyAlwaysRequired || ep.Name == eabPolicyNewAccountRequired
|
||||
}
|
||||
|
||||
// OverrideEnvDisablingPublicAcme determines if ACME is enabled but the OS environment variable
|
||||
// has said to disable public acme support, if we can override that environment variable to
|
||||
// turn on ACME support
|
||||
func (ep EabPolicy) OverrideEnvDisablingPublicAcme() bool {
|
||||
return ep.Name == eabPolicyAlwaysRequired
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
package pki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -26,6 +27,12 @@ var AllowedOuterJWSTypes = map[string]interface{}{
|
||||
"EdDSA2": true,
|
||||
}
|
||||
|
||||
var AllowedEabJWSTypes = map[string]interface{}{
|
||||
"HS256": true,
|
||||
"HS384": true,
|
||||
"HS512": true,
|
||||
}
|
||||
|
||||
// This wraps a JWS message structure.
|
||||
type jwsCtx struct {
|
||||
Algo string `json:"alg"`
|
||||
@@ -45,7 +52,25 @@ func (c *jwsCtx) GetKeyThumbprint() (string, error) {
|
||||
return base64.RawURLEncoding.EncodeToString(keyThumbprint), nil
|
||||
}
|
||||
|
||||
func (c *jwsCtx) UnmarshalJSON(a *acmeState, ac *acmeContext, jws []byte) error {
|
||||
func UnmarshalEabJwsJson(eabBytes []byte) (*jwsCtx, error) {
|
||||
var eabJws jwsCtx
|
||||
var err error
|
||||
if err = json.Unmarshal(eabBytes, &eabJws); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if eabJws.Kid == "" {
|
||||
return nil, fmt.Errorf("invalid header: got missing required field 'kid': %w", ErrMalformed)
|
||||
}
|
||||
|
||||
if _, present := AllowedEabJWSTypes[eabJws.Algo]; !present {
|
||||
return nil, fmt.Errorf("invalid header: unexpected value for 'algo': %w", ErrMalformed)
|
||||
}
|
||||
|
||||
return &eabJws, nil
|
||||
}
|
||||
|
||||
func (c *jwsCtx) UnmarshalOuterJwsJson(a *acmeState, ac *acmeContext, jws []byte) error {
|
||||
var err error
|
||||
if err = json.Unmarshal(jws, c); err != nil {
|
||||
return err
|
||||
@@ -158,3 +183,91 @@ func (c *jwsCtx) VerifyJWS(signature string) (map[string]interface{}, error) {
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func verifyEabPayload(acmeState *acmeState, ac *acmeContext, outer *jwsCtx, expectedPath string, payload map[string]interface{}) (*eabType, error) {
|
||||
// Parse the key out.
|
||||
rawProtectedBase64, ok := payload["protected"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing required field 'protected': %w", ErrMalformed)
|
||||
}
|
||||
jwkBase64 := rawProtectedBase64.(string)
|
||||
|
||||
jwkBytes, err := base64.RawURLEncoding.DecodeString(jwkBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to base64 parse eab 'protected': %s: %w", err, ErrMalformed)
|
||||
}
|
||||
|
||||
eabJws, err := UnmarshalEabJwsJson(jwkBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to json unmarshal eab 'protected': %w", err)
|
||||
}
|
||||
|
||||
if len(eabJws.Url) == 0 {
|
||||
return nil, fmt.Errorf("missing required parameter 'url' in eab 'protected': %w", ErrMalformed)
|
||||
}
|
||||
expectedUrl := ac.clusterUrl.JoinPath(expectedPath).String()
|
||||
if expectedUrl != eabJws.Url {
|
||||
return nil, fmt.Errorf("invalid value for 'url' in eab 'protected': got '%v' expected '%v': %w", eabJws.Url, expectedUrl, ErrUnauthorized)
|
||||
}
|
||||
|
||||
if len(eabJws.Nonce) != 0 {
|
||||
return nil, fmt.Errorf("nonce should not be provided in eab 'protected': %w", ErrMalformed)
|
||||
}
|
||||
|
||||
rawPayloadBase64, ok := payload["payload"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing required field eab 'payload': %w", ErrMalformed)
|
||||
}
|
||||
payloadBase64, ok := rawPayloadBase64.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse 'payload' field: %w", ErrMalformed)
|
||||
}
|
||||
|
||||
rawSignatureBase64, ok := payload["signature"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing required field 'signature': %w", ErrMalformed)
|
||||
}
|
||||
signatureBase64, ok := rawSignatureBase64.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse 'signature' field: %w", ErrMalformed)
|
||||
}
|
||||
|
||||
// go-jose only seems to support compact signature encodings.
|
||||
compactSig := fmt.Sprintf("%v.%v.%v", jwkBase64, payloadBase64, signatureBase64)
|
||||
sig, err := jose.ParseSigned(compactSig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing eab signature: %s: %w", err, ErrMalformed)
|
||||
}
|
||||
|
||||
if len(sig.Signatures) > 1 {
|
||||
// See RFC 8555 Section 6.2. Request Authentication:
|
||||
//
|
||||
// > The JWS MUST NOT have multiple signatures
|
||||
return nil, fmt.Errorf("eab had multiple signatures: %w", ErrMalformed)
|
||||
}
|
||||
|
||||
if hasValues(sig.Signatures[0].Unprotected) {
|
||||
// See RFC 8555 Section 6.2. Request Authentication:
|
||||
//
|
||||
// > The JWS Unprotected Header [RFC7515] MUST NOT be used
|
||||
return nil, fmt.Errorf("eab had unprotected headers: %w", ErrMalformed)
|
||||
}
|
||||
|
||||
// Load the EAB to validate the signature against
|
||||
eabEntry, err := acmeState.LoadEab(ac.sc, eabJws.Kid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: failed to verify eab", ErrUnauthorized)
|
||||
}
|
||||
|
||||
verifiedPayload, err := sig.Verify(eabEntry.MacKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure how eab payload matches the outer JWK key value
|
||||
if !bytes.Equal(outer.Jwk, verifiedPayload) {
|
||||
return nil, fmt.Errorf("eab payload does not match outer JWK key: %w", ErrMalformed)
|
||||
}
|
||||
|
||||
return eabEntry, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -35,6 +36,7 @@ const (
|
||||
acmeAccountPrefix = acmePathPrefix + "accounts/"
|
||||
acmeThumbprintPrefix = acmePathPrefix + "account-thumbprints/"
|
||||
acmeValidationPrefix = acmePathPrefix + "validations/"
|
||||
acmeEabPrefix = acmePathPrefix + "eab/"
|
||||
)
|
||||
|
||||
type acmeState struct {
|
||||
@@ -219,6 +221,7 @@ type acmeAccount struct {
|
||||
AcmeDirectory string `json:"acme-directory"`
|
||||
AccountCreatedDate time.Time `json:"account_created_date"`
|
||||
AccountRevokedDate time.Time `json:"account_revoked_date"`
|
||||
Eab *eabType `json:"eab"`
|
||||
}
|
||||
|
||||
type acmeOrder struct {
|
||||
@@ -257,7 +260,7 @@ func (o acmeOrder) getIdentifierIPValues() []net.IP {
|
||||
return identifiers
|
||||
}
|
||||
|
||||
func (a *acmeState) CreateAccount(ac *acmeContext, c *jwsCtx, contact []string, termsOfServiceAgreed bool) (*acmeAccount, error) {
|
||||
func (a *acmeState) CreateAccount(ac *acmeContext, c *jwsCtx, contact []string, termsOfServiceAgreed bool, eab *eabType) (*acmeAccount, error) {
|
||||
// Write out the thumbprint value/entry out first, if we get an error mid-way through
|
||||
// this is easier to recover from. The new kid with the same existing public key
|
||||
// will rewrite the thumbprint entry. This goes in hand with LoadAccountByKey that
|
||||
@@ -291,6 +294,7 @@ func (a *acmeState) CreateAccount(ac *acmeContext, c *jwsCtx, contact []string,
|
||||
Status: StatusValid,
|
||||
AcmeDirectory: ac.acmeDirectory,
|
||||
AccountCreatedDate: time.Now(),
|
||||
Eab: eab,
|
||||
}
|
||||
json, err := logical.StorageEntryJSON(acmeAccountPrefix+c.Kid, acct)
|
||||
if err != nil {
|
||||
@@ -470,7 +474,7 @@ func (a *acmeState) ParseRequestParams(ac *acmeContext, req *logical.Request, da
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to base64 parse 'protected': %s: %w", err, ErrMalformed)
|
||||
}
|
||||
if err = c.UnmarshalJSON(a, ac, jwkBytes); err != nil {
|
||||
if err = c.UnmarshalOuterJwsJson(a, ac, jwkBytes); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to json unmarshal 'protected': %w", err)
|
||||
}
|
||||
|
||||
@@ -633,6 +637,65 @@ func (a *acmeState) GetIssuedCert(ac *acmeContext, accountId string, serial stri
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
func (a *acmeState) SaveEab(sc *storageContext, eab *eabType) error {
|
||||
json, err := logical.StorageEntryJSON(path.Join(acmeEabPrefix, eab.KeyID), eab)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sc.Storage.Put(sc.Context, json)
|
||||
}
|
||||
|
||||
func (a *acmeState) LoadEab(sc *storageContext, eabKid string) (*eabType, error) {
|
||||
rawEntry, err := sc.Storage.Get(sc.Context, path.Join(acmeEabPrefix, eabKid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rawEntry == nil {
|
||||
return nil, fmt.Errorf("no eab found for kid %s", eabKid)
|
||||
}
|
||||
|
||||
var eab eabType
|
||||
err = rawEntry.DecodeJSON(&eab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eab.KeyID = eabKid
|
||||
return &eab, nil
|
||||
}
|
||||
|
||||
func (a *acmeState) DeleteEab(sc *storageContext, eabKid string) (bool, error) {
|
||||
rawEntry, err := sc.Storage.Get(sc.Context, path.Join(acmeEabPrefix, eabKid))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if rawEntry == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = sc.Storage.Delete(sc.Context, path.Join(acmeEabPrefix, eabKid))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *acmeState) ListEabIds(sc *storageContext) ([]string, error) {
|
||||
entries, err := sc.Storage.List(sc.Context, acmeEabPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ids []string
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry, "/") {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, entry)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func getAuthorizationPath(accountId string, authId string) string {
|
||||
return acmeAccountPrefix + accountId + "/authorizations/" + authId
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type acmeContext struct {
|
||||
// acmeDirectory is a string that can distinguish the various acme directories we have configured
|
||||
// if something needs to remain locked into a directory path structure.
|
||||
acmeDirectory string
|
||||
eabPolicy EabPolicy
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -60,7 +61,13 @@ func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
|
||||
return nil, fmt.Errorf("failed to fetch ACME configuration: %w", err)
|
||||
}
|
||||
|
||||
if isAcmeDisabled(sc, config) {
|
||||
// use string form in case someone messes up our config from raw storage.
|
||||
eabPolicy, err := getEabPolicyByString(string(config.EabPolicyName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isAcmeDisabled(sc, config, eabPolicy) {
|
||||
return nil, ErrAcmeDisabled
|
||||
}
|
||||
|
||||
@@ -87,6 +94,7 @@ func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
|
||||
role: role,
|
||||
issuer: issuer,
|
||||
acmeDirectory: acmeDirectory,
|
||||
eabPolicy: eabPolicy,
|
||||
}
|
||||
|
||||
return op(acmeCtx, r, data)
|
||||
@@ -185,6 +193,10 @@ func (b *backend) acmeAccountRequiredWrapper(op acmeAccountRequiredOperation) fr
|
||||
return nil, fmt.Errorf("error loading account: %w", err)
|
||||
}
|
||||
|
||||
if err = acmeCtx.eabPolicy.EnforceForExistingAccount(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if account.Status != StatusValid {
|
||||
// Treating "revoked" and "deactivated" as the same here.
|
||||
return nil, fmt.Errorf("%w: account in status: %s", ErrUnauthorized, account.Status)
|
||||
@@ -373,7 +385,7 @@ func getRequestedAcmeIssuerFromPath(data *framework.FieldData) string {
|
||||
return requestedIssuer
|
||||
}
|
||||
|
||||
func isAcmeDisabled(sc *storageContext, config *acmeConfigEntry) bool {
|
||||
func isAcmeDisabled(sc *storageContext, config *acmeConfigEntry, policy EabPolicy) bool {
|
||||
if !config.Enabled {
|
||||
return true
|
||||
}
|
||||
@@ -387,8 +399,9 @@ func isAcmeDisabled(sc *storageContext, config *acmeConfigEntry) bool {
|
||||
|
||||
// The OS environment if true will override any configuration option.
|
||||
if disableAcme {
|
||||
// TODO: If EAB is enforced in the configuration, don't mark
|
||||
// ACME as disabled.
|
||||
if policy.OverrideEnvDisablingPublicAcme() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +218,8 @@ func Backend(conf *logical.BackendConfig) *backend {
|
||||
|
||||
// ACME
|
||||
pathAcmeConfig(&b),
|
||||
pathAcmeEabCreateList(&b),
|
||||
pathAcmeEabDelete(&b),
|
||||
},
|
||||
|
||||
Secrets: []*framework.Secret{
|
||||
|
||||
@@ -6721,7 +6721,7 @@ func TestProperAuthing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serial := resp.Data["serial_number"].(string)
|
||||
|
||||
eabKid := "13b80844-e60d-42d2-b7e9-152a8e834b90"
|
||||
paths := map[string]pathAuthChecker{
|
||||
"ca_chain": shouldBeUnauthedReadList,
|
||||
"cert/ca_chain": shouldBeUnauthedReadList,
|
||||
@@ -6839,6 +6839,8 @@ func TestProperAuthing(t *testing.T) {
|
||||
"unified-crl/delta/pem": shouldBeUnauthedReadList,
|
||||
"unified-ocsp": shouldBeUnauthedWriteOnly,
|
||||
"unified-ocsp/dGVzdAo=": shouldBeUnauthedReadList,
|
||||
"acme/eab": shouldBeAuthed,
|
||||
"acme/eab/" + eabKid: shouldBeAuthed,
|
||||
}
|
||||
|
||||
// Add ACME based paths to the test suite
|
||||
@@ -6912,6 +6914,9 @@ func TestProperAuthing(t *testing.T) {
|
||||
if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{order_id}") {
|
||||
raw_path = strings.ReplaceAll(raw_path, "{order_id}", "13b80844-e60d-42d2-b7e9-152a8e834b90")
|
||||
}
|
||||
if strings.Contains(raw_path, "acme/eab") && strings.Contains(raw_path, "{key_id}") {
|
||||
raw_path = strings.ReplaceAll(raw_path, "{key_id}", eabKid)
|
||||
}
|
||||
|
||||
handler, present := paths[raw_path]
|
||||
if !present {
|
||||
|
||||
@@ -109,6 +109,7 @@ func (b *backend) acmeNewAccountHandler(acmeCtx *acmeContext, r *logical.Request
|
||||
var contacts []string
|
||||
var termsOfServiceAgreed bool
|
||||
var status string
|
||||
var eabData map[string]interface{}
|
||||
|
||||
rawContact, present := data["contact"]
|
||||
if present {
|
||||
@@ -152,20 +153,39 @@ func (b *backend) acmeNewAccountHandler(acmeCtx *acmeContext, r *logical.Request
|
||||
}
|
||||
}
|
||||
|
||||
// We ignore the EAB parameter as it is currently not supported.
|
||||
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.acmeNewAccountSearchHandler(acmeCtx, userCtx)
|
||||
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)
|
||||
return b.acmeNewAccountCreateHandler(acmeCtx, userCtx, contacts, termsOfServiceAgreed, r, eabData)
|
||||
}
|
||||
|
||||
return b.acmeNewAccountUpdateHandler(acmeCtx, userCtx, contacts, status)
|
||||
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 {
|
||||
@@ -188,7 +208,7 @@ func formatAccountResponse(acmeCtx *acmeContext, acct *acmeAccount) *logical.Res
|
||||
return resp
|
||||
}
|
||||
|
||||
func (b *backend) acmeNewAccountSearchHandler(acmeCtx *acmeContext, userCtx *jwsCtx) (*logical.Response, error) {
|
||||
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)
|
||||
@@ -200,6 +220,9 @@ func (b *backend) acmeNewAccountSearchHandler(acmeCtx *acmeContext, userCtx *jws
|
||||
}
|
||||
|
||||
if account != nil {
|
||||
if err = acmeCtx.eabPolicy.EnforceForExistingAccount(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return formatAccountResponse(acmeCtx, account), nil
|
||||
}
|
||||
|
||||
@@ -211,7 +234,7 @@ func (b *backend) acmeNewAccountSearchHandler(acmeCtx *acmeContext, userCtx *jws
|
||||
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) (*logical.Response, error) {
|
||||
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)
|
||||
}
|
||||
@@ -228,23 +251,58 @@ func (b *backend) acmeNewAccountCreateHandler(acmeCtx *acmeContext, userCtx *jws
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 := formatAccountResponse(acmeCtx, accountByKid)
|
||||
resp := formatNewAccountResponse(acmeCtx, accountByKid, eabData)
|
||||
|
||||
// Per RFC 8555 Section 7.3. Account Management:
|
||||
//
|
||||
@@ -254,16 +312,24 @@ func (b *backend) acmeNewAccountCreateHandler(acmeCtx *acmeContext, userCtx *jws
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *backend) acmeNewAccountUpdateHandler(acmeCtx *acmeContext, userCtx *jwsCtx, contact []string, status string) (*logical.Response, error) {
|
||||
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 != StatusValid {
|
||||
|
||||
@@ -48,8 +48,9 @@ func (b *backend) acmeDirectoryHandler(acmeCtx *acmeContext, r *logical.Request,
|
||||
"newOrder": acmeCtx.baseUrl.JoinPath("new-order").String(),
|
||||
"revokeCert": acmeCtx.baseUrl.JoinPath("revoke-cert").String(),
|
||||
"keyChange": acmeCtx.baseUrl.JoinPath("key-change").String(),
|
||||
// This is purposefully missing newAuthz as we don't support pre-authorization
|
||||
"meta": map[string]interface{}{
|
||||
"externalAccountRequired": false,
|
||||
"externalAccountRequired": acmeCtx.eabPolicy.IsExternalAccountRequired(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
200
builtin/logical/pki/path_acme_eab.go
Normal file
200
builtin/logical/pki/path_acme_eab.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package pki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
/*
|
||||
* This file unlike the other path_acme_xxx.go are VAULT APIs to manage the
|
||||
* ACME External Account Bindings, this isn't providing any APIs that an ACME
|
||||
* client would use.
|
||||
*/
|
||||
func pathAcmeEabCreateList(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "acme/eab",
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixPKI,
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationSuffix: "acme-configuration",
|
||||
},
|
||||
Callback: b.pathAcmeListEab,
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.pathAcmeCreateEab,
|
||||
ForwardPerformanceSecondary: false,
|
||||
ForwardPerformanceStandby: true,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationVerb: "configure",
|
||||
OperationSuffix: "acme",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: "",
|
||||
HelpDescription: "",
|
||||
}
|
||||
}
|
||||
|
||||
func pathAcmeEabDelete(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "acme/eab/" + uuidNameRegex("key_id"),
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixPKI,
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"key_id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "EAB key identifier",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.DeleteOperation: &framework.PathOperation{
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationSuffix: "acme-configuration",
|
||||
},
|
||||
Callback: b.pathAcmeDeleteEab,
|
||||
ForwardPerformanceSecondary: false,
|
||||
ForwardPerformanceStandby: true,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: "",
|
||||
HelpDescription: "",
|
||||
}
|
||||
}
|
||||
|
||||
type eabType struct {
|
||||
KeyID string `json:"-"`
|
||||
KeyType string `json:"key-type"`
|
||||
KeyBits string `json:"key-bits"`
|
||||
MacKey []byte `json:"mac-key"`
|
||||
CreatedOn time.Time `json:"created-on"`
|
||||
}
|
||||
|
||||
func (b *backend) pathAcmeListEab(ctx context.Context, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
sc := b.makeStorageContext(ctx, r.Storage)
|
||||
|
||||
eabIds, err := b.acmeState.ListEabIds(sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var warnings []string
|
||||
var keyIds []string
|
||||
keyInfos := map[string]interface{}{}
|
||||
|
||||
for _, eabKey := range eabIds {
|
||||
eab, err := b.acmeState.LoadEab(sc, eabKey)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("failed loading eab entry %s: %v", eabKey, err))
|
||||
continue
|
||||
}
|
||||
|
||||
keyIds = append(keyIds, eab.KeyID)
|
||||
keyInfos[eab.KeyID] = map[string]interface{}{
|
||||
"key_type": eab.KeyType,
|
||||
"key_bits": eab.KeyBits,
|
||||
"created_on": eab.CreatedOn.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
resp := logical.ListResponseWithInfo(keyIds, keyInfos)
|
||||
for _, warning := range warnings {
|
||||
resp.AddWarning(warning)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathAcmeCreateEab(ctx context.Context, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
kid := genUuid()
|
||||
macKey, err := generateEabKey(b.GetRandomReader())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed generating eab key: %w", err)
|
||||
}
|
||||
|
||||
eab := &eabType{
|
||||
KeyID: kid,
|
||||
KeyType: "ec",
|
||||
KeyBits: "256",
|
||||
MacKey: macKey,
|
||||
CreatedOn: time.Now(),
|
||||
}
|
||||
|
||||
sc := b.makeStorageContext(ctx, r.Storage)
|
||||
err = b.acmeState.SaveEab(sc, eab)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed saving generated eab: %w", err)
|
||||
}
|
||||
|
||||
encodedKey := base64.RawURLEncoding.EncodeToString(macKey)
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"id": eab.KeyID,
|
||||
"key_type": eab.KeyType,
|
||||
"key_bits": eab.KeyBits,
|
||||
"private_key": encodedKey,
|
||||
"created_on": eab.CreatedOn.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathAcmeDeleteEab(ctx context.Context, r *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
sc := b.makeStorageContext(ctx, r.Storage)
|
||||
keyId := d.Get("key_id").(string)
|
||||
|
||||
_, err := uuid.ParseUUID(keyId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("badly formatted key_id field")
|
||||
}
|
||||
|
||||
deleted, err := b.acmeState.DeleteEab(sc, keyId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed deleting key id: %w", err)
|
||||
}
|
||||
|
||||
resp := &logical.Response{}
|
||||
if !deleted {
|
||||
resp.AddWarning("No key id found with id: " + keyId)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func generateEabKey(random io.Reader) ([]byte, error) {
|
||||
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), random)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyBytes, err := x509.MarshalECPrivateKey(ecKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keyBytes, nil
|
||||
}
|
||||
77
builtin/logical/pki/path_acme_eab_test.go
Normal file
77
builtin/logical/pki/path_acme_eab_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package pki
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestACME_EabVaultAPIs verify the various Vault auth'd APIs for EAB work as expected,
|
||||
// with creation, listing and deletions.
|
||||
func TestACME_EabVaultAPIs(t *testing.T) {
|
||||
b, s := CreateBackendWithStorage(t)
|
||||
|
||||
var ids []string
|
||||
|
||||
// Generate an EAB
|
||||
resp, err := CBWrite(b, s, "acme/eab", map[string]interface{}{})
|
||||
requireSuccessNonNilResponse(t, resp, err, "Failed generating eab")
|
||||
requireFieldsSetInResp(t, resp, "id", "key_type", "key_bits", "private_key", "created_on")
|
||||
require.Equal(t, "ec", resp.Data["key_type"])
|
||||
require.Equal(t, "256", resp.Data["key_bits"])
|
||||
ids = append(ids, resp.Data["id"].(string))
|
||||
_, err = uuid.ParseUUID(resp.Data["id"].(string))
|
||||
require.NoError(t, err, "failed parsing id as a uuid")
|
||||
|
||||
key, err := base64.RawURLEncoding.DecodeString(resp.Data["private_key"].(string))
|
||||
require.NoError(t, err, "failed base64 decoding private key")
|
||||
_, err = x509.ParseECPrivateKey(key)
|
||||
require.NoError(t, err, "failed parsing private key")
|
||||
|
||||
// Generate another EAB
|
||||
resp, err = CBWrite(b, s, "acme/eab", map[string]interface{}{})
|
||||
requireSuccessNonNilResponse(t, resp, err, "Failed generating eab")
|
||||
ids = append(ids, resp.Data["id"].(string))
|
||||
|
||||
// List our EABs
|
||||
resp, err = CBList(b, s, "acme/eab")
|
||||
requireSuccessNonNilResponse(t, resp, err, "failed list")
|
||||
|
||||
require.ElementsMatch(t, ids, resp.Data["keys"])
|
||||
keyInfo := resp.Data["key_info"].(map[string]interface{})
|
||||
id0Map := keyInfo[ids[0]].(map[string]interface{})
|
||||
require.Equal(t, "ec", id0Map["key_type"])
|
||||
require.Equal(t, "256", id0Map["key_bits"])
|
||||
require.NotEmpty(t, id0Map["created_on"])
|
||||
_, err = time.Parse(time.RFC3339, id0Map["created_on"].(string))
|
||||
require.NoError(t, err, "failed to parse created_on date: %s", id0Map["created_on"])
|
||||
|
||||
id1Map := keyInfo[ids[1]].(map[string]interface{})
|
||||
|
||||
require.Equal(t, "ec", id1Map["key_type"])
|
||||
require.Equal(t, "256", id1Map["key_bits"])
|
||||
require.NotEmpty(t, id1Map["created_on"])
|
||||
|
||||
// Delete an EAB
|
||||
resp, err = CBDelete(b, s, "acme/eab/"+ids[0])
|
||||
requireSuccessNonNilResponse(t, resp, err, "failed deleting eab identifier")
|
||||
require.Len(t, resp.Warnings, 0, "no warnings should have been set on delete")
|
||||
|
||||
// Make sure it's really gone
|
||||
resp, err = CBList(b, s, "acme/eab")
|
||||
requireSuccessNonNilResponse(t, resp, err, "failed list post delete")
|
||||
require.Len(t, resp.Data["keys"], 1)
|
||||
require.Contains(t, resp.Data["keys"], ids[1])
|
||||
|
||||
// Delete the same EAB again, we should just get a warning but still success.
|
||||
resp, err = CBDelete(b, s, "acme/eab/"+ids[0])
|
||||
requireSuccessNonNilResponse(t, resp, err, "failed deleting eab identifier")
|
||||
require.Len(t, resp.Warnings, 1, "expected a warning to be set on repeated delete call")
|
||||
}
|
||||
@@ -73,6 +73,7 @@ func TestAcmeBasicWorkflow(t *testing.T) {
|
||||
require.Equal(t, discoveryBaseUrl+"new-order", discovery.OrderURL)
|
||||
require.Equal(t, discoveryBaseUrl+"revoke-cert", discovery.RevokeURL)
|
||||
require.Equal(t, discoveryBaseUrl+"key-change", discovery.KeyChangeURL)
|
||||
require.False(t, discovery.ExternalAccountRequired, "bad value for external account required in directory")
|
||||
|
||||
// Attempt to update prior to creating an account
|
||||
t.Logf("Testing updates with no proper account fail on %s", baseAcmeURL)
|
||||
@@ -310,6 +311,75 @@ func TestAcmeBasicWorkflow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAcmeBasicWorkflowWithEab verify that new accounts require EAB's if enforced by configuration.
|
||||
func TestAcmeBasicWorkflowWithEab(t *testing.T) {
|
||||
t.Parallel()
|
||||
cluster, client, _ := setupAcmeBackend(t)
|
||||
defer cluster.Cleanup()
|
||||
testCtx := context.Background()
|
||||
|
||||
// Enable EAB
|
||||
_, err := client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{
|
||||
"enabled": true,
|
||||
"eab_policy": "always-required",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
baseAcmeURL := "/v1/pki/acme/"
|
||||
accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err, "failed creating ec key")
|
||||
|
||||
acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey)
|
||||
|
||||
t.Logf("Testing discover on %s", baseAcmeURL)
|
||||
discovery, err := acmeClient.Discover(testCtx)
|
||||
require.NoError(t, err, "failed acme discovery call")
|
||||
require.True(t, discovery.ExternalAccountRequired, "bad value for external account required in directory")
|
||||
|
||||
// Create new account without EAB, should fail
|
||||
t.Logf("Testing register on %s", baseAcmeURL)
|
||||
_, err = acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true })
|
||||
require.ErrorContains(t, err, "urn:ietf:params:acme:error:externalAccountRequired",
|
||||
"expected failure creating an account without eab")
|
||||
|
||||
kid, eabKeyBytes := getEABKey(t, client)
|
||||
acct := &acme.Account{
|
||||
ExternalAccountBinding: &acme.ExternalAccountBinding{
|
||||
KID: kid,
|
||||
Key: eabKeyBytes,
|
||||
},
|
||||
}
|
||||
|
||||
// Create new account with EAB
|
||||
t.Logf("Testing register on %s", baseAcmeURL)
|
||||
_, err = acmeClient.Register(testCtx, acct, func(tosURL string) bool { return true })
|
||||
require.NoError(t, err, "failed registering new account with eab")
|
||||
|
||||
// Make sure our EAB is no longer available
|
||||
resp, err := client.Logical().ListWithContext(context.Background(), "pki/acme/eab")
|
||||
require.NoError(t, err, "failed to list eab tokens")
|
||||
require.Nil(t, resp, "list response for eab tokens should have been nil due to empty list")
|
||||
|
||||
// Attempt to create another account with the same EAB as before -- should fail
|
||||
accountKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err, "failed creating ec key")
|
||||
|
||||
acmeClient2 := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey2)
|
||||
acct2 := &acme.Account{
|
||||
ExternalAccountBinding: &acme.ExternalAccountBinding{
|
||||
KID: kid,
|
||||
Key: eabKeyBytes,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = acmeClient2.Register(testCtx, acct2, func(tosURL string) bool { return true })
|
||||
require.ErrorContains(t, err, "urn:ietf:params:acme:error:unauthorized", "should fail due to EAB re-use")
|
||||
|
||||
// We can lookup/find an existing account without EAB if we have the account key
|
||||
_, err = acmeClient.GetReg(testCtx /* unused url */, "")
|
||||
require.NoError(t, err, "expected to lookup existing account without eab")
|
||||
}
|
||||
|
||||
// TestAcmeNonce a basic test that will validate we get back a nonce with the proper status codes
|
||||
// based on the
|
||||
func TestAcmeNonce(t *testing.T) {
|
||||
@@ -477,7 +547,8 @@ func setupAcmeBackend(t *testing.T) (*vault.TestCluster, *api.Client, string) {
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.Logical().WriteWithContext(context.Background(), "pki/config/acme", map[string]interface{}{
|
||||
"enabled": true,
|
||||
"enabled": true,
|
||||
"eab_policy": "not-required",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -621,3 +692,18 @@ func getAcmeClientForCluster(t *testing.T, cluster *vault.TestCluster, baseUrl s
|
||||
DirectoryURL: baseAcmeURL + "directory",
|
||||
}
|
||||
}
|
||||
|
||||
func getEABKey(t *testing.T, client *api.Client) (string, []byte) {
|
||||
resp, err := client.Logical().WriteWithContext(ctx, "pki/acme/eab", map[string]interface{}{})
|
||||
require.NoError(t, err, "failed getting eab key")
|
||||
require.NotNil(t, resp, "eab key returned nil response")
|
||||
require.NotEmpty(t, resp.Data["id"], "eab key response missing id field")
|
||||
kid := resp.Data["id"].(string)
|
||||
|
||||
require.NotEmpty(t, resp.Data["private_key"], "eab key response missing private_key field")
|
||||
base64Key := resp.Data["private_key"].(string)
|
||||
privateKeyBytes, err := base64.RawURLEncoding.DecodeString(base64Key)
|
||||
require.NoError(t, err, "failed base 64 decoding eab key response")
|
||||
|
||||
return kid, privateKeyBytes
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ const (
|
||||
)
|
||||
|
||||
type acmeConfigEntry struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AllowedIssuers []string `json:"allowed_issuers="`
|
||||
AllowedRoles []string `json:"allowed_roles"`
|
||||
DefaultRole string `json:"default_role"`
|
||||
DNSResolver string `json:"dns_resolver"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AllowedIssuers []string `json:"allowed_issuers="`
|
||||
AllowedRoles []string `json:"allowed_roles"`
|
||||
DefaultRole string `json:"default_role"`
|
||||
DNSResolver string `json:"dns_resolver"`
|
||||
EabPolicyName EabPolicyName `json:"eab_policy_name"`
|
||||
}
|
||||
|
||||
var defaultAcmeConfig = acmeConfigEntry{
|
||||
@@ -30,6 +31,7 @@ var defaultAcmeConfig = acmeConfigEntry{
|
||||
AllowedRoles: []string{"*"},
|
||||
DefaultRole: "",
|
||||
DNSResolver: "",
|
||||
EabPolicyName: eabPolicyAlwaysRequired,
|
||||
}
|
||||
|
||||
func (sc *storageContext) getAcmeConfig() (*acmeConfigEntry, error) {
|
||||
@@ -99,6 +101,11 @@ func pathAcmeConfig(b *backend) *framework.Path {
|
||||
Description: `DNS resolver to use for domain resolution on this mount. Defaults to using the default system resolver. Must be in the format <host>:<port>, with both parts mandatory.`,
|
||||
Default: "",
|
||||
},
|
||||
"eab_policy": {
|
||||
Type: framework.TypeString,
|
||||
Description: `Specify the policy to use for external account binding behaviour, 'not-required', 'new-account-required' or 'always-required'`,
|
||||
Default: "always-required",
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
@@ -143,6 +150,7 @@ func genResponseFromAcmeConfig(config *acmeConfigEntry) *logical.Response {
|
||||
"default_role": config.DefaultRole,
|
||||
"enabled": config.Enabled,
|
||||
"dns_resolver": config.DNSResolver,
|
||||
"eab_policy": config.EabPolicyName,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -197,6 +205,15 @@ func (b *backend) pathAcmeWrite(ctx context.Context, req *logical.Request, d *fr
|
||||
}
|
||||
}
|
||||
|
||||
if eabPolicyRaw, ok := d.GetOk("eab_policy"); ok {
|
||||
eabPolicy, err := getEabPolicyByString(eabPolicyRaw.(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid eab policy name provided, valid values are '%s', '%s', '%s'",
|
||||
eabPolicyNotRequired, eabPolicyNewAccountRequired, eabPolicyAlwaysRequired)
|
||||
}
|
||||
config.EabPolicyName = eabPolicy.Name
|
||||
}
|
||||
|
||||
allowAnyRole := len(config.AllowedRoles) == 1 && config.AllowedRoles[0] == "*"
|
||||
if !allowAnyRole {
|
||||
foundDefault := len(config.DefaultRole) == 0
|
||||
|
||||
@@ -189,7 +189,9 @@ func (vpc *VaultPkiCluster) CreateAcmeMount(mountName string) (*VaultPkiMount, e
|
||||
return nil, fmt.Errorf("failed updating cluster config: %w", err)
|
||||
}
|
||||
|
||||
cfg := map[string]interface{}{}
|
||||
cfg := map[string]interface{}{
|
||||
"eab_policy": "not-required",
|
||||
}
|
||||
if vpc.Dns != nil {
|
||||
cfg["dns_resolver"] = vpc.Dns.GetRemoteAddr()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user