Add per-issuer AIA URI information to PKI secrets engine (#16563)
* Add per-issuer AIA URI information Per discussion on GitHub with @maxb, this allows issuers to have their own copy of AIA URIs. Because each issuer has its own URLs (for CA and CRL access), its necessary to mint their issued certs pointing to the correct issuer and not to the global default issuer. For anyone using multiple issuers within a mount, this change allows the issuer to point back to itself via leaf's AIA info. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add documentation on per-issuer AIA info Also add it to the considerations page as something to watch out for. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tests for per-issuer AIA information Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Refactor AIA setting on the issuer This introduces a common helper per Steve's suggestion. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Clarify error messages w.r.t. AIA naming Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Clarify error messages regarding AIA URLs This clarifies which request parameter the invalid URL is contained in, disambiguating the sometimes ambiguous usage of AIA, per suggestion by Max. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Rename getURLs -> getGlobalAIAURLs Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Correct AIA acronym expansion word orders Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix bad comment suggesting re-generating roots Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add two entries to URL tests Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
parent
0c22c76907
commit
49fd772fcc
|
@ -4922,6 +4922,81 @@ AwEHoUQDQgAE57NX8bR/nDoW8yRgLswoXBQcjHrdyfuHS0gPwki6BNnfunUzryVb
|
||||||
require.Equal(t, len(importedIssuers), 0)
|
require.Equal(t, len(importedIssuers), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPerIssuerAIA(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
b, s := createBackendWithStorage(t)
|
||||||
|
|
||||||
|
// Generating a root without anything should not have AIAs.
|
||||||
|
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
|
||||||
|
"common_name": "root example.com",
|
||||||
|
"issuer_name": "root",
|
||||||
|
"key_type": "ec",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
rootCert := parseCert(t, resp.Data["certificate"].(string))
|
||||||
|
require.Empty(t, rootCert.OCSPServer)
|
||||||
|
require.Empty(t, rootCert.IssuingCertificateURL)
|
||||||
|
require.Empty(t, rootCert.CRLDistributionPoints)
|
||||||
|
|
||||||
|
// Set some local URLs on the issuer.
|
||||||
|
_, err = CBWrite(b, s, "issuer/default", map[string]interface{}{
|
||||||
|
"issuing_certificates": []string{"https://google.com"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = CBWrite(b, s, "roles/testing", map[string]interface{}{
|
||||||
|
"allow_any_name": true,
|
||||||
|
"ttl": "85s",
|
||||||
|
"key_type": "ec",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Issue something with this re-configured issuer.
|
||||||
|
resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{
|
||||||
|
"common_name": "localhost.com",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
leafCert := parseCert(t, resp.Data["certificate"].(string))
|
||||||
|
require.Empty(t, leafCert.OCSPServer)
|
||||||
|
require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://google.com"})
|
||||||
|
require.Empty(t, leafCert.CRLDistributionPoints)
|
||||||
|
|
||||||
|
// Set global URLs and ensure they don't appear on this issuer's leaf.
|
||||||
|
_, err = CBWrite(b, s, "config/urls", map[string]interface{}{
|
||||||
|
"issuing_certificates": []string{"https://example.com/ca", "https://backup.example.com/ca"},
|
||||||
|
"crl_distribution_points": []string{"https://example.com/crl", "https://backup.example.com/crl"},
|
||||||
|
"ocsp_servers": []string{"https://example.com/ocsp", "https://backup.example.com/ocsp"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{
|
||||||
|
"common_name": "localhost.com",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
leafCert = parseCert(t, resp.Data["certificate"].(string))
|
||||||
|
require.Empty(t, leafCert.OCSPServer)
|
||||||
|
require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://google.com"})
|
||||||
|
require.Empty(t, leafCert.CRLDistributionPoints)
|
||||||
|
|
||||||
|
// Now come back and remove the local modifications and ensure we get
|
||||||
|
// the defaults again.
|
||||||
|
_, err = CBPatch(b, s, "issuer/default", map[string]interface{}{
|
||||||
|
"issuing_certificates": []string{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{
|
||||||
|
"common_name": "localhost.com",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
leafCert = parseCert(t, resp.Data["certificate"].(string))
|
||||||
|
require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://example.com/ca", "https://backup.example.com/ca"})
|
||||||
|
require.Equal(t, leafCert.OCSPServer, []string{"https://example.com/ocsp", "https://backup.example.com/ocsp"})
|
||||||
|
require.Equal(t, leafCert.CRLDistributionPoints, []string{"https://example.com/crl", "https://backup.example.com/crl"})
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
initTest sync.Once
|
initTest sync.Once
|
||||||
rsaCAKey string
|
rsaCAKey string
|
||||||
|
|
|
@ -141,7 +141,7 @@ func (sc *storageContext) fetchCAInfoByIssuerId(issuerId issuerID, usage issuerU
|
||||||
RevocationSigAlg: entry.RevocationSigAlg,
|
RevocationSigAlg: entry.RevocationSigAlg,
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := getURLs(sc.Context, sc.Storage)
|
entries, err := entry.GetAIAURLs(sc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
|
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
|
||||||
}
|
}
|
||||||
|
@ -674,8 +674,9 @@ func generateCert(sc *storageContext,
|
||||||
data.Params.PermittedDNSDomains = input.apiData.Get("permitted_dns_domains").([]string)
|
data.Params.PermittedDNSDomains = input.apiData.Get("permitted_dns_domains").([]string)
|
||||||
|
|
||||||
if data.SigningBundle == nil {
|
if data.SigningBundle == nil {
|
||||||
// Generating a self-signed root certificate
|
// Generating a self-signed root certificate. Since we have no
|
||||||
entries, err := getURLs(ctx, sc.Storage)
|
// issuer entry yet, we default to the global URLs.
|
||||||
|
entries, err := getGlobalAIAURLs(ctx, sc.Storage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
|
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
|
||||||
}
|
}
|
||||||
|
@ -1395,7 +1396,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
|
||||||
return creation, nil
|
return creation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will have been read in from the getURLs function
|
// This will have been read in from the getGlobalAIAURLs function
|
||||||
creation.Params.URLs = caSign.URLs
|
creation.Params.URLs = caSign.URLs
|
||||||
|
|
||||||
// If the max path length in the role is not nil, it was specified at
|
// If the max path length in the role is not nil, it was specified at
|
||||||
|
|
|
@ -57,7 +57,7 @@ func validateURLs(urls []string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getURLs(ctx context.Context, storage logical.Storage) (*certutil.URLEntries, error) {
|
func getGlobalAIAURLs(ctx context.Context, storage logical.Storage) (*certutil.URLEntries, error) {
|
||||||
entry, err := storage.Get(ctx, "urls")
|
entry, err := storage.Get(ctx, "urls")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -98,7 +98,7 @@ func writeURLs(ctx context.Context, storage logical.Storage, entries *certutil.U
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) pathReadURL(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
func (b *backend) pathReadURL(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||||
entries, err := getURLs(ctx, req.Storage)
|
entries, err := getGlobalAIAURLs(ctx, req.Storage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ func (b *backend) pathReadURL(ctx context.Context, req *logical.Request, _ *fram
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) pathWriteURL(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
func (b *backend) pathWriteURL(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
entries, err := getURLs(ctx, req.Storage)
|
entries, err := getGlobalAIAURLs(ctx, req.Storage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -124,21 +124,21 @@ func (b *backend) pathWriteURL(ctx context.Context, req *logical.Request, data *
|
||||||
entries.IssuingCertificates = urlsInt.([]string)
|
entries.IssuingCertificates = urlsInt.([]string)
|
||||||
if badURL := validateURLs(entries.IssuingCertificates); badURL != "" {
|
if badURL := validateURLs(entries.IssuingCertificates); badURL != "" {
|
||||||
return logical.ErrorResponse(fmt.Sprintf(
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
"invalid URL found in issuing certificates: %s", badURL)), nil
|
"invalid URL found in Authority Information Access (AIA) parameter issuing_certificates: %s", badURL)), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if urlsInt, ok := data.GetOk("crl_distribution_points"); ok {
|
if urlsInt, ok := data.GetOk("crl_distribution_points"); ok {
|
||||||
entries.CRLDistributionPoints = urlsInt.([]string)
|
entries.CRLDistributionPoints = urlsInt.([]string)
|
||||||
if badURL := validateURLs(entries.CRLDistributionPoints); badURL != "" {
|
if badURL := validateURLs(entries.CRLDistributionPoints); badURL != "" {
|
||||||
return logical.ErrorResponse(fmt.Sprintf(
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
"invalid URL found in CRL distribution points: %s", badURL)), nil
|
"invalid URL found in Authority Information Access (AIA) parameter crl_distribution_points: %s", badURL)), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if urlsInt, ok := data.GetOk("ocsp_servers"); ok {
|
if urlsInt, ok := data.GetOk("ocsp_servers"); ok {
|
||||||
entries.OCSPServers = urlsInt.([]string)
|
entries.OCSPServers = urlsInt.([]string)
|
||||||
if badURL := validateURLs(entries.OCSPServers); badURL != "" {
|
if badURL := validateURLs(entries.OCSPServers); badURL != "" {
|
||||||
return logical.ErrorResponse(fmt.Sprintf(
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
"invalid URL found in OCSP servers: %s", badURL)), nil
|
"invalid URL found in Authority Information Access (AIA) parameter ocsp_servers: %s", badURL)), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,6 +119,21 @@ which may not be known at modification time (such as with PKCS#11 managed
|
||||||
RSA keys).`,
|
RSA keys).`,
|
||||||
Default: "",
|
Default: "",
|
||||||
}
|
}
|
||||||
|
fields["issuing_certificates"] = &framework.FieldSchema{
|
||||||
|
Type: framework.TypeCommaStringSlice,
|
||||||
|
Description: `Comma-separated list of URLs to be used
|
||||||
|
for the issuing certificate attribute. See also RFC 5280 Section 4.2.2.1.`,
|
||||||
|
}
|
||||||
|
fields["crl_distribution_points"] = &framework.FieldSchema{
|
||||||
|
Type: framework.TypeCommaStringSlice,
|
||||||
|
Description: `Comma-separated list of URLs to be used
|
||||||
|
for the CRL distribution points attribute. See also RFC 5280 Section 4.2.1.13.`,
|
||||||
|
}
|
||||||
|
fields["ocsp_servers"] = &framework.FieldSchema{
|
||||||
|
Type: framework.TypeCommaStringSlice,
|
||||||
|
Description: `Comma-separated list of URLs to be used
|
||||||
|
for the OCSP servers attribute. See also RFC 5280 Section 4.2.2.1.`,
|
||||||
|
}
|
||||||
|
|
||||||
return &framework.Path{
|
return &framework.Path{
|
||||||
// Returns a JSON entry.
|
// Returns a JSON entry.
|
||||||
|
@ -208,6 +223,9 @@ func respondReadIssuer(issuer *issuerEntry) (*logical.Response, error) {
|
||||||
"usage": issuer.Usage.Names(),
|
"usage": issuer.Usage.Names(),
|
||||||
"revocation_signature_algorithm": revSigAlgStr,
|
"revocation_signature_algorithm": revSigAlgStr,
|
||||||
"revoked": issuer.Revoked,
|
"revoked": issuer.Revoked,
|
||||||
|
"issuing_certificates": []string{},
|
||||||
|
"crl_distribution_points": []string{},
|
||||||
|
"ocsp_servers": []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if issuer.Revoked {
|
if issuer.Revoked {
|
||||||
|
@ -215,6 +233,12 @@ func respondReadIssuer(issuer *issuerEntry) (*logical.Response, error) {
|
||||||
data["revocation_time_rfc3339"] = issuer.RevocationTimeUTC.Format(time.RFC3339Nano)
|
data["revocation_time_rfc3339"] = issuer.RevocationTimeUTC.Format(time.RFC3339Nano)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issuer.AIAURIs != nil {
|
||||||
|
data["issuing_certificates"] = issuer.AIAURIs.IssuingCertificates
|
||||||
|
data["crl_distribution_points"] = issuer.AIAURIs.CRLDistributionPoints
|
||||||
|
data["ocsp_servers"] = issuer.AIAURIs.OCSPServers
|
||||||
|
}
|
||||||
|
|
||||||
return &logical.Response{
|
return &logical.Response{
|
||||||
Data: data,
|
Data: data,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -302,6 +326,20 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AIA access changes
|
||||||
|
issuerCertificates := data.Get("issuing_certificates").([]string)
|
||||||
|
if badURL := validateURLs(issuerCertificates); badURL != "" {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter issuing_certificates: %s", badURL)), nil
|
||||||
|
}
|
||||||
|
crlDistributionPoints := data.Get("crl_distribution_points").([]string)
|
||||||
|
if badURL := validateURLs(crlDistributionPoints); badURL != "" {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter crl_distribution_points: %s", badURL)), nil
|
||||||
|
}
|
||||||
|
ocspServers := data.Get("ocsp_servers").([]string)
|
||||||
|
if badURL := validateURLs(ocspServers); badURL != "" {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter ocsp_servers: %s", badURL)), nil
|
||||||
|
}
|
||||||
|
|
||||||
modified := false
|
modified := false
|
||||||
|
|
||||||
var oldName string
|
var oldName string
|
||||||
|
@ -331,6 +369,49 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issuer.AIAURIs == nil && (len(issuerCertificates) > 0 || len(crlDistributionPoints) > 0 || len(ocspServers) > 0) {
|
||||||
|
issuer.AIAURIs = &certutil.URLEntries{}
|
||||||
|
}
|
||||||
|
if issuer.AIAURIs != nil {
|
||||||
|
// Associative mapping from data source to destination on the
|
||||||
|
// backing issuer object.
|
||||||
|
type aiaPair struct {
|
||||||
|
Source *[]string
|
||||||
|
Dest *[]string
|
||||||
|
}
|
||||||
|
pairs := []aiaPair{
|
||||||
|
{
|
||||||
|
Source: &issuerCertificates,
|
||||||
|
Dest: &issuer.AIAURIs.IssuingCertificates,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: &crlDistributionPoints,
|
||||||
|
Dest: &issuer.AIAURIs.CRLDistributionPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: &ocspServers,
|
||||||
|
Dest: &issuer.AIAURIs.OCSPServers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each pair, if it is different on the object, update it.
|
||||||
|
for _, pair := range pairs {
|
||||||
|
if isStringArrayDifferent(*pair.Source, *pair.Dest) {
|
||||||
|
*pair.Dest = *pair.Source
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no AIA URLs exist on the issuer, set the AIA URLs entry to nil
|
||||||
|
// to ease usage later.
|
||||||
|
if len(issuer.AIAURIs.IssuingCertificates) == 0 && len(issuer.AIAURIs.CRLDistributionPoints) == 0 && len(issuer.AIAURIs.OCSPServers) == 0 {
|
||||||
|
issuer.AIAURIs = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updating the chain should be the last modification as there's a chance
|
||||||
|
// it'll write it out to disk for us. We'd hate to then modify the issuer
|
||||||
|
// again and write it a second time.
|
||||||
var updateChain bool
|
var updateChain bool
|
||||||
var constructedChain []issuerID
|
var constructedChain []issuerID
|
||||||
for index, newPathRef := range newPath {
|
for index, newPathRef := range newPath {
|
||||||
|
@ -511,6 +592,56 @@ func (b *backend) pathPatchIssuer(ctx context.Context, req *logical.Request, dat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AIA access changes.
|
||||||
|
if issuer.AIAURIs == nil {
|
||||||
|
issuer.AIAURIs = &certutil.URLEntries{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associative mapping from data source to destination on the
|
||||||
|
// backing issuer object. For PATCH requests, we use the source
|
||||||
|
// data parameter as we still need to validate them and process
|
||||||
|
// it into a string list.
|
||||||
|
type aiaPair struct {
|
||||||
|
Source string
|
||||||
|
Dest *[]string
|
||||||
|
}
|
||||||
|
pairs := []aiaPair{
|
||||||
|
{
|
||||||
|
Source: "issuing_certificates",
|
||||||
|
Dest: &issuer.AIAURIs.IssuingCertificates,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: "crl_distribution_points",
|
||||||
|
Dest: &issuer.AIAURIs.CRLDistributionPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: "ocsp_servers",
|
||||||
|
Dest: &issuer.AIAURIs.OCSPServers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each pair, if it is different on the object, update it.
|
||||||
|
for _, pair := range pairs {
|
||||||
|
rawURLsValue, ok := data.GetOk(pair.Source)
|
||||||
|
if ok {
|
||||||
|
urlsValue := rawURLsValue.([]string)
|
||||||
|
if badURL := validateURLs(urlsValue); badURL != "" {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter %v: %s", pair.Source, badURL)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isStringArrayDifferent(urlsValue, *pair.Dest) {
|
||||||
|
modified = true
|
||||||
|
*pair.Dest = urlsValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no AIA URLs exist on the issuer, set the AIA URLs entry to nil to
|
||||||
|
// ease usage later.
|
||||||
|
if len(issuer.AIAURIs.IssuingCertificates) == 0 && len(issuer.AIAURIs.CRLDistributionPoints) == 0 && len(issuer.AIAURIs.OCSPServers) == 0 {
|
||||||
|
issuer.AIAURIs = nil
|
||||||
|
}
|
||||||
|
|
||||||
// Manual Chain Changes
|
// Manual Chain Changes
|
||||||
newPathData, ok := data.GetOk("manual_chain")
|
newPathData, ok := data.GetOk("manual_chain")
|
||||||
if ok {
|
if ok {
|
||||||
|
|
|
@ -114,12 +114,12 @@ func (b *backend) pathGenerateIntermediate(ctx context.Context, req *logical.Req
|
||||||
Data: map[string]interface{}{},
|
Data: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := getURLs(ctx, req.Storage)
|
entries, err := getGlobalAIAURLs(ctx, req.Storage)
|
||||||
if err == nil && len(entries.OCSPServers) == 0 && len(entries.IssuingCertificates) == 0 && len(entries.CRLDistributionPoints) == 0 {
|
if err == nil && len(entries.OCSPServers) == 0 && len(entries.IssuingCertificates) == 0 && len(entries.CRLDistributionPoints) == 0 {
|
||||||
// If the operator hasn't configured any of the URLs prior to
|
// If the operator hasn't configured any of the URLs prior to
|
||||||
// generating this issuer, we should add a warning to the response,
|
// generating this issuer, we should add a warning to the response,
|
||||||
// informing them they might want to do so and re-generate the issuer.
|
// informing them they might want to do so and re-generate the issuer.
|
||||||
resp.AddWarning("This mount hasn't configured any authority access information fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls with this information.")
|
resp.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information. Since this certificate is an intermediate, it might be useful to regenerate this certificate after fixing this problem for the root mount.")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
|
|
|
@ -300,8 +300,8 @@ func (b *backend) pathImportIssuers(ctx context.Context, req *logical.Request, d
|
||||||
// Also while we're here, we should let the user know the next steps.
|
// Also while we're here, we should let the user know the next steps.
|
||||||
// In particular, if there's no default AIA URLs configuration, we should
|
// In particular, if there's no default AIA URLs configuration, we should
|
||||||
// tell the user that's probably next.
|
// tell the user that's probably next.
|
||||||
if entries, err := getURLs(ctx, req.Storage); err == nil && len(entries.IssuingCertificates) == 0 && len(entries.CRLDistributionPoints) == 0 && len(entries.OCSPServers) == 0 {
|
if entries, err := getGlobalAIAURLs(ctx, req.Storage); err == nil && len(entries.IssuingCertificates) == 0 && len(entries.CRLDistributionPoints) == 0 && len(entries.OCSPServers) == 0 {
|
||||||
response.AddWarning("This mount hasn't configured any authority access information fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls with this information.")
|
response.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
|
|
|
@ -185,7 +185,7 @@ func (b *backend) pathCAGenerateRoot(ctx context.Context, req *logical.Request,
|
||||||
// If the operator hasn't configured any of the URLs prior to
|
// If the operator hasn't configured any of the URLs prior to
|
||||||
// generating this issuer, we should add a warning to the response,
|
// generating this issuer, we should add a warning to the response,
|
||||||
// informing them they might want to do so prior to issuing leaves.
|
// informing them they might want to do so prior to issuing leaves.
|
||||||
resp.AddWarning("This mount hasn't configured any authority access information fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls with this information.")
|
resp.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information.")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
|
@ -407,8 +407,8 @@ func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.R
|
||||||
if len(parsedBundle.Certificate.OCSPServer) == 0 && len(parsedBundle.Certificate.IssuingCertificateURL) == 0 && len(parsedBundle.Certificate.CRLDistributionPoints) == 0 {
|
if len(parsedBundle.Certificate.OCSPServer) == 0 && len(parsedBundle.Certificate.IssuingCertificateURL) == 0 && len(parsedBundle.Certificate.CRLDistributionPoints) == 0 {
|
||||||
// If the operator hasn't configured any of the URLs prior to
|
// If the operator hasn't configured any of the URLs prior to
|
||||||
// generating this issuer, we should add a warning to the response,
|
// generating this issuer, we should add a warning to the response,
|
||||||
// informing them they might want to do so and re-generate the issuer.
|
// informing them they might want to do so prior to issuing leaves.
|
||||||
resp.AddWarning("This mount hasn't configured any authority access information fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls with this information.")
|
resp.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information.")
|
||||||
}
|
}
|
||||||
|
|
||||||
caChain := append([]string{cb.Certificate}, cb.CAChain...)
|
caChain := append([]string{cb.Certificate}, cb.CAChain...)
|
||||||
|
|
|
@ -150,6 +150,7 @@ type issuerEntry struct {
|
||||||
Revoked bool `json:"revoked"`
|
Revoked bool `json:"revoked"`
|
||||||
RevocationTime int64 `json:"revocation_time"`
|
RevocationTime int64 `json:"revocation_time"`
|
||||||
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
|
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
|
||||||
|
AIAURIs *certutil.URLEntries `json:"aia_uris,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type localCRLConfigEntry struct {
|
type localCRLConfigEntry struct {
|
||||||
|
@ -477,6 +478,19 @@ func (i issuerEntry) CanMaybeSignWithAlgo(algo x509.SignatureAlgorithm) error {
|
||||||
return fmt.Errorf("unable to use issuer of type %v to sign with %v key type", cert.PublicKeyAlgorithm.String(), algo.String())
|
return fmt.Errorf("unable to use issuer of type %v to sign with %v key type", cert.PublicKeyAlgorithm.String(), algo.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i issuerEntry) GetAIAURLs(sc *storageContext) (urls *certutil.URLEntries, err error) {
|
||||||
|
// Default to the per-issuer AIA URLs.
|
||||||
|
urls = 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 urls == nil || (len(urls.IssuingCertificates) == 0 && len(urls.CRLDistributionPoints) == 0 && len(urls.OCSPServers) == 0) {
|
||||||
|
urls, err = getGlobalAIAURLs(sc.Context, sc.Storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return urls, err
|
||||||
|
}
|
||||||
|
|
||||||
func (sc *storageContext) listIssuers() ([]issuerID, error) {
|
func (sc *storageContext) listIssuers() ([]issuerID, error) {
|
||||||
strList, err := sc.Storage.List(sc.Context, issuerPrefix)
|
strList, err := sc.Storage.List(sc.Context, issuerPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -208,3 +208,17 @@ func extractRef(data *framework.FieldData, paramName string) string {
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isStringArrayDifferent(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range a {
|
||||||
|
if v != b[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
secrets/pki: Add support for per-issuer Authority Information Access (AIA) URLs
|
||||||
|
```
|
|
@ -1995,6 +1995,29 @@ do so, import a new issuer and a new `issuer_id` will be assigned.
|
||||||
This most commonly needs to be modified when using PKCS#11 managed keys
|
This most commonly needs to be modified when using PKCS#11 managed keys
|
||||||
with the `CKM_RSA_PKCS_PSS` mechanism type.
|
with the `CKM_RSA_PKCS_PSS` mechanism type.
|
||||||
|
|
||||||
|
- `issuing_certificates` `(array<string>: nil)` - Specifies the URL values for
|
||||||
|
the Issuing Certificate field. This can be an array or a comma-separated
|
||||||
|
string list. See also [RFC 5280 Section 4.2.2.1](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.2.1)
|
||||||
|
for information about the Authority Information Access field.
|
||||||
|
|
||||||
|
- `crl_distribution_points` `(array<string>: nil)` - Specifies the URL values
|
||||||
|
for the CRL Distribution Points field. This can be an array or a
|
||||||
|
comma-separated string list. See also [RFC 5280 Section 4.2.1.13](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.13)
|
||||||
|
for information about the CRL Distribution Points field.
|
||||||
|
|
||||||
|
~> Note: When multiple Performance Replication clusters are enabled, each
|
||||||
|
cluster will have its own CRL. Additionally, when multiple issuers are
|
||||||
|
in use under a single mount, each issuer will also have its own CRL
|
||||||
|
distribution point. These separate CRLs should either be aggregated into a
|
||||||
|
single CRL (externally; as Vault does not support this functionality)
|
||||||
|
or multiple `crl_distribution_points` should be specified here, pointing
|
||||||
|
to each cluster and issuer.
|
||||||
|
|
||||||
|
- `ocsp_servers` `(array<string>: nil)` - Specifies the URL values for the OCSP
|
||||||
|
Servers field. This can be an array or a comma-separated string list. See also
|
||||||
|
[RFC 5280 Section 4.2.2.1](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.2.1)
|
||||||
|
for information about the Authority Information Access field.
|
||||||
|
|
||||||
#### Sample Payload
|
#### Sample Payload
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -2028,7 +2051,11 @@ $ curl \
|
||||||
"key_id": "baadd98d-ec5a-66ac-06b7-dfc91c02c9cf",
|
"key_id": "baadd98d-ec5a-66ac-06b7-dfc91c02c9cf",
|
||||||
"leaf_not_after_behavior": "truncate",
|
"leaf_not_after_behavior": "truncate",
|
||||||
"manual_chain": null,
|
"manual_chain": null,
|
||||||
"usage": "read-only,issuing-certificates,crl-signing"
|
"usage": "read-only,issuing-certificates,crl-signing",
|
||||||
|
"revocation_signature_algorithm": "",
|
||||||
|
"issuing_certificates": ["<url1>", "<url2>"],
|
||||||
|
"crl_distribution_points": ["<url1>", "<url2>"],
|
||||||
|
"ocsp_servers": ["<url1>", "<url2>"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -2713,6 +2740,12 @@ parameter.
|
||||||
| :----- | :----------------- |
|
| :----- | :----------------- |
|
||||||
| `POST` | `/pki/config/urls` |
|
| `POST` | `/pki/config/urls` |
|
||||||
|
|
||||||
|
~> **Note**: When using multiple issuers within the same mount, it is strongly
|
||||||
|
suggested to use the per-issuer AIA information instead of the global
|
||||||
|
AIA information. If any of the per-issuer AIA fields are set, the entire
|
||||||
|
issuer's preferences will be used instead. Otherwise, these fields are used
|
||||||
|
as a fallback.
|
||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
- `issuing_certificates` `(array<string>: nil)` - Specifies the URL values for
|
- `issuing_certificates` `(array<string>: nil)` - Specifies the URL values for
|
||||||
|
|
|
@ -258,6 +258,12 @@ comma-separated string parameter.
|
||||||
field separately, or the CRLs should be consolidated and served outside of
|
field separately, or the CRLs should be consolidated and served outside of
|
||||||
Vault.
|
Vault.
|
||||||
|
|
||||||
|
~> Note: When using multiple issuers in the same mount, it is suggested to use
|
||||||
|
the per-issuer AIA fields rather than the global (`/config/urls`) variant.
|
||||||
|
This is for correctness: these fields are used for chain building and
|
||||||
|
automatic CRL detection in certain applications. If they point to the wrong
|
||||||
|
issuer's information, these applications may break.
|
||||||
|
|
||||||
## Automate Leaf Certificate Renewal
|
## Automate Leaf Certificate Renewal
|
||||||
|
|
||||||
As much as possible, for managing certificates for services at scale, it is
|
As much as possible, for managing certificates for services at scale, it is
|
||||||
|
|
Loading…
Reference in New Issue