b4c3aca7a1
* Squash pki/acme package down to pki folder Without refactoring most of PKI to export the storage layer, which we were initially hesitant about, it would be nearly impossible to have the ACME layer handle its own storage while being in the acme/ subpackage under the pki package. Thus, merge the two packages together again. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Properly format errors for missing parameters When missing required ACME request parameters, don't return Vault-level errors, but drop into the PKI package to return properly-formatted ACME error messages. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Error type clarifications Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix GetOk with type conversion calls Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
146 lines
4.2 KiB
Go
146 lines
4.2 KiB
Go
package pki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
const (
|
|
pathAcmeDirectoryHelpSync = `Read the proper URLs for various ACME operations`
|
|
pathAcmeDirectoryHelpDesc = `Provide an ACME directory response that contains URLS for various ACME operations.`
|
|
)
|
|
|
|
func pathAcmeRootDirectory(b *backend) *framework.Path {
|
|
return patternAcmeDirectory(b, "acme/directory")
|
|
}
|
|
|
|
func pathAcmeRoleDirectory(b *backend) *framework.Path {
|
|
return patternAcmeDirectory(b, "roles/"+framework.GenericNameRegex("role")+"/acme/directory")
|
|
}
|
|
|
|
func pathAcmeIssuerDirectory(b *backend) *framework.Path {
|
|
return patternAcmeDirectory(b, "issuer/"+framework.GenericNameRegex(issuerRefParam)+"/acme/directory")
|
|
}
|
|
|
|
func pathAcmeIssuerAndRoleDirectory(b *backend) *framework.Path {
|
|
return patternAcmeDirectory(b,
|
|
"issuer/"+framework.GenericNameRegex(issuerRefParam)+
|
|
"/roles/"+framework.GenericNameRegex("role")+"/acme/directory")
|
|
}
|
|
|
|
func patternAcmeDirectory(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.ReadOperation: &framework.PathOperation{
|
|
Callback: b.acmeWrapper(b.acmeDirectoryHandler),
|
|
ForwardPerformanceSecondary: false,
|
|
ForwardPerformanceStandby: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathAcmeDirectoryHelpSync,
|
|
HelpDescription: pathAcmeDirectoryHelpDesc,
|
|
}
|
|
}
|
|
|
|
type acmeOperation func(acmeCtx acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error)
|
|
|
|
type acmeContext struct {
|
|
baseUrl *url.URL
|
|
sc *storageContext
|
|
}
|
|
|
|
func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
|
|
return acmeErrorWrapper(func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
sc := b.makeStorageContext(ctx, r.Storage)
|
|
|
|
if false {
|
|
// TODO sclark: Check if ACME is enable here
|
|
return nil, fmt.Errorf("ACME is disabled in configuration: %w", ErrServerInternal)
|
|
}
|
|
|
|
baseUrl, err := getAcmeBaseUrl(sc, r.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
acmeCtx := acmeContext{
|
|
baseUrl: baseUrl,
|
|
sc: sc,
|
|
}
|
|
|
|
return op(acmeCtx, r, data)
|
|
})
|
|
}
|
|
|
|
func getAcmeBaseUrl(sc *storageContext, path string) (*url.URL, error) {
|
|
cfg, err := sc.getClusterConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed loading cluster config: %w", err)
|
|
}
|
|
|
|
if cfg.Path == "" {
|
|
return nil, fmt.Errorf("ACME feature requires local cluster path configuration to be set: %w", ErrServerInternal)
|
|
}
|
|
|
|
baseUrl, err := url.Parse(cfg.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ACME feature a proper URL configured in local cluster path: %w", ErrServerInternal)
|
|
}
|
|
|
|
directoryPrefix := ""
|
|
lastIndex := strings.LastIndex(path, "/acme/")
|
|
if lastIndex != -1 {
|
|
directoryPrefix = path[0:lastIndex]
|
|
}
|
|
|
|
return baseUrl.JoinPath(directoryPrefix), nil
|
|
}
|
|
|
|
func acmeErrorWrapper(op framework.OperationFunc) framework.OperationFunc {
|
|
return func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
resp, err := op(ctx, r, data)
|
|
if err != nil {
|
|
return TranslateError(err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
func (b *backend) acmeDirectoryHandler(acmeCtx acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
|
rawBody, err := json.Marshal(map[string]interface{}{
|
|
"newNonce": acmeCtx.baseUrl.JoinPath("/acme/new-nonce").String(),
|
|
"newAccount": acmeCtx.baseUrl.JoinPath("/acme/new-account").String(),
|
|
"newOrder": acmeCtx.baseUrl.JoinPath("/acme/new-order").String(),
|
|
"revokeCert": acmeCtx.baseUrl.JoinPath("/acme/revoke-cert").String(),
|
|
"keyChange": acmeCtx.baseUrl.JoinPath("/acme/key-change").String(),
|
|
"meta": map[string]interface{}{
|
|
"externalAccountRequired": false,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed encoding response: %w", err)
|
|
}
|
|
|
|
return &logical.Response{
|
|
Data: map[string]interface{}{
|
|
logical.HTTPContentType: "application/json",
|
|
logical.HTTPStatusCode: http.StatusOK,
|
|
logical.HTTPRawBody: rawBody,
|
|
},
|
|
}, nil
|
|
}
|