mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			368 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			368 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) HashiCorp, Inc.
 | |
| // SPDX-License-Identifier: MPL-2.0
 | |
| 
 | |
| 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
 | |
| }
 | 
