Skip to content

Commit

Permalink
backport of commit 000d754 (#20870)
Browse files Browse the repository at this point in the history
Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
  • Loading branch information
1 parent b4df2be commit 70e46e8
Show file tree
Hide file tree
Showing 11 changed files with 362 additions and 197 deletions.
5 changes: 5 additions & 0 deletions builtin/logical/pki/acme_jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

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
}
36 changes: 24 additions & 12 deletions builtin/logical/pki/acme_wrappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

acmeBaseUrl, clusterBase, err := getAcmeBaseUrl(sc, r.Path)
acmeBaseUrl, clusterBase, err := getAcmeBaseUrl(sc, r)
if err != nil {
return nil, err
}
Expand All @@ -87,7 +87,10 @@ func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
return nil, err
}

acmeDirectory := getAcmeDirectory(data)
acmeDirectory, err := getAcmeDirectory(r)
if err != nil {
return nil, err
}

acmeCtx := &acmeContext{
baseUrl: acmeBaseUrl,
Expand Down Expand Up @@ -237,19 +240,18 @@ func buildAcmeFrameworkPaths(b *backend, patternFunc func(b *backend, pattern st
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)
if err != nil {
return nil, nil, err
}

directoryPrefix := ""
lastIndex := strings.LastIndex(path, "/acme/")
if lastIndex != -1 {
directoryPrefix = path[0:lastIndex]
directoryPrefix, err := getAcmeDirectory(r)
if err != nil {
return nil, nil, err
}

return baseUrl.JoinPath(directoryPrefix, "/acme/"), baseUrl, nil
return baseUrl.JoinPath(directoryPrefix), baseUrl, nil
}

func getBasePathFromClusterConfig(sc *storageContext) (*url.URL, error) {
Expand Down Expand Up @@ -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)
}

func getAcmeDirectory(data *framework.FieldData) string {
requestedIssuer := getRequestedAcmeIssuerFromPath(data)
requestedRole := getRequestedAcmeRoleFromPath(data)
// getAcmeDirectory return the base acme directory path, without a leading '/' and including
// the trailing /acme/ folder which is the root of all our various directories
func getAcmeDirectory(r *logical.Request) (string, error) {
acmePath := r.Path
if !strings.HasPrefix(acmePath, "/") {
acmePath = "/" + acmePath
}

lastIndex := strings.LastIndex(acmePath, "/acme/")
if lastIndex == -1 {
return "", fmt.Errorf("%w: unable to determine acme base folder path: %s", ErrServerInternal, acmePath)
}

return fmt.Sprintf("issuer-%s::role-%s", requestedIssuer, requestedRole)
// 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) {
Expand Down
14 changes: 10 additions & 4 deletions builtin/logical/pki/acme_wrappers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package pki
import (
"context"
"fmt"
"strings"
"testing"

"github.com/hashicorp/vault/sdk/framework"
Expand Down Expand Up @@ -92,15 +93,20 @@ func TestACMEIssuerRoleLoading(t *testing.T) {
return nil, nil
})

var acmePath string
fieldRaw := map[string]interface{}{}
if tt.roleName != "" {
fieldRaw["role"] = tt.roleName
}
if tt.issuerName != "" {
fieldRaw[issuerRefParam] = tt.issuerName
acmePath = "issuer/" + tt.issuerName + "/"
}
if tt.roleName != "" {
fieldRaw["role"] = tt.roleName
acmePath = acmePath + "roles/" + tt.roleName + "/"
}

acmePath = strings.TrimLeft(acmePath+"/acme/directory", "/")

resp, err := f(context.Background(), &logical.Request{Storage: s}, &framework.FieldData{
resp, err := f(context.Background(), &logical.Request{Path: acmePath, Storage: s}, &framework.FieldData{
Raw: fieldRaw,
Schema: getCsrSignVerbatimSchemaFields(),
})
Expand Down
3 changes: 2 additions & 1 deletion builtin/logical/pki/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,6 @@ func Backend(conf *logical.BackendConfig) *backend {

// ACME
pathAcmeConfig(&b),
pathAcmeEabCreate(&b),
pathAcmeEabList(&b),
pathAcmeEabDelete(&b),
},
Expand Down Expand Up @@ -248,6 +247,7 @@ func Backend(conf *logical.BackendConfig) *backend {
acmePaths = append(acmePaths, pathAcmeChallenge(&b)...)
acmePaths = append(acmePaths, pathAcmeAuthorization(&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 {
b.Backend.Paths = append(b.Backend.Paths, acmePath)
Expand All @@ -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/+/finalize")
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 {
Expand Down
10 changes: 6 additions & 4 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6862,9 +6862,8 @@ func TestProperAuthing(t *testing.T) {
"unified-crl/delta/pem": shouldBeUnauthedReadList,
"unified-ocsp": shouldBeUnauthedWriteOnly,
"unified-ocsp/dGVzdAo=": shouldBeUnauthedReadList,
"acme/new-eab": shouldBeAuthed,
"acme/eab": shouldBeAuthed,
"acme/eab/" + eabKid: shouldBeAuthed,
"eab": shouldBeAuthed,
"eab/" + eabKid: shouldBeAuthed,
}

// Add ACME based paths to the test suite
Expand All @@ -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/finalize"] = 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 {
Expand Down Expand Up @@ -6938,7 +6940,7 @@ 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}") {
if strings.Contains(raw_path, "eab") && strings.Contains(raw_path, "{key_id}") {
raw_path = strings.ReplaceAll(raw_path, "{key_id}", eabKid)
}

Expand Down
77 changes: 45 additions & 32 deletions builtin/logical/pki/path_acme_eab.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
*/
func pathAcmeEabList(b *backend) *framework.Path {
return &framework.Path{
Pattern: "acme/eab/?$",
Pattern: "eab/?$",

DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
Expand All @@ -45,16 +45,17 @@ func pathAcmeEabList(b *backend) *framework.Path {
}
}

func pathAcmeEabCreate(b *backend) *framework.Path {
return &framework.Path{
Pattern: "acme/new-eab",

DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
},
func pathAcmeNewEab(b *backend) []*framework.Path {
return buildAcmeFrameworkPaths(b, patternAcmeNewEab, "/new-eab")
}

Fields: map[string]*framework.FieldSchema{},
func patternAcmeNewEab(b *backend, pattern string) *framework.Path {
fields := map[string]*framework.FieldSchema{}
addFieldsForACMEPath(fields, pattern)

return &framework.Path{
Pattern: pattern,
Fields: fields,
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathAcmeCreateEab,
Expand All @@ -67,15 +68,18 @@ func pathAcmeEabCreate(b *backend) *framework.Path {
},
},

HelpSynopsis: "Generate or list external account bindings to be used for ACME",
HelpDescription: `Generate single use id/key pairs to be used for ACME EAB or list
identifiers that have been generated but yet to be used.`,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
},

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 {
return &framework.Path{
Pattern: "acme/eab/" + uuidNameRegex("key_id"),
Pattern: "eab/" + uuidNameRegex("key_id"),

DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
Expand Down Expand Up @@ -109,11 +113,12 @@ a warning that it did not exist.`,
}

type eabType struct {
KeyID string `json:"-"`
KeyType string `json:"key-type"`
KeyBits int `json:"key-bits"`
PrivateBytes []byte `json:"private-bytes"`
CreatedOn time.Time `json:"created-on"`
KeyID string `json:"-"`
KeyType string `json:"key-type"`
KeyBits int `json:"key-bits"`
PrivateBytes []byte `json:"private-bytes"`
AcmeDirectory string `json:"acme-directory"`
CreatedOn time.Time `json:"created-on"`
}

func (b *backend) pathAcmeListEab(ctx context.Context, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
Expand All @@ -137,9 +142,10 @@ func (b *backend) pathAcmeListEab(ctx context.Context, r *logical.Request, _ *fr

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),
"key_type": eab.KeyType,
"key_bits": eab.KeyBits,
"acme_directory": eab.AcmeDirectory,
"created_on": eab.CreatedOn.Format(time.RFC3339),
}
}

Expand All @@ -150,20 +156,26 @@ func (b *backend) pathAcmeListEab(ctx context.Context, r *logical.Request, _ *fr
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()
size := 32
bytes, err := uuid.GenerateRandomBytesWithReader(size, rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed generating eab key: %w", err)
}

acmeDirectory, err := getAcmeDirectory(r)
if err != nil {
return nil, err
}

eab := &eabType{
KeyID: kid,
KeyType: "hs",
KeyBits: size * 8,
PrivateBytes: bytes,
CreatedOn: time.Now(),
KeyID: kid,
KeyType: "hs",
KeyBits: size * 8,
PrivateBytes: bytes,
AcmeDirectory: acmeDirectory,
CreatedOn: time.Now(),
}

sc := b.makeStorageContext(ctx, r.Storage)
Expand All @@ -176,11 +188,12 @@ func (b *backend) pathAcmeCreateEab(ctx context.Context, r *logical.Request, _ *

return &logical.Response{
Data: map[string]interface{}{
"id": eab.KeyID,
"key_type": eab.KeyType,
"key_bits": eab.KeyBits,
"key": encodedKey,
"created_on": eab.CreatedOn.Format(time.RFC3339),
"id": eab.KeyID,
"key_type": eab.KeyType,
"key_bits": eab.KeyBits,
"key": encodedKey,
"acme_directory": eab.AcmeDirectory,
"created_on": eab.CreatedOn.Format(time.RFC3339),
},
}, nil
}
Expand Down
75 changes: 0 additions & 75 deletions builtin/logical/pki/path_acme_eab_test.go

This file was deleted.

Loading

0 comments on commit 70e46e8

Please sign in to comment.