mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-06 06:37:02 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
368 lines
13 KiB
Go
368 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
paths "path"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
type PKIIssueCACommand struct {
|
|
*BaseCommand
|
|
|
|
flagConfig string
|
|
flagReturnIndicator string
|
|
flagDefaultDisabled bool
|
|
flagList bool
|
|
|
|
flagKeyStorageSource string
|
|
flagNewIssuerName string
|
|
}
|
|
|
|
func (c *PKIIssueCACommand) Synopsis() string {
|
|
return "Given a parent certificate, and a list of generation parameters, creates an issuer on a specified mount"
|
|
}
|
|
|
|
func (c *PKIIssueCACommand) Help() string {
|
|
helpText := `
|
|
Usage: vault pki issue PARENT CHILD_MOUNT options
|
|
|
|
PARENT is the fully qualified path of the Certificate Authority in vault which will issue the new intermediate certificate.
|
|
|
|
CHILD_MOUNT is the path of the mount in vault where the new issuer is saved.
|
|
|
|
options are the superset of the options passed to generate/intermediate and sign-intermediate commands. At least one option must be set.
|
|
|
|
This command creates a intermediate certificate authority certificate signed by the parent in the CHILD_MOUNT.
|
|
|
|
` + c.Flags().Help()
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *PKIIssueCACommand) Flags() *FlagSets {
|
|
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
|
|
f := set.NewFlagSet("Command Options")
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "type",
|
|
Target: &c.flagKeyStorageSource,
|
|
Default: "internal",
|
|
EnvVar: "",
|
|
Usage: `Options are "existing" - to use an existing key inside vault, "internal" - to generate a new key inside vault, or "kms" - to link to an external key. Exported keys are not available through this API.`,
|
|
Completion: complete.PredictSet("internal", "existing", "kms"),
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "issuer_name",
|
|
Target: &c.flagNewIssuerName,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Usage: `If present, the newly created issuer will be given this name.`,
|
|
})
|
|
|
|
return set
|
|
}
|
|
|
|
func (c *PKIIssueCACommand) Run(args []string) int {
|
|
// Parse Args
|
|
f := c.Flags()
|
|
if err := f.Parse(args); err != nil {
|
|
c.UI.Error(err.Error())
|
|
return 1
|
|
}
|
|
args = f.Args()
|
|
|
|
if len(args) < 3 {
|
|
c.UI.Error("Not enough arguments expected parent issuer and child-mount location and some key_value argument")
|
|
return 1
|
|
}
|
|
|
|
stdin := (io.Reader)(os.Stdin)
|
|
data, err := parseArgsData(stdin, args[2:])
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err))
|
|
return 1
|
|
}
|
|
|
|
parentMountIssuer := sanitizePath(args[0]) // /pki/issuer/default
|
|
|
|
intermediateMount := sanitizePath(args[1])
|
|
|
|
return pkiIssue(c.BaseCommand, parentMountIssuer, intermediateMount, c.flagNewIssuerName, c.flagKeyStorageSource, data)
|
|
}
|
|
|
|
func pkiIssue(c *BaseCommand, parentMountIssuer string, intermediateMount string, flagNewIssuerName string, flagKeyStorageSource string, data map[string]interface{}) int {
|
|
// Check We Have a Client
|
|
client, err := c.Client()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Failed to obtain client: %v", err))
|
|
return 1
|
|
}
|
|
|
|
// Sanity Check the Parent Issuer
|
|
if !strings.Contains(parentMountIssuer, "/issuer/") {
|
|
c.UI.Error(fmt.Sprintf("Parent Issuer %v is Not a PKI Issuer Path of the format /mount/issuer/issuer-ref", parentMountIssuer))
|
|
return 1
|
|
}
|
|
_, err = readIssuer(client, parentMountIssuer)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Unable to access parent issuer %v: %v", parentMountIssuer, err))
|
|
return 1
|
|
}
|
|
|
|
// Set-up Failure State (Immediately Before First Write Call)
|
|
failureState := inCaseOfFailure{
|
|
intermediateMount: intermediateMount,
|
|
parentMount: strings.Split(parentMountIssuer, "/issuer/")[0],
|
|
parentIssuer: parentMountIssuer,
|
|
newName: flagNewIssuerName,
|
|
}
|
|
|
|
// Generate Certificate Signing Request
|
|
csrResp, err := client.Logical().Write(intermediateMount+"/intermediate/generate/"+flagKeyStorageSource, data)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "no handler for route") { // Mount Given Does Not Exist
|
|
c.UI.Error(fmt.Sprintf("Given Intermediate Mount %v Does Not Exist: %v", intermediateMount, err))
|
|
} else if strings.Contains(err.Error(), "unsupported path") { // Expected if Not a PKI Mount
|
|
c.UI.Error(fmt.Sprintf("Given Intermeidate Mount %v Is Not a PKI Mount: %v", intermediateMount, err))
|
|
} else {
|
|
c.UI.Error(fmt.Sprintf("Failled to Generate Intermediate CSR on %v: %v", intermediateMount, err))
|
|
}
|
|
return 1
|
|
}
|
|
// Parse CSR Response, Also Verifies that this is a PKI Mount
|
|
// (e.g. calling the above call on cubbyhole/ won't return an error response)
|
|
csrPemRaw, present := csrResp.Data["csr"]
|
|
if !present {
|
|
c.UI.Error(fmt.Sprintf("Failed to Generate Intermediate CSR on %v, got response: %v", intermediateMount, csrResp))
|
|
return 1
|
|
}
|
|
keyIdRaw, present := csrResp.Data["key_id"]
|
|
if !present && flagKeyStorageSource == "internal" {
|
|
c.UI.Error(fmt.Sprintf("Failed to Generate Key on %v, got response: %v", intermediateMount, csrResp))
|
|
return 1
|
|
}
|
|
|
|
// If that all Parses, then we've successfully generated a CSR! Save It (and the Key-ID)
|
|
failureState.csrGenerated = true
|
|
if flagKeyStorageSource == "internal" {
|
|
failureState.createdKeyId = keyIdRaw.(string)
|
|
}
|
|
csr := csrPemRaw.(string)
|
|
failureState.csr = csr
|
|
data["csr"] = csr
|
|
|
|
// Next, Sign the CSR
|
|
rootResp, err := client.Logical().Write(parentMountIssuer+"/sign-intermediate", data)
|
|
if err != nil {
|
|
c.UI.Error(failureState.generateFailureMessage())
|
|
c.UI.Error(fmt.Sprintf("Error Signing Intermiate On %v", err))
|
|
return 1
|
|
}
|
|
// Success! Save Our Progress (and Parse the Response)
|
|
failureState.csrSigned = true
|
|
serialNumber := rootResp.Data["serial_number"].(string)
|
|
failureState.certSerialNumber = serialNumber
|
|
|
|
caChain := rootResp.Data["ca_chain"].([]interface{})
|
|
caChainPemBundle := ""
|
|
for _, cert := range caChain {
|
|
caChainPemBundle += cert.(string) + "\n"
|
|
}
|
|
failureState.caChain = caChainPemBundle
|
|
|
|
// Next Import Certificate
|
|
certificate := rootResp.Data["certificate"].(string)
|
|
issuerId, err := importIssuerWithName(client, intermediateMount, certificate, flagNewIssuerName)
|
|
failureState.certIssuerId = issuerId
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "error naming issuer") {
|
|
failureState.certImported = true
|
|
c.UI.Error(failureState.generateFailureMessage())
|
|
c.UI.Error(fmt.Sprintf("Error Naming Newly Imported Issuer: %v", err))
|
|
return 1
|
|
} else {
|
|
c.UI.Error(failureState.generateFailureMessage())
|
|
c.UI.Error(fmt.Sprintf("Error Importing Into %v Newly Created Issuer %v: %v", intermediateMount, certificate, err))
|
|
return 1
|
|
}
|
|
}
|
|
failureState.certImported = true
|
|
|
|
// Then Import Issuing Certificate
|
|
issuingCa := rootResp.Data["issuing_ca"].(string)
|
|
_, parentIssuerName := paths.Split(parentMountIssuer)
|
|
_, err = importIssuerWithName(client, intermediateMount, issuingCa, parentIssuerName)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "error naming issuer") {
|
|
c.UI.Warn(fmt.Sprintf("Unable to Set Name on Parent Cert from %v Imported Into %v with serial %v, err: %v", parentIssuerName, intermediateMount, serialNumber, err))
|
|
} else {
|
|
c.UI.Error(failureState.generateFailureMessage())
|
|
c.UI.Error(fmt.Sprintf("Error Importing Into %v Newly Created Issuer %v: %v", intermediateMount, certificate, err))
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// Finally Import CA_Chain (just in case there's more information)
|
|
if len(caChain) > 2 { // We've already imported parent cert and newly issued cert above
|
|
importData := map[string]interface{}{
|
|
"pem_bundle": caChainPemBundle,
|
|
}
|
|
_, err := client.Logical().Write(intermediateMount+"/issuers/import/cert", importData)
|
|
if err != nil {
|
|
c.UI.Error(failureState.generateFailureMessage())
|
|
c.UI.Error(fmt.Sprintf("Error Importing CaChain into %v: %v", intermediateMount, err))
|
|
return 1
|
|
}
|
|
}
|
|
failureState.caChainImported = true
|
|
|
|
// Finally we read our newly issued certificate in order to tell our caller about it
|
|
readAndOutputNewCertificate(client, intermediateMount, issuerId, c)
|
|
|
|
return 0
|
|
}
|
|
|
|
func readAndOutputNewCertificate(client *api.Client, intermediateMount string, issuerId string, c *BaseCommand) {
|
|
resp, err := client.Logical().Read(sanitizePath(intermediateMount + "/issuer/" + issuerId))
|
|
if err != nil || resp == nil {
|
|
c.UI.Error(fmt.Sprintf("Error Reading Fully Imported Certificate from %v : %v",
|
|
intermediateMount+"/issuer/"+issuerId, err))
|
|
return
|
|
}
|
|
|
|
OutputSecret(c.UI, resp)
|
|
}
|
|
|
|
func importIssuerWithName(client *api.Client, mount string, bundle string, name string) (issuerUUID string, err error) {
|
|
importData := map[string]interface{}{
|
|
"pem_bundle": bundle,
|
|
}
|
|
writeResp, err := client.Logical().Write(mount+"/issuers/import/cert", importData)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
mapping := writeResp.Data["mapping"].(map[string]interface{})
|
|
if len(mapping) > 1 {
|
|
return "", fmt.Errorf("multiple issuers returned, while expected one, got %v", writeResp)
|
|
}
|
|
for issuerId := range mapping {
|
|
issuerUUID = issuerId
|
|
}
|
|
if name != "" && name != "default" {
|
|
nameReq := map[string]interface{}{
|
|
"issuer_name": name,
|
|
}
|
|
ctx := context.Background()
|
|
_, err = client.Logical().JSONMergePatch(ctx, mount+"/issuer/"+issuerUUID, nameReq)
|
|
if err != nil {
|
|
return issuerUUID, fmt.Errorf("error naming issuer %v to %v: %v", issuerUUID, name, err)
|
|
}
|
|
}
|
|
return issuerUUID, nil
|
|
}
|
|
|
|
type inCaseOfFailure struct {
|
|
csrGenerated bool
|
|
csrSigned bool
|
|
certImported bool
|
|
certNamed bool
|
|
caChainImported bool
|
|
|
|
intermediateMount string
|
|
createdKeyId string
|
|
csr string
|
|
caChain string
|
|
parentMount string
|
|
parentIssuer string
|
|
certSerialNumber string
|
|
certIssuerId string
|
|
newName string
|
|
}
|
|
|
|
func (state inCaseOfFailure) generateFailureMessage() string {
|
|
message := "A failure has occurred"
|
|
|
|
if state.csrGenerated {
|
|
message += fmt.Sprintf(" after \n a Certificate Signing Request was successfully generated on mount %v", state.intermediateMount)
|
|
}
|
|
if state.csrSigned {
|
|
message += fmt.Sprintf(" and after \n that Certificate Signing Request was successfully signed by mount %v", state.parentMount)
|
|
}
|
|
if state.certImported {
|
|
message += fmt.Sprintf(" and after \n the signed certificate was reimported into mount %v , with issuerID %v", state.intermediateMount, state.certIssuerId)
|
|
}
|
|
|
|
if state.csrGenerated {
|
|
message += "\n\nTO CONTINUE: \n" + state.toContinue()
|
|
}
|
|
if state.csrGenerated && !state.certImported {
|
|
message += "\n\nTO ABORT: \n" + state.toAbort()
|
|
}
|
|
|
|
message += "\n"
|
|
|
|
return message
|
|
}
|
|
|
|
func (state inCaseOfFailure) toContinue() string {
|
|
message := ""
|
|
if !state.csrSigned {
|
|
message += fmt.Sprintf("You can continue to work with this Certificate Signing Request CSR PEM, by saving"+
|
|
" it as `pki_int.csr`: %v \n Then call `vault write %v/sign-intermediate csr=@pki_int.csr ...` adding the "+
|
|
"same key-value arguements as to `pki issue` (except key_type and issuer_name) to generate the certificate "+
|
|
"and ca_chain", state.csr, state.parentIssuer)
|
|
}
|
|
if !state.certImported {
|
|
if state.caChain != "" {
|
|
message += fmt.Sprintf("The certificate chain, signed by %v, for this new certificate is: %v", state.parentIssuer, state.caChain)
|
|
}
|
|
message += fmt.Sprintf("You can continue to work with this Certificate (and chain) by saving it as "+
|
|
"chain.pem and importing it as `vault write %v/issuers/import/cert pem_bundle=@chain.pem`",
|
|
state.intermediateMount)
|
|
}
|
|
if !state.certNamed {
|
|
issuerId := state.certIssuerId
|
|
if issuerId == "" {
|
|
message += fmt.Sprintf("The issuer_id is returned as the key in a key_value map from importing the " +
|
|
"certificate chain.")
|
|
issuerId = "<issuer-uuid>"
|
|
}
|
|
message += fmt.Sprintf("You can name the newly imported issuer by calling `vault patch %v/issuer/%v "+
|
|
"issuer_name=%v`", state.intermediateMount, issuerId, state.newName)
|
|
}
|
|
return message
|
|
}
|
|
|
|
func (state inCaseOfFailure) toAbort() string {
|
|
if !state.csrGenerated || (!state.csrSigned && state.createdKeyId == "") {
|
|
return "No state was created by running this command. Try rerunning this command after resolving the error."
|
|
}
|
|
message := ""
|
|
if state.csrGenerated && state.createdKeyId != "" {
|
|
message += fmt.Sprintf(" A key, with key ID %v was created on mount %v as part of this command."+
|
|
" If you do not with to use this key and corresponding CSR/cert, you can delete that information by calling"+
|
|
" `vault delete %v/key/%v`", state.createdKeyId, state.intermediateMount, state.intermediateMount, state.createdKeyId)
|
|
}
|
|
if state.csrSigned {
|
|
message += fmt.Sprintf("A certificate with serial number %v was signed by mount %v as part of this command."+
|
|
" If you do not want to use this certificate, consider revoking it by calling `vault write %v/revoke/%v`",
|
|
state.certSerialNumber, state.parentMount, state.parentMount, state.certSerialNumber)
|
|
}
|
|
//if state.certImported {
|
|
// message += fmt.Sprintf("An issuer with UUID %v was created on mount %v as part of this command. " +
|
|
// "If you do not wish to use this issuer, consider deleting it by calling `vault delete %v/issuer/%v`",
|
|
// state.certIssuerId, state.intermediateMount, state.intermediateMount, state.certIssuerId)
|
|
//}
|
|
|
|
return message
|
|
}
|