420 lines
13 KiB
Go
420 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package pki
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func pathConfigCA(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "config/ca",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
OperationVerb: "configure",
|
|
OperationSuffix: "ca",
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"pem_bundle": {
|
|
Type: framework.TypeString,
|
|
Description: `PEM-format, concatenated unencrypted
|
|
secret key and certificate.`,
|
|
},
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathImportIssuers,
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"mapping": {
|
|
Type: framework.TypeMap,
|
|
Description: "A mapping of issuer_id to key_id for all issuers included in this request",
|
|
Required: true,
|
|
},
|
|
"imported_keys": {
|
|
Type: framework.TypeCommaStringSlice,
|
|
Description: "Net-new keys imported as a part of this request",
|
|
Required: true,
|
|
},
|
|
"imported_issuers": {
|
|
Type: framework.TypeCommaStringSlice,
|
|
Description: "Net-new issuers imported as a part of this request",
|
|
Required: true,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
// Read more about why these flags are set in backend.go.
|
|
ForwardPerformanceStandby: true,
|
|
ForwardPerformanceSecondary: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathConfigCAHelpSyn,
|
|
HelpDescription: pathConfigCAHelpDesc,
|
|
}
|
|
}
|
|
|
|
const pathConfigCAHelpSyn = `
|
|
Set the CA certificate and private key used for generated credentials.
|
|
`
|
|
|
|
const pathConfigCAHelpDesc = `
|
|
This sets the CA information used for credentials generated by this
|
|
by this mount. This must be a PEM-format, concatenated unencrypted
|
|
secret key and certificate.
|
|
|
|
For security reasons, the secret key cannot be retrieved later.
|
|
`
|
|
|
|
func pathConfigIssuers(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "config/issuers",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
defaultRef: {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) to the default issuer.`,
|
|
},
|
|
"default_follows_latest_issuer": {
|
|
Type: framework.TypeBool,
|
|
Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
|
|
Default: false,
|
|
},
|
|
},
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: b.pathCAIssuersRead,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationSuffix: "issuers-configuration",
|
|
},
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"default": {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) to the default issuer.`,
|
|
Required: true,
|
|
},
|
|
"default_follows_latest_issuer": {
|
|
Type: framework.TypeBool,
|
|
Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
|
|
Required: true,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathCAIssuersWrite,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "configure",
|
|
OperationSuffix: "issuers",
|
|
},
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"default": {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) to the default issuer.`,
|
|
},
|
|
"default_follows_latest_issuer": {
|
|
Type: framework.TypeBool,
|
|
Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
// Read more about why these flags are set in backend.go.
|
|
ForwardPerformanceStandby: true,
|
|
ForwardPerformanceSecondary: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathConfigIssuersHelpSyn,
|
|
HelpDescription: pathConfigIssuersHelpDesc,
|
|
}
|
|
}
|
|
|
|
func pathReplaceRoot(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "root/replace",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
OperationVerb: "replace",
|
|
OperationSuffix: "root",
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"default": {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) to the default issuer.`,
|
|
Default: "next",
|
|
},
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathCAIssuersWrite,
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"default": {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) to the default issuer.`,
|
|
Required: true,
|
|
},
|
|
"default_follows_latest_issuer": {
|
|
Type: framework.TypeBool,
|
|
Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
|
|
Required: true,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
// Read more about why these flags are set in backend.go.
|
|
ForwardPerformanceStandby: true,
|
|
ForwardPerformanceSecondary: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathConfigIssuersHelpSyn,
|
|
HelpDescription: pathConfigIssuersHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathCAIssuersRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Cannot read defaults until migration has completed"), nil
|
|
}
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
config, err := sc.getIssuersConfig()
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error loading issuers configuration: " + err.Error()), nil
|
|
}
|
|
|
|
return b.formatCAIssuerConfigRead(config), nil
|
|
}
|
|
|
|
func (b *backend) formatCAIssuerConfigRead(config *issuerConfigEntry) *logical.Response {
|
|
return &logical.Response{
|
|
Data: map[string]interface{}{
|
|
defaultRef: config.DefaultIssuerId,
|
|
"default_follows_latest_issuer": config.DefaultFollowsLatestIssuer,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathCAIssuersWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
// Since we're planning on updating issuers here, grab the lock so we've
|
|
// got a consistent view.
|
|
b.issuersLock.Lock()
|
|
defer b.issuersLock.Unlock()
|
|
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Cannot update defaults until migration has completed"), nil
|
|
}
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
|
|
// Validate the new default reference.
|
|
newDefault := data.Get(defaultRef).(string)
|
|
if len(newDefault) == 0 || newDefault == defaultRef {
|
|
return logical.ErrorResponse("Invalid issuer specification; must be non-empty and can't be 'default'."), nil
|
|
}
|
|
parsedIssuer, err := sc.resolveIssuerReference(newDefault)
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error resolving issuer reference: " + err.Error()), nil
|
|
}
|
|
entry, err := sc.fetchIssuerById(parsedIssuer)
|
|
if err != nil {
|
|
return logical.ErrorResponse("Unable to fetch issuer: " + err.Error()), nil
|
|
}
|
|
|
|
// Get the other new parameters. This doesn't exist on the /root/replace
|
|
// variant of this call.
|
|
var followIssuer bool
|
|
followIssuersRaw, followOk := data.GetOk("default_follows_latest_issuer")
|
|
if followOk {
|
|
followIssuer = followIssuersRaw.(bool)
|
|
}
|
|
|
|
// Update the config
|
|
config, err := sc.getIssuersConfig()
|
|
if err != nil {
|
|
return logical.ErrorResponse("Unable to fetch existing issuers configuration: " + err.Error()), nil
|
|
}
|
|
config.DefaultIssuerId = parsedIssuer
|
|
if followOk {
|
|
config.DefaultFollowsLatestIssuer = followIssuer
|
|
}
|
|
|
|
// Add our warning if necessary.
|
|
response := b.formatCAIssuerConfigRead(config)
|
|
if len(entry.KeyID) == 0 {
|
|
msg := "This selected default issuer has no key associated with it. Some operations like issuing certificates and signing CRLs will be unavailable with the requested default issuer until a key is imported or the default issuer is changed."
|
|
response.AddWarning(msg)
|
|
b.Logger().Error(msg)
|
|
}
|
|
|
|
if err := sc.setIssuersConfig(config); err != nil {
|
|
return logical.ErrorResponse("Error updating issuer configuration: " + err.Error()), nil
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
const pathConfigIssuersHelpSyn = `Read and set the default issuer certificate for signing.`
|
|
|
|
const pathConfigIssuersHelpDesc = `
|
|
This path allows configuration of issuer parameters.
|
|
|
|
Presently, the "default" parameter controls which issuer is the default,
|
|
accessible by the existing signing paths (/root/sign-intermediate,
|
|
/root/sign-self-issued, /sign-verbatim, /sign/:role, and /issue/:role).
|
|
|
|
The /root/replace path is aliased to this path, with default taking the
|
|
value of the issuer with the name "next", if it exists.
|
|
`
|
|
|
|
func pathConfigKeys(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "config/keys",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
defaultRef: {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) of the default key.`,
|
|
},
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathKeyDefaultWrite,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "configure",
|
|
OperationSuffix: "keys",
|
|
},
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"default": {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) to the default issuer.`,
|
|
Required: true,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
ForwardPerformanceStandby: true,
|
|
ForwardPerformanceSecondary: true,
|
|
},
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: b.pathKeyDefaultRead,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationSuffix: "keys-configuration",
|
|
},
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"default": {
|
|
Type: framework.TypeString,
|
|
Description: `Reference (name or identifier) to the default issuer.`,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
ForwardPerformanceStandby: false,
|
|
ForwardPerformanceSecondary: false,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathConfigKeysHelpSyn,
|
|
HelpDescription: pathConfigKeysHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathKeyDefaultRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Cannot read key defaults until migration has completed"), nil
|
|
}
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
config, err := sc.getKeysConfig()
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error loading keys configuration: " + err.Error()), nil
|
|
}
|
|
|
|
return &logical.Response{
|
|
Data: map[string]interface{}{
|
|
defaultRef: config.DefaultKeyId,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (b *backend) pathKeyDefaultWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
// Since we're planning on updating keys here, grab the lock so we've
|
|
// got a consistent view.
|
|
b.issuersLock.Lock()
|
|
defer b.issuersLock.Unlock()
|
|
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Cannot update key defaults until migration has completed"), nil
|
|
}
|
|
|
|
newDefault := data.Get(defaultRef).(string)
|
|
if len(newDefault) == 0 || newDefault == defaultRef {
|
|
return logical.ErrorResponse("Invalid key specification; must be non-empty and can't be 'default'."), nil
|
|
}
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
parsedKey, err := sc.resolveKeyReference(newDefault)
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error resolving issuer reference: " + err.Error()), nil
|
|
}
|
|
|
|
err = sc.updateDefaultKeyId(parsedKey)
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error updating issuer configuration: " + err.Error()), nil
|
|
}
|
|
|
|
return &logical.Response{
|
|
Data: map[string]interface{}{
|
|
defaultRef: parsedKey,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
const pathConfigKeysHelpSyn = `Read and set the default key used for signing`
|
|
|
|
const pathConfigKeysHelpDesc = `
|
|
This path allows configuration of key parameters.
|
|
|
|
The "default" parameter controls which key is the default used by signing paths.
|
|
`
|