Skip to content

Commit

Permalink
Add per-issuer AIA URI information
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
cipherboy committed Jul 25, 2022
1 parent 0e1bec0 commit 7ab6961
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 16 deletions.
5 changes: 3 additions & 2 deletions builtin/logical/pki/cert_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (sc *storageContext) fetchCAInfoByIssuerId(issuerId issuerID, usage issuerU
LeafNotAfterBehavior: entry.LeafNotAfterBehavior,
}

entries, err := getURLs(sc.Context, sc.Storage)
entries, err := entry.GetAIAURLs(sc)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
}
Expand Down Expand Up @@ -672,7 +672,8 @@ func generateCert(sc *storageContext,
data.Params.PermittedDNSDomains = input.apiData.Get("permitted_dns_domains").([]string)

if data.SigningBundle == nil {
// Generating a self-signed root certificate
// Generating a self-signed root certificate. Since we have no
// issuer entry yet, we default to the global URLs.
entries, err := getURLs(ctx, sc.Storage)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
Expand Down
176 changes: 166 additions & 10 deletions builtin/logical/pki/path_fetch_issuers.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ this issuer; valid values are "read-only", "issuing-certificates", and
and always set.`,
Default: []string{"read-only", "issuing-certificates", "crl-signing"},
}
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{
// Returns a JSON entry.
Expand Down Expand Up @@ -179,17 +194,28 @@ func respondReadIssuer(issuer *issuerEntry) (*logical.Response, error) {
respManualChain = append(respManualChain, string(entity))
}

data := map[string]interface{}{
"issuer_id": issuer.ID,
"issuer_name": issuer.Name,
"key_id": issuer.KeyID,
"certificate": issuer.Certificate,
"manual_chain": respManualChain,
"ca_chain": issuer.CAChain,
"leaf_not_after_behavior": issuer.LeafNotAfterBehavior.String(),
"usage": issuer.Usage.Names(),
"issuing_certificates": []string{},
"crl_distribution_points": []string{},
"ocsp_servers": []string{},
}

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{
Data: map[string]interface{}{
"issuer_id": issuer.ID,
"issuer_name": issuer.Name,
"key_id": issuer.KeyID,
"certificate": issuer.Certificate,
"manual_chain": respManualChain,
"ca_chain": issuer.CAChain,
"leaf_not_after_behavior": issuer.LeafNotAfterBehavior.String(),
"usage": issuer.Usage.Names(),
},
Data: data,
}, nil
}

Expand Down Expand Up @@ -258,6 +284,19 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
return logical.ErrorResponse(fmt.Sprintf("Unable to parse specified usages: %v - valid values are %v", rawUsage, AllIssuerUsages.Names())), nil
}

issuerCertificates := data.Get("issuing_certificates").([]string)
if badURL := validateURLs(issuerCertificates); badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in 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 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 OCSP servers: %s", badURL)), nil
}

modified := false

var oldName string
Expand All @@ -277,6 +316,55 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
modified = true
}

if issuer.AIAURIs == nil && (len(issuerCertificates) > 0 || len(crlDistributionPoints) > 0 || len(ocspServers) > 0) {
issuer.AIAURIs = &certutil.URLEntries{}
}
if issuer.AIAURIs != nil {
if len(issuerCertificates) != len(issuer.AIAURIs.IssuingCertificates) {
modified = true
issuer.AIAURIs.IssuingCertificates = issuerCertificates
} else {
for index, value := range issuer.AIAURIs.IssuingCertificates {
if len(issuerCertificates) < index+1 || issuerCertificates[index] != value {
modified = true
issuer.AIAURIs.IssuingCertificates = issuerCertificates
break
}
}
}
if len(crlDistributionPoints) != len(issuer.AIAURIs.CRLDistributionPoints) {
modified = true
issuer.AIAURIs.CRLDistributionPoints = crlDistributionPoints
} else {
for index, value := range issuer.AIAURIs.CRLDistributionPoints {
if len(crlDistributionPoints) < index+1 || crlDistributionPoints[index] != value {
modified = true
issuer.AIAURIs.CRLDistributionPoints = crlDistributionPoints
break
}
}
}
if len(ocspServers) != len(issuer.AIAURIs.OCSPServers) {
modified = true
issuer.AIAURIs.OCSPServers = ocspServers
} else {
for index, value := range issuer.AIAURIs.OCSPServers {
if len(ocspServers) < index+1 || ocspServers[index] != value {
modified = true
issuer.AIAURIs.OCSPServers = ocspServers
break
}
}
}

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 constructedChain []issuerID
for index, newPathRef := range newPath {
Expand Down Expand Up @@ -426,6 +514,74 @@ func (b *backend) pathPatchIssuer(ctx context.Context, req *logical.Request, dat
}
}

// AIA access changes.
if issuer.AIAURIs == nil {
issuer.AIAURIs = &certutil.URLEntries{}
}
rawIssuerCertificates, ok := data.GetOk("issuing_certificates")
if ok {
issuerCertificates := rawIssuerCertificates.([]string)
if badURL := validateURLs(issuerCertificates); badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in issuing certificates: %s", badURL)), nil
}

if len(issuerCertificates) != len(issuer.AIAURIs.IssuingCertificates) {
modified = true
issuer.AIAURIs.IssuingCertificates = issuerCertificates
} else {
for index, value := range issuer.AIAURIs.IssuingCertificates {
if len(issuerCertificates) < index+1 || issuerCertificates[index] != value {
modified = true
issuer.AIAURIs.IssuingCertificates = issuerCertificates
break
}
}
}
}
rawCrlDistributionPoints, ok := data.GetOk("crl_distribution_points")
if ok {
crlDistributionPoints := rawCrlDistributionPoints.([]string)
if badURL := validateURLs(crlDistributionPoints); badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in CRL distribution points: %s", badURL)), nil
}

if len(crlDistributionPoints) != len(issuer.AIAURIs.CRLDistributionPoints) {
modified = true
issuer.AIAURIs.CRLDistributionPoints = crlDistributionPoints
} else {
for index, value := range issuer.AIAURIs.CRLDistributionPoints {
if len(crlDistributionPoints) < index+1 || crlDistributionPoints[index] != value {
modified = true
issuer.AIAURIs.CRLDistributionPoints = crlDistributionPoints
break
}
}
}
}
rawOcspServers, ok := data.GetOk("ocsp_servers")
if ok {
ocspServers := rawOcspServers.([]string)
if badURL := validateURLs(ocspServers); badURL != "" {
return logical.ErrorResponse(fmt.Sprintf("invalid URL found in OCSP servers: %s", badURL)), nil
}

if len(ocspServers) != len(issuer.AIAURIs.OCSPServers) {
modified = true
issuer.AIAURIs.OCSPServers = ocspServers
} else {
for index, value := range issuer.AIAURIs.OCSPServers {
if len(ocspServers) < index+1 || ocspServers[index] != value {
modified = true
issuer.AIAURIs.OCSPServers = ocspServers
break
}
}
}
}
if len(issuer.AIAURIs.IssuingCertificates) == 0 && len(issuer.AIAURIs.CRLDistributionPoints) == 0 && len(issuer.AIAURIs.OCSPServers) == 0 {
issuer.AIAURIs = nil
}

// Manual Chain Changes
newPathData, ok := data.GetOk("manual_chain")
if ok {
Expand Down
2 changes: 1 addition & 1 deletion builtin/logical/pki/path_intermediate.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (b *backend) pathGenerateIntermediate(ctx context.Context, req *logical.Req
// If the operator hasn't configured any of the URLs prior to
// generating this issuer, we should add a warning to the response,
// 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 access information (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 {
Expand Down
2 changes: 1 addition & 1 deletion builtin/logical/pki/path_manage_issuers.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func (b *backend) pathImportIssuers(ctx context.Context, req *logical.Request, d
// In particular, if there's no default AIA URLs configuration, we should
// 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 {
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 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 or the newly generated issuer with this information.")
}

return response, nil
Expand Down
4 changes: 2 additions & 2 deletions builtin/logical/pki/path_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// generating this issuer, we should add a warning to the response,
// 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 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 or the newly generated issuer with this information.")
}

switch format {
Expand Down Expand Up @@ -388,7 +388,7 @@ func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.R
// If the operator hasn't configured any of the URLs prior to
// generating this issuer, we should add a warning to the response,
// 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 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 or the newly generated issuer with this information.")
}

caChain := append([]string{cb.Certificate}, cb.CAChain...)
Expand Down
14 changes: 14 additions & 0 deletions builtin/logical/pki/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ type issuerEntry struct {
SerialNumber string `json:"serial_number"`
LeafNotAfterBehavior certutil.NotAfterBehavior `json:"not_after_behavior"`
Usage issuerUsage `json:"usage"`
AIAURIs *certutil.URLEntries `json:"aia_uris,omitempty"`
}

type localCRLConfigEntry struct {
Expand Down Expand Up @@ -427,6 +428,19 @@ func (i issuerEntry) EnsureUsage(usage issuerUsage) error {
return fmt.Errorf("unknown delta between usages: %v -> %v / for issuer [%v]", usage.Names(), i.Usage.Names(), issuerRef)
}

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 = getURLs(sc.Context, sc.Storage)
}

return urls, err
}

func (sc *storageContext) listIssuers() ([]issuerID, error) {
strList, err := sc.Storage.List(sc.Context, issuerPrefix)
if err != nil {
Expand Down

0 comments on commit 7ab6961

Please sign in to comment.