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