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