295 lines
8.5 KiB
Go
295 lines
8.5 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package pki
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
var decodedTokenPrefix = mustBase64Decode("vault-eab-0-")
|
|
|
|
func mustBase64Decode(s string) []byte {
|
|
bytes, err := base64.RawURLEncoding.DecodeString(s)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Token prefix value: %s failed decoding: %v", s, err))
|
|
}
|
|
|
|
// Should be dividable by 3 otherwise our prefix will not be properly honored.
|
|
if len(bytes)%3 != 0 {
|
|
panic(fmt.Sprintf("Token prefix value: %s is not dividable by 3, will not prefix properly", s))
|
|
}
|
|
return bytes
|
|
}
|
|
|
|
/*
|
|
* 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 pathAcmeEabList(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "eab/?$",
|
|
Fields: map[string]*framework.FieldSchema{},
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ListOperation: &framework.PathOperation{
|
|
Callback: b.pathAcmeListEab,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
OperationVerb: "list-eab-keys",
|
|
Description: "List all eab key identifiers yet to be used.",
|
|
},
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"keys": {
|
|
Type: framework.TypeStringSlice,
|
|
Description: `A list of unused eab keys`,
|
|
Required: true,
|
|
},
|
|
"key_info": {
|
|
Type: framework.TypeMap,
|
|
Description: `EAB details keyed by the eab key id`,
|
|
Required: false,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: "list external account bindings to be used for ACME",
|
|
HelpDescription: `list identifiers that have been generated but yet to be used.`,
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
opSuffix := getAcmeOperationSuffix(pattern)
|
|
|
|
return &framework.Path{
|
|
Pattern: pattern,
|
|
Fields: fields,
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathAcmeCreateEab,
|
|
ForwardPerformanceSecondary: false,
|
|
ForwardPerformanceStandby: true,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
OperationVerb: "generate-eab-key",
|
|
OperationSuffix: opSuffix,
|
|
Description: "Generate an ACME EAB token for a directory",
|
|
},
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"id": {
|
|
Type: framework.TypeString,
|
|
Description: `The EAB key identifier`,
|
|
Required: true,
|
|
},
|
|
"key_type": {
|
|
Type: framework.TypeString,
|
|
Description: `The EAB key type`,
|
|
Required: true,
|
|
},
|
|
"key": {
|
|
Type: framework.TypeString,
|
|
Description: `The EAB hmac key`,
|
|
Required: true,
|
|
},
|
|
"acme_directory": {
|
|
Type: framework.TypeString,
|
|
Description: `The ACME directory to which the key belongs`,
|
|
Required: true,
|
|
},
|
|
"created_on": {
|
|
Type: framework.TypeTime,
|
|
Description: `An RFC3339 formatted date time when the EAB token was created`,
|
|
Required: true,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
|
|
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: "eab/" + uuidNameRegex("key_id"),
|
|
|
|
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{
|
|
Callback: b.pathAcmeDeleteEab,
|
|
ForwardPerformanceSecondary: false,
|
|
ForwardPerformanceStandby: true,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
OperationVerb: "delete-eab-key",
|
|
Description: "Delete an unused EAB token",
|
|
},
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: "Delete an external account binding id prior to its use within an ACME account",
|
|
HelpDescription: `Allows an operator to delete an external account binding,
|
|
before its bound to a new ACME account. If the identifier provided does not exist or
|
|
was already consumed by an ACME account a successful response is returned along with
|
|
a warning that it did not exist.`,
|
|
}
|
|
}
|
|
|
|
type eabType struct {
|
|
KeyID string `json:"-"`
|
|
KeyType string `json:"key-type"`
|
|
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) {
|
|
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,
|
|
"acme_directory": path.Join(eab.AcmeDirectory, "directory"),
|
|
"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, 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",
|
|
PrivateBytes: append(decodedTokenPrefix, bytes...), // we do this to avoid generating tokens that start with -
|
|
AcmeDirectory: acmeDirectory,
|
|
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(eab.PrivateBytes)
|
|
|
|
return &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"id": eab.KeyID,
|
|
"key_type": eab.KeyType,
|
|
"key": encodedKey,
|
|
"acme_directory": path.Join(eab.AcmeDirectory, "directory"),
|
|
"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
|
|
}
|
|
|
|
// getAcmeOperationSuffix used mainly to compute the OpenAPI spec suffix value to distinguish
|
|
// different versions of ACME Vault APIs based on directory paths
|
|
func getAcmeOperationSuffix(pattern string) string {
|
|
hasRole := strings.Contains(pattern, framework.GenericNameRegex("role"))
|
|
hasIssuer := strings.Contains(pattern, framework.GenericNameRegex(issuerRefParam))
|
|
|
|
switch {
|
|
case hasRole && hasIssuer:
|
|
return "for-issuer-and-role"
|
|
case hasRole:
|
|
return "for-role"
|
|
case hasIssuer:
|
|
return "for-issuer"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|