vault/command/pki_issue_intermediate.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* 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>
2023-08-10 18:14:03 -07:00

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
}