9338c22c53
* Rename files to match test suite and existing pattern * Factor out issuer loading into a dedicated function - Add a little more checks/validation when loading the a PKI issuer - Factor out the issuer loading into a dedicated function - Leverage existing health check code to parse issuer certificates * Read parent issuer once instead of reloading it for every child - Read in our parent issuer once instead of running it for every child we want to compare against - Provides clearer error message that we have failed reading from which path to the end user * PR Feedback - Rename a variable for clarity - Use readIssuer in the validation of the parent issuer within pkiIssuer - Add some missing return 1 statements in error handlers that had been missed
307 lines
8.3 KiB
Go
307 lines
8.3 KiB
Go
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
|
|
}
|