open-vault/command/pki_verify_sign_command.go
Steven Clark 5329b92793
Stop using title capitalization for PKI help (#19104)
- Match the existing vault kv capitalization scheme for Synopsis help of each sub-command.
 - A few small tweaks as well to the messages text in a few cases
2023-02-09 16:40:26 +00:00

242 lines
6.8 KiB
Go

package command
import (
"bytes"
"crypto/x509"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/ghodss/yaml"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/certutil"
"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
}
err, results := verifySignBetween(client, issuer, 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, issuerPath string, issuedPath string) (error, map[string]bool) {
// Note that this eats warnings
// Fetch and Parse the Potential Issuer:
issuerResp, err := client.Logical().Read(issuerPath)
if err != nil {
return fmt.Errorf("error: unable to fetch issuer %v: %w", issuerPath, err), nil
}
issuerCertPem := issuerResp.Data["certificate"].(string)
issuerCertBundle, err := certutil.ParsePEMBundle(issuerCertPem)
if err != nil {
return err, nil
}
issuerKeyId := issuerCertBundle.Certificate.SubjectKeyId
// Fetch and Parse the Potential Issued Cert
issuedCertResp, err := client.Logical().Read(issuedPath)
if err != nil {
return fmt.Errorf("error: unable to fetch issuer %v: %w", issuerPath, err), nil
}
if len(issuedPath) <= 2 {
return fmt.Errorf("%v", issuedPath), nil
}
caChainRaw := issuedCertResp.Data["ca_chain"]
if caChainRaw == nil {
return fmt.Errorf("no ca_chain information on %v", issuedPath), nil
}
caChainCast := caChainRaw.([]interface{})
caChain := make([]string, len(caChainCast))
for i, cert := range caChainCast {
caChain[i] = cert.(string)
}
issuedCertPem := issuedCertResp.Data["certificate"].(string)
issuedCertBundle, err := certutil.ParsePEMBundle(issuedCertPem)
if err != nil {
return err, nil
}
parentKeyId := issuedCertBundle.Certificate.AuthorityKeyId
// Check the Chain-Match
rootCertPool := x509.NewCertPool()
rootCertPool.AddCert(issuerCertBundle.Certificate)
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 err, nil
} 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 caChain {
if strings.TrimSpace(cert) == strings.TrimSpace(issuerCertPem) { // TODO: Decode into ASN1 to Check
pathMatch = true
break
}
}
signatureMatch := false
err = issuedCertBundle.Certificate.CheckSignatureFrom(issuerCertBundle.Certificate)
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(issuerCertBundle.Certificate.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 nil, result
}
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
}