| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "bytes" |
| "crypto/x509" |
| "encoding/json" |
| "fmt" |
| "strconv" |
| "strings" |
| |
| "github.com/hashicorp/vault/command/healthcheck" |
| |
| "github.com/ghodss/yaml" |
| "github.com/hashicorp/vault/api" |
| "github.com/ryanuber/columnize" |
| ) |
| |
| type PKIVerifySignCommand struct { |
| *BaseCommand |
| |
| flagConfig string |
| flagReturnIndicator string |
| flagDefaultDisabled bool |
| flagList bool |
| } |
| |
| func (c *PKIVerifySignCommand) Synopsis() string { |
| return "Check whether one certificate validates another specified certificate" |
| } |
| |
| func (c *PKIVerifySignCommand) Help() string { |
| helpText := ` |
| Usage: vault pki verify-sign POSSIBLE-ISSUER POSSIBLE-ISSUED |
| |
| Verifies whether the listed issuer has signed the listed issued certificate. |
| |
| POSSIBLE-ISSUER and POSSIBLE-ISSUED are the fully name-spaced path to |
| an issuer certificate, for instance: 'ns1/mount1/issuer/issuerName/json'. |
| |
| Returns five fields of information: |
| |
| - signature_match: was the key of the issuer used to sign the issued. |
| - path_match: the possible issuer appears in the valid certificate chain |
| of the issued. |
| - key_id_match: does the key-id of the issuer match the key_id of the |
| subject. |
| - subject_match: does the subject name of the issuer match the issuer |
| subject of the issued. |
| - trust_match: if someone trusted the parent issuer, is the chain |
| provided sufficient to trust the child issued. |
| |
| ` + c.Flags().Help() |
| return strings.TrimSpace(helpText) |
| } |
| |
| func (c *PKIVerifySignCommand) Flags() *FlagSets { |
| set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) |
| return set |
| } |
| |
| func (c *PKIVerifySignCommand) Run(args []string) int { |
| f := c.Flags() |
| if err := f.Parse(args); err != nil { |
| c.UI.Error(err.Error()) |
| return 1 |
| } |
| |
| args = f.Args() |
| |
| if len(args) < 2 { |
| if len(args) == 0 { |
| c.UI.Error("Not enough arguments (expected potential issuer and issued, got nothing)") |
| } else { |
| c.UI.Error("Not enough arguments (expected both potential issuer and issued, got only one)") |
| } |
| return 1 |
| } else if len(args) > 2 { |
| c.UI.Error(fmt.Sprintf("Too many arguments (expected only potential issuer and issued, got %d arguments)", len(args))) |
| for _, arg := range args { |
| if strings.HasPrefix(arg, "-") { |
| c.UI.Warn(fmt.Sprintf("Options (%v) must be specified before positional arguments (%v)", arg, args[0])) |
| break |
| } |
| } |
| return 1 |
| } |
| |
| issuer := sanitizePath(args[0]) |
| issued := sanitizePath(args[1]) |
| |
| client, err := c.Client() |
| if err != nil { |
| c.UI.Error(fmt.Sprintf("Failed to obtain client: %s", err)) |
| return 1 |
| } |
| |
| issuerResp, err := readIssuer(client, issuer) |
| if err != nil { |
| c.UI.Error(fmt.Sprintf("Failed to read issuer: %s: %s", issuer, err.Error())) |
| return 1 |
| } |
| |
| results, err := verifySignBetween(client, issuerResp, issued) |
| if err != nil { |
| c.UI.Error(fmt.Sprintf("Failed to run verification: %v", err)) |
| return pkiRetUsage |
| } |
| |
| c.outputResults(results, issuer, issued) |
| |
| return 0 |
| } |
| |
| func verifySignBetween(client *api.Client, issuerResp *issuerResponse, issuedPath string) (map[string]bool, error) { |
| // Note that this eats warnings |
| |
| issuerCert := issuerResp.certificate |
| issuerKeyId := issuerCert.SubjectKeyId |
| |
| // Fetch and Parse the Potential Issued Cert |
| issuedCertBundle, err := readIssuer(client, issuedPath) |
| if err != nil { |
| return nil, fmt.Errorf("error: unable to fetch issuer %v: %w", issuedPath, err) |
| } |
| parentKeyId := issuedCertBundle.certificate.AuthorityKeyId |
| |
| // Check the Chain-Match |
| rootCertPool := x509.NewCertPool() |
| rootCertPool.AddCert(issuerCert) |
| checkTrustPathOptions := x509.VerifyOptions{ |
| Roots: rootCertPool, |
| } |
| trust := false |
| trusts, err := issuedCertBundle.certificate.Verify(checkTrustPathOptions) |
| if err != nil && !strings.Contains(err.Error(), "certificate signed by unknown authority") { |
| return nil, err |
| } else if err == nil { |
| for _, chain := range trusts { |
| // Output of this Should Only Have One Trust with Chain of Length Two (Child followed by Parent) |
| for _, cert := range chain { |
| if issuedCertBundle.certificate.Equal(cert) { |
| trust = true |
| break |
| } |
| } |
| } |
| } |
| |
| pathMatch := false |
| for _, cert := range issuedCertBundle.caChain { |
| if bytes.Equal(cert.Raw, issuerCert.Raw) { |
| pathMatch = true |
| break |
| } |
| } |
| |
| signatureMatch := false |
| err = issuedCertBundle.certificate.CheckSignatureFrom(issuerCert) |
| if err == nil { |
| signatureMatch = true |
| } |
| |
| result := map[string]bool{ |
| // This comparison isn't strictly correct, despite a standard ordering these are sets |
| "subject_match": bytes.Equal(issuerCert.RawSubject, issuedCertBundle.certificate.RawIssuer), |
| "path_match": pathMatch, |
| "trust_match": trust, // TODO: Refactor into a reasonable function |
| "key_id_match": bytes.Equal(parentKeyId, issuerKeyId), |
| "signature_match": signatureMatch, |
| } |
| |
| return result, nil |
| } |
| |
| type issuerResponse struct { |
| keyId string |
| certificate *x509.Certificate |
| caChain []*x509.Certificate |
| } |
| |
| func readIssuer(client *api.Client, issuerPath string) (*issuerResponse, error) { |
| issuerResp, err := client.Logical().Read(issuerPath) |
| if err != nil { |
| return nil, err |
| } |
| issuerCertPem, err := requireStrRespField(issuerResp, "certificate") |
| if err != nil { |
| return nil, err |
| } |
| issuerCert, err := healthcheck.ParsePEMCert(issuerCertPem) |
| if err != nil { |
| return nil, fmt.Errorf("unable to parse issuer %v's certificate: %w", issuerPath, err) |
| } |
| |
| caChainPem, err := requireStrListRespField(issuerResp, "ca_chain") |
| if err != nil { |
| return nil, fmt.Errorf("unable to parse issuer %v's CA chain: %w", issuerPath, err) |
| } |
| |
| var caChain []*x509.Certificate |
| for _, pem := range caChainPem { |
| trimmedPem := strings.TrimSpace(pem) |
| if trimmedPem == "" { |
| continue |
| } |
| cert, err := healthcheck.ParsePEMCert(trimmedPem) |
| if err != nil { |
| return nil, err |
| } |
| caChain = append(caChain, cert) |
| } |
| |
| keyId := optStrRespField(issuerResp, "key_id") |
| |
| return &issuerResponse{ |
| keyId: keyId, |
| certificate: issuerCert, |
| caChain: caChain, |
| }, nil |
| } |
| |
| func optStrRespField(resp *api.Secret, reqField string) string { |
| if resp == nil || resp.Data == nil { |
| return "" |
| } |
| if val, present := resp.Data[reqField]; !present { |
| return "" |
| } else if strVal, castOk := val.(string); !castOk || strVal == "" { |
| return "" |
| } else { |
| return strVal |
| } |
| } |
| |
| func requireStrRespField(resp *api.Secret, reqField string) (string, error) { |
| if resp == nil || resp.Data == nil { |
| return "", fmt.Errorf("nil response received, %s field unavailable", reqField) |
| } |
| if val, present := resp.Data[reqField]; !present { |
| return "", fmt.Errorf("response did not contain field: %s", reqField) |
| } else if strVal, castOk := val.(string); !castOk || strVal == "" { |
| return "", fmt.Errorf("field %s value was blank or not a string: %v", reqField, val) |
| } else { |
| return strVal, nil |
| } |
| } |
| |
| func requireStrListRespField(resp *api.Secret, reqField string) ([]string, error) { |
| if resp == nil || resp.Data == nil { |
| return nil, fmt.Errorf("nil response received, %s field unavailable", reqField) |
| } |
| if val, present := resp.Data[reqField]; !present { |
| return nil, fmt.Errorf("response did not contain field: %s", reqField) |
| } else { |
| return healthcheck.StringList(val) |
| } |
| } |
| |
| func (c *PKIVerifySignCommand) outputResults(results map[string]bool, potentialParent, potentialChild string) error { |
| switch Format(c.UI) { |
| case "", "table": |
| return c.outputResultsTable(results, potentialParent, potentialChild) |
| case "json": |
| return c.outputResultsJSON(results) |
| case "yaml": |
| return c.outputResultsYAML(results) |
| default: |
| return fmt.Errorf("unknown output format: %v", Format(c.UI)) |
| } |
| } |
| |
| func (c *PKIVerifySignCommand) outputResultsTable(results map[string]bool, potentialParent, potentialChild string) error { |
| c.UI.Output("issuer:" + potentialParent) |
| c.UI.Output("issued:" + potentialChild + "\n") |
| data := []string{"field" + hopeDelim + "value"} |
| for field, finding := range results { |
| row := field + hopeDelim + strconv.FormatBool(finding) |
| data = append(data, row) |
| } |
| c.UI.Output(tableOutput(data, &columnize.Config{ |
| Delim: hopeDelim, |
| })) |
| c.UI.Output("\n") |
| |
| return nil |
| } |
| |
| func (c *PKIVerifySignCommand) outputResultsJSON(results map[string]bool) error { |
| bytes, err := json.MarshalIndent(results, "", " ") |
| if err != nil { |
| return err |
| } |
| |
| c.UI.Output(string(bytes)) |
| return nil |
| } |
| |
| func (c *PKIVerifySignCommand) outputResultsYAML(results map[string]bool) error { |
| bytes, err := yaml.Marshal(results) |
| if err != nil { |
| return err |
| } |
| |
| c.UI.Output(string(bytes)) |
| return nil |
| } |