// 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 = "" } 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 }