mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-01 12:01:10 +02:00
* 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>
185 lines
5.1 KiB
Go
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
|
|
}
|