backport of commit 000d754c40b5daaae21e97dd548d3c308c7c6475 (#20870)

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
This commit is contained in:
hc-github-team-secure-vault-core 2023-05-30 15:34:01 -04:00 committed by GitHub
parent a1d3c88f56
commit da127db836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 363 additions and 198 deletions

View File

@ -269,5 +269,10 @@ func verifyEabPayload(acmeState *acmeState, ac *acmeContext, outer *jwsCtx, expe
return nil, fmt.Errorf("eab payload does not match outer JWK key: %w", ErrMalformed) return nil, fmt.Errorf("eab payload does not match outer JWK key: %w", ErrMalformed)
} }
if eabEntry.AcmeDirectory != ac.acmeDirectory {
// This EAB was not created for this specific ACME directory, reject it
return nil, fmt.Errorf("%w: failed to verify eab", ErrUnauthorized)
}
return eabEntry, nil return eabEntry, nil
} }

View File

@ -77,7 +77,7 @@ func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
return nil, fmt.Errorf("%w: Can not perform ACME operations until migration has completed", ErrServerInternal) return nil, fmt.Errorf("%w: Can not perform ACME operations until migration has completed", ErrServerInternal)
} }
acmeBaseUrl, clusterBase, err := getAcmeBaseUrl(sc, r.Path) acmeBaseUrl, clusterBase, err := getAcmeBaseUrl(sc, r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -87,7 +87,10 @@ func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
return nil, err return nil, err
} }
acmeDirectory := getAcmeDirectory(data) acmeDirectory, err := getAcmeDirectory(r)
if err != nil {
return nil, err
}
acmeCtx := &acmeContext{ acmeCtx := &acmeContext{
baseUrl: acmeBaseUrl, baseUrl: acmeBaseUrl,
@ -237,19 +240,18 @@ func buildAcmeFrameworkPaths(b *backend, patternFunc func(b *backend, pattern st
return patterns return patterns
} }
func getAcmeBaseUrl(sc *storageContext, path string) (*url.URL, *url.URL, error) { func getAcmeBaseUrl(sc *storageContext, r *logical.Request) (*url.URL, *url.URL, error) {
baseUrl, err := getBasePathFromClusterConfig(sc) baseUrl, err := getBasePathFromClusterConfig(sc)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
directoryPrefix := "" directoryPrefix, err := getAcmeDirectory(r)
lastIndex := strings.LastIndex(path, "/acme/") if err != nil {
if lastIndex != -1 { return nil, nil, err
directoryPrefix = path[0:lastIndex]
} }
return baseUrl.JoinPath(directoryPrefix, "/acme/"), baseUrl, nil return baseUrl.JoinPath(directoryPrefix), baseUrl, nil
} }
func getBasePathFromClusterConfig(sc *storageContext) (*url.URL, error) { func getBasePathFromClusterConfig(sc *storageContext) (*url.URL, error) {
@ -291,11 +293,21 @@ func getAcmeIssuer(sc *storageContext, issuerName string) (*issuerEntry, error)
return nil, fmt.Errorf("%w: issuer missing proper issuance usage or key", ErrServerInternal) return nil, fmt.Errorf("%w: issuer missing proper issuance usage or key", ErrServerInternal)
} }
func getAcmeDirectory(data *framework.FieldData) string { // getAcmeDirectory return the base acme directory path, without a leading '/' and including
requestedIssuer := getRequestedAcmeIssuerFromPath(data) // the trailing /acme/ folder which is the root of all our various directories
requestedRole := getRequestedAcmeRoleFromPath(data) func getAcmeDirectory(r *logical.Request) (string, error) {
acmePath := r.Path
if !strings.HasPrefix(acmePath, "/") {
acmePath = "/" + acmePath
}
return fmt.Sprintf("issuer-%s::role-%s", requestedIssuer, requestedRole) lastIndex := strings.LastIndex(acmePath, "/acme/")
if lastIndex == -1 {
return "", fmt.Errorf("%w: unable to determine acme base folder path: %s", ErrServerInternal, acmePath)
}
// Skip the leading '/' and return our base path with the /acme/
return strings.TrimLeft(acmePath[0:lastIndex]+"/acme/", "/"), nil
} }
func getAcmeRoleAndIssuer(sc *storageContext, data *framework.FieldData, config *acmeConfigEntry) (*roleEntry, *issuerEntry, error) { func getAcmeRoleAndIssuer(sc *storageContext, data *framework.FieldData, config *acmeConfigEntry) (*roleEntry, *issuerEntry, error) {

View File

@ -6,6 +6,7 @@ package pki
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
@ -92,15 +93,20 @@ func TestACMEIssuerRoleLoading(t *testing.T) {
return nil, nil return nil, nil
}) })
var acmePath string
fieldRaw := map[string]interface{}{} fieldRaw := map[string]interface{}{}
if tt.roleName != "" {
fieldRaw["role"] = tt.roleName
}
if tt.issuerName != "" { if tt.issuerName != "" {
fieldRaw[issuerRefParam] = tt.issuerName fieldRaw[issuerRefParam] = tt.issuerName
acmePath = "issuer/" + tt.issuerName + "/"
}
if tt.roleName != "" {
fieldRaw["role"] = tt.roleName
acmePath = acmePath + "roles/" + tt.roleName + "/"
} }
resp, err := f(context.Background(), &logical.Request{Storage: s}, &framework.FieldData{ acmePath = strings.TrimLeft(acmePath+"/acme/directory", "/")
resp, err := f(context.Background(), &logical.Request{Path: acmePath, Storage: s}, &framework.FieldData{
Raw: fieldRaw, Raw: fieldRaw,
Schema: getCsrSignVerbatimSchemaFields(), Schema: getCsrSignVerbatimSchemaFields(),
}) })

View File

@ -218,7 +218,6 @@ func Backend(conf *logical.BackendConfig) *backend {
// ACME // ACME
pathAcmeConfig(&b), pathAcmeConfig(&b),
pathAcmeEabCreate(&b),
pathAcmeEabList(&b), pathAcmeEabList(&b),
pathAcmeEabDelete(&b), pathAcmeEabDelete(&b),
}, },
@ -248,6 +247,7 @@ func Backend(conf *logical.BackendConfig) *backend {
acmePaths = append(acmePaths, pathAcmeChallenge(&b)...) acmePaths = append(acmePaths, pathAcmeChallenge(&b)...)
acmePaths = append(acmePaths, pathAcmeAuthorization(&b)...) acmePaths = append(acmePaths, pathAcmeAuthorization(&b)...)
acmePaths = append(acmePaths, pathAcmeRevoke(&b)...) acmePaths = append(acmePaths, pathAcmeRevoke(&b)...)
acmePaths = append(acmePaths, pathAcmeNewEab(&b)...) // auth'd API that lives underneath the various /acme paths
for _, acmePath := range acmePaths { for _, acmePath := range acmePaths {
b.Backend.Paths = append(b.Backend.Paths, acmePath) b.Backend.Paths = append(b.Backend.Paths, acmePath)
@ -268,6 +268,7 @@ func Backend(conf *logical.BackendConfig) *backend {
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/order/+") b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/order/+")
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/order/+/finalize") b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/order/+/finalize")
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/order/+/cert") b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/order/+/cert")
// We specifically do NOT add acme/new-eab to this as it should be auth'd
} }
if constants.IsEnterprise { if constants.IsEnterprise {

View File

@ -6862,9 +6862,8 @@ func TestProperAuthing(t *testing.T) {
"unified-crl/delta/pem": shouldBeUnauthedReadList, "unified-crl/delta/pem": shouldBeUnauthedReadList,
"unified-ocsp": shouldBeUnauthedWriteOnly, "unified-ocsp": shouldBeUnauthedWriteOnly,
"unified-ocsp/dGVzdAo=": shouldBeUnauthedReadList, "unified-ocsp/dGVzdAo=": shouldBeUnauthedReadList,
"acme/new-eab": shouldBeAuthed, "eab": shouldBeAuthed,
"acme/eab": shouldBeAuthed, "eab/" + eabKid: shouldBeAuthed,
"acme/eab/" + eabKid: shouldBeAuthed,
} }
// Add ACME based paths to the test suite // Add ACME based paths to the test suite
@ -6881,6 +6880,9 @@ func TestProperAuthing(t *testing.T) {
paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90"] = shouldBeUnauthedWriteOnly paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90"] = shouldBeUnauthedWriteOnly
paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/finalize"] = shouldBeUnauthedWriteOnly paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/finalize"] = shouldBeUnauthedWriteOnly
paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/cert"] = shouldBeUnauthedWriteOnly paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/cert"] = shouldBeUnauthedWriteOnly
// Make sure this new-eab path is auth'd
paths[acmePrefix+"acme/new-eab"] = shouldBeAuthed
} }
for path, checkerType := range paths { for path, checkerType := range paths {
@ -6938,7 +6940,7 @@ func TestProperAuthing(t *testing.T) {
if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{order_id}") { 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") 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}") { if strings.Contains(raw_path, "eab") && strings.Contains(raw_path, "{key_id}") {
raw_path = strings.ReplaceAll(raw_path, "{key_id}", eabKid) raw_path = strings.ReplaceAll(raw_path, "{key_id}", eabKid)
} }

View File

@ -22,7 +22,7 @@ import (
*/ */
func pathAcmeEabList(b *backend) *framework.Path { func pathAcmeEabList(b *backend) *framework.Path {
return &framework.Path{ return &framework.Path{
Pattern: "acme/eab/?$", Pattern: "eab/?$",
DisplayAttrs: &framework.DisplayAttributes{ DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI, OperationPrefix: operationPrefixPKI,
@ -45,16 +45,17 @@ func pathAcmeEabList(b *backend) *framework.Path {
} }
} }
func pathAcmeEabCreate(b *backend) *framework.Path { func pathAcmeNewEab(b *backend) []*framework.Path {
return buildAcmeFrameworkPaths(b, patternAcmeNewEab, "/new-eab")
}
func patternAcmeNewEab(b *backend, pattern string) *framework.Path {
fields := map[string]*framework.FieldSchema{}
addFieldsForACMEPath(fields, pattern)
return &framework.Path{ return &framework.Path{
Pattern: "acme/new-eab", Pattern: pattern,
Fields: fields,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
},
Fields: map[string]*framework.FieldSchema{},
Operations: map[logical.Operation]framework.OperationHandler{ Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{ logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathAcmeCreateEab, Callback: b.pathAcmeCreateEab,
@ -67,15 +68,18 @@ func pathAcmeEabCreate(b *backend) *framework.Path {
}, },
}, },
HelpSynopsis: "Generate or list external account bindings to be used for ACME", DisplayAttrs: &framework.DisplayAttributes{
HelpDescription: `Generate single use id/key pairs to be used for ACME EAB or list OperationPrefix: operationPrefixPKI,
identifiers that have been generated but yet to be used.`, },
HelpSynopsis: "Generate external account bindings to be used for ACME",
HelpDescription: `Generate single use id/key pairs to be used for ACME EAB.`,
} }
} }
func pathAcmeEabDelete(b *backend) *framework.Path { func pathAcmeEabDelete(b *backend) *framework.Path {
return &framework.Path{ return &framework.Path{
Pattern: "acme/eab/" + uuidNameRegex("key_id"), Pattern: "eab/" + uuidNameRegex("key_id"),
DisplayAttrs: &framework.DisplayAttributes{ DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI, OperationPrefix: operationPrefixPKI,
@ -113,6 +117,7 @@ type eabType struct {
KeyType string `json:"key-type"` KeyType string `json:"key-type"`
KeyBits int `json:"key-bits"` KeyBits int `json:"key-bits"`
PrivateBytes []byte `json:"private-bytes"` PrivateBytes []byte `json:"private-bytes"`
AcmeDirectory string `json:"acme-directory"`
CreatedOn time.Time `json:"created-on"` CreatedOn time.Time `json:"created-on"`
} }
@ -139,6 +144,7 @@ func (b *backend) pathAcmeListEab(ctx context.Context, r *logical.Request, _ *fr
keyInfos[eab.KeyID] = map[string]interface{}{ keyInfos[eab.KeyID] = map[string]interface{}{
"key_type": eab.KeyType, "key_type": eab.KeyType,
"key_bits": eab.KeyBits, "key_bits": eab.KeyBits,
"acme_directory": eab.AcmeDirectory,
"created_on": eab.CreatedOn.Format(time.RFC3339), "created_on": eab.CreatedOn.Format(time.RFC3339),
} }
} }
@ -150,7 +156,7 @@ func (b *backend) pathAcmeListEab(ctx context.Context, r *logical.Request, _ *fr
return resp, nil return resp, nil
} }
func (b *backend) pathAcmeCreateEab(ctx context.Context, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) { func (b *backend) pathAcmeCreateEab(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
kid := genUuid() kid := genUuid()
size := 32 size := 32
bytes, err := uuid.GenerateRandomBytesWithReader(size, rand.Reader) bytes, err := uuid.GenerateRandomBytesWithReader(size, rand.Reader)
@ -158,11 +164,17 @@ func (b *backend) pathAcmeCreateEab(ctx context.Context, r *logical.Request, _ *
return nil, fmt.Errorf("failed generating eab key: %w", err) return nil, fmt.Errorf("failed generating eab key: %w", err)
} }
acmeDirectory, err := getAcmeDirectory(r)
if err != nil {
return nil, err
}
eab := &eabType{ eab := &eabType{
KeyID: kid, KeyID: kid,
KeyType: "hs", KeyType: "hs",
KeyBits: size * 8, KeyBits: size * 8,
PrivateBytes: bytes, PrivateBytes: bytes,
AcmeDirectory: acmeDirectory,
CreatedOn: time.Now(), CreatedOn: time.Now(),
} }
@ -180,6 +192,7 @@ func (b *backend) pathAcmeCreateEab(ctx context.Context, r *logical.Request, _ *
"key_type": eab.KeyType, "key_type": eab.KeyType,
"key_bits": eab.KeyBits, "key_bits": eab.KeyBits,
"key": encodedKey, "key": encodedKey,
"acme_directory": eab.AcmeDirectory,
"created_on": eab.CreatedOn.Format(time.RFC3339), "created_on": eab.CreatedOn.Format(time.RFC3339),
}, },
}, nil }, nil

View File

@ -1,75 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"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/new-eab", map[string]interface{}{})
requireSuccessNonNilResponse(t, resp, err, "Failed generating eab")
requireFieldsSetInResp(t, resp, "id", "key_type", "key_bits", "key", "created_on")
require.Equal(t, "hs", 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")
_, err = base64.RawURLEncoding.DecodeString(resp.Data["key"].(string))
require.NoError(t, err, "failed base64 decoding private key")
require.NoError(t, err, "failed parsing private key")
// Generate another EAB
resp, err = CBWrite(b, s, "acme/new-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, "hs", 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, "hs", 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")
}

View File

@ -45,16 +45,16 @@ func TestAcmeBasicWorkflow(t *testing.T) {
name string name string
prefixUrl string prefixUrl string
}{ }{
{"root", ""}, {"root", "acme/"},
{"role", "/roles/test-role"}, {"role", "roles/test-role/acme/"},
{"issuer", "/issuer/int-ca"}, {"issuer", "issuer/int-ca/acme/"},
{"issuer_role", "/issuer/int-ca/roles/test-role"}, {"issuer_role", "issuer/int-ca/roles/test-role/acme/"},
} }
testCtx := context.Background() testCtx := context.Background()
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
baseAcmeURL := "/v1/pki" + tc.prefixUrl + "/acme/" baseAcmeURL := "/v1/pki/" + tc.prefixUrl
accountKey, err := rsa.GenerateKey(rand.Reader, 2048) accountKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "failed creating rsa key") require.NoError(t, err, "failed creating rsa key")
@ -329,7 +329,19 @@ func TestAcmeBasicWorkflowWithEab(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
baseAcmeURL := "/v1/pki/acme/" cases := []struct {
name string
prefixUrl string
}{
{"root", "acme/"},
{"role", "roles/test-role/acme/"},
{"issuer", "issuer/int-ca/acme/"},
{"issuer_role", "issuer/int-ca/roles/test-role/acme/"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
baseAcmeURL := "/v1/pki/" + tc.prefixUrl
accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed creating ec key") require.NoError(t, err, "failed creating ec key")
@ -346,7 +358,26 @@ func TestAcmeBasicWorkflowWithEab(t *testing.T) {
require.ErrorContains(t, err, "urn:ietf:params:acme:error:externalAccountRequired", require.ErrorContains(t, err, "urn:ietf:params:acme:error:externalAccountRequired",
"expected failure creating an account without eab") "expected failure creating an account without eab")
kid, eabKeyBytes := getEABKey(t, client) // Test fetch, list, delete workflow
kid, _ := getEABKey(t, client, tc.prefixUrl)
resp, err := client.Logical().ListWithContext(testCtx, "pki/eab")
require.NoError(t, err, "failed to list eab tokens")
require.NotNil(t, resp, "list response for eab tokens should not be nil")
require.Contains(t, resp.Data, "keys")
require.Contains(t, resp.Data, "key_info")
require.Len(t, resp.Data["keys"], 1)
require.Contains(t, resp.Data["keys"], kid)
_, err = client.Logical().DeleteWithContext(testCtx, "pki/eab/"+kid)
require.NoError(t, err, "failed to delete eab")
// List eabs should return zero results
resp, err = client.Logical().ListWithContext(testCtx, "pki/eab")
require.NoError(t, err, "failed to list eab tokens")
require.Nil(t, resp, "list response for eab tokens should have been nil")
// fetch a new EAB
kid, eabKeyBytes := getEABKey(t, client, tc.prefixUrl)
acct := &acme.Account{ acct := &acme.Account{
ExternalAccountBinding: &acme.ExternalAccountBinding{ ExternalAccountBinding: &acme.ExternalAccountBinding{
KID: kid, KID: kid,
@ -355,7 +386,7 @@ func TestAcmeBasicWorkflowWithEab(t *testing.T) {
} }
// Make sure we can list our key // Make sure we can list our key
resp, err := client.Logical().ListWithContext(context.Background(), "pki/acme/eab") resp, err = client.Logical().ListWithContext(testCtx, "pki/eab")
require.NoError(t, err, "failed to list eab tokens") require.NoError(t, err, "failed to list eab tokens")
require.NotNil(t, resp, "list response for eab tokens should not be nil") require.NotNil(t, resp, "list response for eab tokens should not be nil")
require.Contains(t, resp.Data, "keys") require.Contains(t, resp.Data, "keys")
@ -370,6 +401,7 @@ func TestAcmeBasicWorkflowWithEab(t *testing.T) {
keyBits := infoForKid["key_bits"].(json.Number) keyBits := infoForKid["key_bits"].(json.Number)
require.Equal(t, "256", keyBits.String()) require.Equal(t, "256", keyBits.String())
require.Equal(t, "hs", infoForKid["key_type"]) require.Equal(t, "hs", infoForKid["key_type"])
require.Equal(t, tc.prefixUrl, infoForKid["acme_directory"])
// Create new account with EAB // Create new account with EAB
t.Logf("Testing register on %s", baseAcmeURL) t.Logf("Testing register on %s", baseAcmeURL)
@ -377,7 +409,7 @@ func TestAcmeBasicWorkflowWithEab(t *testing.T) {
require.NoError(t, err, "failed registering new account with eab") require.NoError(t, err, "failed registering new account with eab")
// Make sure our EAB is no longer available // Make sure our EAB is no longer available
resp, err = client.Logical().ListWithContext(context.Background(), "pki/acme/eab") resp, err = client.Logical().ListWithContext(context.Background(), "pki/eab")
require.NoError(t, err, "failed to list eab tokens") 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") require.Nil(t, resp, "list response for eab tokens should have been nil due to empty list")
@ -399,6 +431,8 @@ func TestAcmeBasicWorkflowWithEab(t *testing.T) {
// We can lookup/find an existing account without EAB if we have the account key // We can lookup/find an existing account without EAB if we have the account key
_, err = acmeClient.GetReg(testCtx /* unused url */, "") _, err = acmeClient.GetReg(testCtx /* unused url */, "")
require.NoError(t, err, "expected to lookup existing account without eab") 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 // TestAcmeNonce a basic test that will validate we get back a nonce with the proper status codes
@ -540,6 +574,41 @@ func TestAcmeAccountsCrossingDirectoryPath(t *testing.T) {
// swallows the error we are sending back to a no account error // swallows the error we are sending back to a no account error
} }
// TestAcmeEabCrossingDirectoryPath make sure that if an account attempts to use a different ACME
// directory path that an EAB was created within we get an error.
func TestAcmeEabCrossingDirectoryPath(t *testing.T) {
t.Parallel()
cluster, client, _ := setupAcmeBackend(t)
defer cluster.Cleanup()
// 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 := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "failed creating rsa key")
testCtx := context.Background()
acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey)
// fetch a new EAB
kid, eabKeyBytes := getEABKey(t, client, "roles/test-role/acme/")
acct := &acme.Account{
ExternalAccountBinding: &acme.ExternalAccountBinding{
KID: kid,
Key: eabKeyBytes,
},
}
// Create new account
_, err = acmeClient.Register(testCtx, acct, func(tosURL string) bool { return true })
require.ErrorContains(t, err, "failed to verify eab", "should have failed as EAB is for a different directory")
}
// TestAcmeDisabledWithEnvVar verifies if VAULT_DISABLE_PUBLIC_ACME is set that we completely // TestAcmeDisabledWithEnvVar verifies if VAULT_DISABLE_PUBLIC_ACME is set that we completely
// disable the ACME service // disable the ACME service
func TestAcmeDisabledWithEnvVar(t *testing.T) { func TestAcmeDisabledWithEnvVar(t *testing.T) {
@ -1010,8 +1079,8 @@ func getAcmeClientForCluster(t *testing.T, cluster *vault.TestCluster, baseUrl s
} }
} }
func getEABKey(t *testing.T, client *api.Client) (string, []byte) { func getEABKey(t *testing.T, client *api.Client, baseUrl string) (string, []byte) {
resp, err := client.Logical().WriteWithContext(ctx, "pki/acme/new-eab", map[string]interface{}{}) resp, err := client.Logical().WriteWithContext(ctx, path.Join("pki/", baseUrl, "/new-eab"), map[string]interface{}{})
require.NoError(t, err, "failed getting eab key") require.NoError(t, err, "failed getting eab key")
require.NotNil(t, resp, "eab key returned nil response") require.NotNil(t, resp, "eab key returned nil response")
require.NotEmpty(t, resp.Data["id"], "eab key response missing id field") require.NotEmpty(t, resp.Data["id"], "eab key response missing id field")
@ -1022,5 +1091,12 @@ func getEABKey(t *testing.T, client *api.Client) (string, []byte) {
privateKeyBytes, err := base64.RawURLEncoding.DecodeString(base64Key) privateKeyBytes, err := base64.RawURLEncoding.DecodeString(base64Key)
require.NoError(t, err, "failed base 64 decoding eab key response") require.NoError(t, err, "failed base 64 decoding eab key response")
require.Equal(t, "hs", resp.Data["key_type"], "eab key_type field mis-match")
require.Equal(t, json.Number("256"), resp.Data["key_bits"], "eab key_bits field mis-match")
require.Equal(t, baseUrl, resp.Data["acme_directory"], "eab acme_directory field mis-match")
require.NotEmpty(t, resp.Data["created_on"], "empty created_on field")
_, err = time.Parse(time.RFC3339, resp.Data["created_on"].(string))
require.NoError(t, err, "failed parsing eab created_on field")
return kid, privateKeyBytes return kid, privateKeyBytes
} }

View File

@ -1529,7 +1529,7 @@ func (b *backend) doTidyAcme(ctx context.Context, req *logical.Request, logger h
b.tidyStatus.acmeAccountsCount = uint(len(thumbprints)) b.tidyStatus.acmeAccountsCount = uint(len(thumbprints))
b.tidyStatusLock.Unlock() b.tidyStatusLock.Unlock()
baseUrl, _, err := getAcmeBaseUrl(sc, req.Path) baseUrl, _, err := getAcmeBaseUrl(sc, req)
if err != nil { if err != nil {
return err return err
} }

View File

@ -36,6 +36,7 @@ func Test_ACME(t *testing.T) {
tc := map[string]func(t *testing.T, cluster *VaultPkiCluster){ tc := map[string]func(t *testing.T, cluster *VaultPkiCluster){
"certbot": SubtestACMECertbot, "certbot": SubtestACMECertbot,
"certbot eab": SubtestACMECertbotEab,
"acme ip sans": SubtestACMEIPAndDNS, "acme ip sans": SubtestACMEIPAndDNS,
"acme wildcard": SubtestACMEWildcardDNS, "acme wildcard": SubtestACMEWildcardDNS,
"acme prevents ica": SubtestACMEPreventsICADNS, "acme prevents ica": SubtestACMEPreventsICADNS,
@ -153,6 +154,111 @@ func SubtestACMECertbot(t *testing.T, cluster *VaultPkiCluster) {
require.NotEqual(t, 0, retcode, "expected non-zero retcode double revoke command result") require.NotEqual(t, 0, retcode, "expected non-zero retcode double revoke command result")
} }
func SubtestACMECertbotEab(t *testing.T, cluster *VaultPkiCluster) {
mountName := "pki-certbot-eab"
pki, err := cluster.CreateAcmeMount(mountName)
require.NoError(t, err, "failed setting up acme mount")
err = pki.UpdateAcmeConfig(true, map[string]interface{}{
"eab_policy": "new-account-required",
})
require.NoError(t, err)
eabId, base64EabKey, err := pki.GetEabKey("acme/")
directory := "https://" + pki.GetActiveContainerIP() + ":8200/v1/" + mountName + "/acme/directory"
vaultNetwork := pki.GetContainerNetworkName()
logConsumer, logStdout, logStderr := getDockerLog(t)
t.Logf("creating on network: %v", vaultNetwork)
runner, err := hDocker.NewServiceRunner(hDocker.RunOptions{
ImageRepo: "docker.mirror.hashicorp.services/certbot/certbot",
ImageTag: "latest",
ContainerName: "vault_pki_certbot_eab_test",
NetworkName: vaultNetwork,
Entrypoint: []string{"sleep", "45"},
LogConsumer: logConsumer,
LogStdout: logStdout,
LogStderr: logStderr,
})
require.NoError(t, err, "failed creating service runner")
ctx := context.Background()
result, err := runner.Start(ctx, true, false)
require.NoError(t, err, "could not start container")
require.NotNil(t, result, "could not start container")
defer runner.Stop(context.Background(), result.Container.ID)
networks, err := runner.GetNetworkAndAddresses(result.Container.ID)
require.NoError(t, err, "could not read container's IP address")
require.Contains(t, networks, vaultNetwork, "expected to contain vault network")
ipAddr := networks[vaultNetwork]
hostname := "certbot-eab-acme-client.dadgarcorp.com"
err = pki.AddHostname(hostname, ipAddr)
require.NoError(t, err, "failed to update vault host files")
certbotCmd := []string{
"certbot",
"certonly",
"--no-eff-email",
"--email", "certbot.client@dadgarcorp.com",
"--eab-kid", eabId,
"--eab-hmac-key", base64EabKey,
"--agree-tos",
"--no-verify-ssl",
"--standalone",
"--non-interactive",
"--server", directory,
"-d", hostname,
}
logCatCmd := []string{"cat", "/var/log/letsencrypt/letsencrypt.log"}
stdout, stderr, retcode, err := runner.RunCmdWithOutput(ctx, result.Container.ID, certbotCmd)
t.Logf("Certbot Issue Command: %v\nstdout: %v\nstderr: %v\n", certbotCmd, string(stdout), string(stderr))
if err != nil || retcode != 0 {
logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd)
t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr))
}
require.NoError(t, err, "got error running issue command")
require.Equal(t, 0, retcode, "expected zero retcode issue command result")
certbotRevokeCmd := []string{
"certbot",
"revoke",
"--no-eff-email",
"--email", "certbot.client@dadgarcorp.com",
"--agree-tos",
"--no-verify-ssl",
"--non-interactive",
"--no-delete-after-revoke",
"--cert-name", hostname,
}
stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRevokeCmd)
t.Logf("Certbot Revoke Command: %v\nstdout: %v\nstderr: %v\n", certbotRevokeCmd, string(stdout), string(stderr))
if err != nil || retcode != 0 {
logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd)
t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr))
}
require.NoError(t, err, "got error running revoke command")
require.Equal(t, 0, retcode, "expected zero retcode revoke command result")
// Revoking twice should fail.
stdout, stderr, retcode, err = runner.RunCmdWithOutput(ctx, result.Container.ID, certbotRevokeCmd)
t.Logf("Certbot Double Revoke Command: %v\nstdout: %v\nstderr: %v\n", certbotRevokeCmd, string(stdout), string(stderr))
if err != nil || retcode == 0 {
logsStdout, logsStderr, _, _ := runner.RunCmdWithOutput(ctx, result.Container.ID, logCatCmd)
t.Logf("Certbot logs\nstdout: %v\nstderr: %v\n", string(logsStdout), string(logsStderr))
}
require.NoError(t, err, "got error running double revoke command")
require.NotEqual(t, 0, retcode, "expected non-zero retcode double revoke command result")
}
func SubtestACMEIPAndDNS(t *testing.T, cluster *VaultPkiCluster) { func SubtestACMEIPAndDNS(t *testing.T, cluster *VaultPkiCluster) {
pki, err := cluster.CreateAcmeMount("pki-ip-dns-sans") pki, err := cluster.CreateAcmeMount("pki-ip-dns-sans")
require.NoError(t, err, "failed setting up acme mount") require.NoError(t, err, "failed setting up acme mount")

View File

@ -5,7 +5,9 @@ package pkiext_binary
import ( import (
"context" "context"
"encoding/base64"
"fmt" "fmt"
"path"
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
) )
@ -114,6 +116,23 @@ func (vpm *VaultPkiMount) UpdateRole(roleName string, config map[string]interfac
return err return err
} }
func (vpm *VaultPkiMount) GetEabKey(acmeDirectory string) (string, string, error) {
eabPath := path.Join(vpm.mount, acmeDirectory, "/new-eab")
resp, err := vpm.GetActiveNode().Logical().WriteWithContext(context.Background(), eabPath, map[string]interface{}{})
if err != nil {
return "", "", fmt.Errorf("failed fetching eab from %s: %w", eabPath, err)
}
eabId := resp.Data["id"].(string)
base64EabKey := resp.Data["key"].(string)
// just make sure we get something valid back from the server, we still want to pass back the base64 version
// to the caller...
_, err = base64.RawURLEncoding.DecodeString(base64EabKey)
if err != nil {
return "", "", fmt.Errorf("failed decoding key response field: %s: %w", base64EabKey, err)
}
return eabId, base64EabKey, nil
}
func mergeWithDefaults(config map[string]interface{}, defaults map[string]interface{}) map[string]interface{} { func mergeWithDefaults(config map[string]interface{}, defaults map[string]interface{}) map[string]interface{} {
myConfig := config myConfig := config
if myConfig == nil { if myConfig == nil {