Scott Miller fd9e113c82
Use a less strict URL validation for PKI issuing and crl distribution urls (#26477)
* Use a less strict URL validation for PKI issuing and crl distribution urls

* comma handling

* limit to ldap

* remove comma hack

* changelog

* Add unit test validating ldap CRL urls

---------

Co-authored-by: Steve Clark <steven.clark@hashicorp.com>
2024-04-18 17:35:33 +00:00

185 lines
5.1 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package issuing
import (
"context"
"fmt"
"net/url"
"strings"
"unicode/utf8"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/logical"
)
const ClusterConfigPath = "config/cluster"
type AiaConfigEntry struct {
IssuingCertificates []string `json:"issuing_certificates"`
CRLDistributionPoints []string `json:"crl_distribution_points"`
OCSPServers []string `json:"ocsp_servers"`
EnableTemplating bool `json:"enable_templating"`
}
type ClusterConfigEntry struct {
Path string `json:"path"`
AIAPath string `json:"aia_path"`
}
func GetAIAURLs(ctx context.Context, s logical.Storage, i *IssuerEntry) (*certutil.URLEntries, error) {
// Default to the per-issuer AIA URLs.
entries := i.AIAURIs
// If none are set (either due to a nil entry or because no URLs have
// been provided), fall back to the global AIA URL config.
if entries == nil || (len(entries.IssuingCertificates) == 0 && len(entries.CRLDistributionPoints) == 0 && len(entries.OCSPServers) == 0) {
var err error
entries, err = GetGlobalAIAURLs(ctx, s)
if err != nil {
return nil, err
}
}
if entries == nil {
return &certutil.URLEntries{}, nil
}
return ToURLEntries(ctx, s, i.ID, entries)
}
func GetGlobalAIAURLs(ctx context.Context, storage logical.Storage) (*AiaConfigEntry, error) {
entry, err := storage.Get(ctx, "urls")
if err != nil {
return nil, err
}
entries := &AiaConfigEntry{
IssuingCertificates: []string{},
CRLDistributionPoints: []string{},
OCSPServers: []string{},
EnableTemplating: false,
}
if entry == nil {
return entries, nil
}
if err := entry.DecodeJSON(entries); err != nil {
return nil, err
}
return entries, nil
}
func ToURLEntries(ctx context.Context, s logical.Storage, issuer IssuerID, c *AiaConfigEntry) (*certutil.URLEntries, error) {
if len(c.IssuingCertificates) == 0 && len(c.CRLDistributionPoints) == 0 && len(c.OCSPServers) == 0 {
return &certutil.URLEntries{}, nil
}
result := certutil.URLEntries{
IssuingCertificates: c.IssuingCertificates[:],
CRLDistributionPoints: c.CRLDistributionPoints[:],
OCSPServers: c.OCSPServers[:],
}
if c.EnableTemplating {
cfg, err := GetClusterConfig(ctx, s)
if err != nil {
return nil, fmt.Errorf("error fetching cluster-local address config: %w", err)
}
for name, source := range map[string]*[]string{
"issuing_certificates": &result.IssuingCertificates,
"crl_distribution_points": &result.CRLDistributionPoints,
"ocsp_servers": &result.OCSPServers,
} {
templated := make([]string, len(*source))
for index, uri := range *source {
if strings.Contains(uri, "{{cluster_path}}") && len(cfg.Path) == 0 {
return nil, fmt.Errorf("unable to template AIA URLs as we lack local cluster address information (path)")
}
if strings.Contains(uri, "{{cluster_aia_path}}") && len(cfg.AIAPath) == 0 {
return nil, fmt.Errorf("unable to template AIA URLs as we lack local cluster address information (aia_path)")
}
if strings.Contains(uri, "{{issuer_id}}") && len(issuer) == 0 {
// Elide issuer AIA info as we lack an issuer_id.
return nil, fmt.Errorf("unable to template AIA URLs as we lack an issuer_id for this operation")
}
uri = strings.ReplaceAll(uri, "{{cluster_path}}", cfg.Path)
uri = strings.ReplaceAll(uri, "{{cluster_aia_path}}", cfg.AIAPath)
uri = strings.ReplaceAll(uri, "{{issuer_id}}", issuer.String())
templated[index] = uri
}
if uri := ValidateURLs(templated); uri != "" {
return nil, fmt.Errorf("error validating templated %v; invalid URI: %v", name, uri)
}
*source = templated
}
}
return &result, nil
}
func GetClusterConfig(ctx context.Context, s logical.Storage) (*ClusterConfigEntry, error) {
entry, err := s.Get(ctx, ClusterConfigPath)
if err != nil {
return nil, err
}
var result ClusterConfigEntry
if entry == nil {
return &result, nil
}
if err = entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func ValidateURLs(urls []string) string {
for _, curr := range urls {
if !isURL(curr) || strings.Contains(curr, "{{issuer_id}}") || strings.Contains(curr, "{{cluster_path}}") || strings.Contains(curr, "{{cluster_aia_path}}") {
return curr
}
}
return ""
}
const (
maxURLRuneCount = 2083
minURLRuneCount = 3
)
// IsURL checks if the string is an URL.
func isURL(str string) bool {
if str == "" || utf8.RuneCountInString(str) >= maxURLRuneCount || len(str) <= minURLRuneCount || strings.HasPrefix(str, ".") {
return false
}
strTemp := str
if strings.Contains(str, ":") && !strings.Contains(str, "://") {
// support no indicated urlscheme but with colon for port number
// http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString
strTemp = "http://" + str
}
u, err := url.ParseRequestURI(strTemp)
if err != nil {
return false
}
if strings.HasPrefix(u.Host, ".") {
return false
}
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
return false
}
return true
}