// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package pki import ( "context" "encoding/pem" "fmt" "net/http" "strings" "time" "github.com/hashicorp/vault/helper/constants" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/logical" ) var pathFetchReadSchema = map[int][]framework.Response{ http.StatusOK: {{ Description: "OK", Fields: map[string]*framework.FieldSchema{ "certificate": { Type: framework.TypeString, Description: `Certificate`, Required: false, }, "revocation_time": { Type: framework.TypeString, Description: `Revocation time`, Required: false, }, "revocation_time_rfc3339": { Type: framework.TypeString, Description: `Revocation time RFC 3339 formatted`, Required: false, }, "issuer_id": { Type: framework.TypeString, Description: `ID of the issuer`, Required: false, }, "ca_chain": { Type: framework.TypeStringSlice, Description: `Issuing CA Chain`, Required: false, }, }, }}, } // Returns the CA in raw format func pathFetchCA(b *backend) *framework.Path { return &framework.Path{ Pattern: `ca(/pem)?`, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathFetchRead, Responses: pathFetchReadSchema, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } // Returns the CA chain func pathFetchCAChain(b *backend) *framework.Path { return &framework.Path{ Pattern: `(cert/)?ca_chain`, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathFetchRead, Responses: pathFetchReadSchema, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } // Returns the CRL in raw format func pathFetchCRL(b *backend) *framework.Path { return &framework.Path{ Pattern: `crl(/pem|/delta(/pem)?)?`, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathFetchRead, Responses: pathFetchReadSchema, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } // Returns the CRL in raw format func pathFetchUnifiedCRL(b *backend) *framework.Path { return &framework.Path{ Pattern: `unified-crl(/pem|/delta(/pem)?)?`, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathFetchRead, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } // Returns any valid (non-revoked) cert in raw format. func pathFetchValidRaw(b *backend) *framework.Path { return &framework.Path{ Pattern: `cert/(?P[0-9A-Fa-f-:]+)/raw(/pem)?`, Fields: map[string]*framework.FieldSchema{ "serial": { Type: framework.TypeString, Description: `Certificate serial number, in colon- or hyphen-separated octal`, }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathFetchRead, Responses: pathFetchReadSchema, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } // Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path // also handles returning the CA cert in a non-raw format. func pathFetchValid(b *backend) *framework.Path { return &framework.Path{ Pattern: `cert/(?P[0-9A-Fa-f-:]+)`, Fields: map[string]*framework.FieldSchema{ "serial": { Type: framework.TypeString, Description: `Certificate serial number, in colon- or hyphen-separated octal`, }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathFetchRead, Responses: pathFetchReadSchema, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } // This returns the CRL in a non-raw format func pathFetchCRLViaCertPath(b *backend) *framework.Path { pattern := `cert/(crl|delta-crl)` if constants.IsEnterprise { pattern = `cert/(crl|delta-crl|unified-crl|unified-delta-crl)` } return &framework.Path{ Pattern: pattern, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathFetchRead, Responses: pathFetchReadSchema, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } // This returns the list of serial numbers for certs func pathFetchListCerts(b *backend) *framework.Path { return &framework.Path{ Pattern: "certs/?$", Operations: map[logical.Operation]framework.OperationHandler{ logical.ListOperation: &framework.PathOperation{ Callback: b.pathFetchCertList, Responses: map[int][]framework.Response{ http.StatusOK: {{ Description: "OK", Fields: map[string]*framework.FieldSchema{ "keys": { Type: framework.TypeStringSlice, Description: `A list of keys`, Required: true, }, }, }}, }, }, }, HelpSynopsis: pathFetchHelpSyn, HelpDescription: pathFetchHelpDesc, } } func (b *backend) pathFetchCertList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (response *logical.Response, retErr error) { entries, err := req.Storage.List(ctx, "certs/") if err != nil { return nil, err } for i := range entries { entries[i] = denormalizeSerial(entries[i]) } return logical.ListResponse(entries), nil } func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { var serial, pemType, contentType string var certEntry, revokedEntry *logical.StorageEntry var funcErr error var certificate []byte var fullChain []byte var revocationTime int64 var revocationIssuerId string var revocationTimeRfc3339 string response = &logical.Response{ Data: map[string]interface{}{}, } sc := b.makeStorageContext(ctx, req.Storage) // Some of these need to return raw and some non-raw; // this is basically handled by setting contentType or not. // Errors don't cause an immediate exit, because the raw // paths still need to return raw output. modifiedCtx := &IfModifiedSinceHelper{ req: req, issuerRef: defaultRef, } switch { case req.Path == "ca" || req.Path == "ca/pem" || req.Path == "cert/ca" || req.Path == "cert/ca/raw" || req.Path == "cert/ca/raw/pem": modifiedCtx.reqType = ifModifiedCA ret, err := sendNotModifiedResponseIfNecessary(modifiedCtx, sc, response) if err != nil || ret { retErr = err goto reply } serial = "ca" contentType = "application/pkix-cert" if req.Path == "ca/pem" || req.Path == "cert/ca/raw/pem" { pemType = "CERTIFICATE" contentType = "application/pem-certificate-chain" } else if req.Path == "cert/ca" { pemType = "CERTIFICATE" contentType = "" } case req.Path == "ca_chain" || req.Path == "cert/ca_chain": serial = "ca_chain" if req.Path == "ca_chain" { contentType = "application/pkix-cert" } case req.Path == "crl" || req.Path == "crl/pem" || req.Path == "crl/delta" || req.Path == "crl/delta/pem" || req.Path == "cert/crl" || req.Path == "cert/crl/raw" || req.Path == "cert/crl/raw/pem" || req.Path == "cert/delta-crl" || req.Path == "cert/delta-crl/raw" || req.Path == "cert/delta-crl/raw/pem" || req.Path == "unified-crl" || req.Path == "unified-crl/pem" || req.Path == "unified-crl/delta" || req.Path == "unified-crl/delta/pem" || req.Path == "cert/unified-crl" || req.Path == "cert/unified-crl/raw" || req.Path == "cert/unified-crl/raw/pem" || req.Path == "cert/unified-delta-crl" || req.Path == "cert/unified-delta-crl/raw" || req.Path == "cert/unified-delta-crl/raw/pem": config, err := b.crlBuilder.getConfigWithUpdate(sc) if err != nil { retErr = err goto reply } var isDelta bool var isUnified bool if strings.Contains(req.Path, "delta") { isDelta = true } if strings.Contains(req.Path, "unified") || shouldLocalPathsUseUnified(config) { isUnified = true } modifiedCtx.reqType = ifModifiedCRL if !isUnified && isDelta { modifiedCtx.reqType = ifModifiedDeltaCRL } else if isUnified && !isDelta { modifiedCtx.reqType = ifModifiedUnifiedCRL } else if isUnified && isDelta { modifiedCtx.reqType = ifModifiedUnifiedDeltaCRL } ret, err := sendNotModifiedResponseIfNecessary(modifiedCtx, sc, response) if err != nil || ret { retErr = err goto reply } serial = legacyCRLPath if !isUnified && isDelta { serial = deltaCRLPath } else if isUnified && !isDelta { serial = unifiedCRLPath } else if isUnified && isDelta { serial = unifiedDeltaCRLPath } contentType = "application/pkix-crl" if strings.Contains(req.Path, "pem") { pemType = "X509 CRL" contentType = "application/x-pem-file" } else if req.Path == "cert/crl" || req.Path == "cert/delta-crl" || req.Path == "cert/unified-crl" || req.Path == "cert/unified-delta-crl" { pemType = "X509 CRL" contentType = "" } case strings.HasSuffix(req.Path, "/pem") || strings.HasSuffix(req.Path, "/raw"): serial = data.Get("serial").(string) contentType = "application/pkix-cert" if strings.HasSuffix(req.Path, "/pem") { pemType = "CERTIFICATE" contentType = "application/pem-certificate-chain" } default: serial = data.Get("serial").(string) pemType = "CERTIFICATE" } if len(serial) == 0 { response = logical.ErrorResponse("The serial number must be provided") goto reply } // Prefer fetchCAInfo to fetchCertBySerial for CA certificates. if serial == "ca_chain" || serial == "ca" { caInfo, err := sc.fetchCAInfo(defaultRef, ReadOnlyUsage) if err != nil { switch err.(type) { case errutil.UserError: response = logical.ErrorResponse(err.Error()) goto reply default: retErr = err goto reply } } if serial == "ca_chain" { rawChain := caInfo.GetFullChain() var chainStr string for _, ca := range rawChain { block := pem.Block{ Type: "CERTIFICATE", Bytes: ca.Bytes, } chainStr = strings.Join([]string{chainStr, strings.TrimSpace(string(pem.EncodeToMemory(&block)))}, "\n") } fullChain = []byte(strings.TrimSpace(chainStr)) certificate = fullChain } else if serial == "ca" { certificate = caInfo.Certificate.Raw if len(pemType) != 0 { block := pem.Block{ Type: pemType, Bytes: certificate, } // This is convoluted on purpose to ensure that we don't have trailing // newlines via various paths certificate = []byte(strings.TrimSpace(string(pem.EncodeToMemory(&block)))) } } goto reply } certEntry, funcErr = fetchCertBySerial(sc, req.Path, serial) if funcErr != nil { switch funcErr.(type) { case errutil.UserError: response = logical.ErrorResponse(funcErr.Error()) goto reply default: retErr = funcErr goto reply } } if certEntry == nil { response = nil goto reply } certificate = certEntry.Value if len(pemType) != 0 { block := pem.Block{ Type: pemType, Bytes: certEntry.Value, } // This is convoluted on purpose to ensure that we don't have trailing // newlines via various paths certificate = []byte(strings.TrimSpace(string(pem.EncodeToMemory(&block)))) } revokedEntry, funcErr = fetchCertBySerial(sc, "revoked/", serial) if funcErr != nil { switch funcErr.(type) { case errutil.UserError: response = logical.ErrorResponse(funcErr.Error()) goto reply default: retErr = funcErr goto reply } } if revokedEntry != nil { var revInfo revocationInfo err := revokedEntry.DecodeJSON(&revInfo) if err != nil { return logical.ErrorResponse(fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)), nil } revocationTime = revInfo.RevocationTime revocationIssuerId = revInfo.CertificateIssuer.String() if !revInfo.RevocationTimeUTC.IsZero() { revocationTimeRfc3339 = revInfo.RevocationTimeUTC.Format(time.RFC3339Nano) } } reply: switch { case len(contentType) != 0: response = &logical.Response{ Data: map[string]interface{}{ logical.HTTPContentType: contentType, logical.HTTPRawBody: certificate, }, } if retErr != nil { if b.Logger().IsWarn() { b.Logger().Warn("possible error, but cannot return in raw response. Note that an empty CA probably means none was configured, and an empty CRL is possibly correct", "error", retErr) } } retErr = nil if len(certificate) > 0 { response.Data[logical.HTTPStatusCode] = 200 } else { response.Data[logical.HTTPStatusCode] = 204 } case retErr != nil: response = nil return case response == nil: return case response.IsError(): return response, nil default: response.Data["certificate"] = string(certificate) response.Data["revocation_time"] = revocationTime response.Data["revocation_time_rfc3339"] = revocationTimeRfc3339 // Only output this field if we have a value for it as it doesn't make sense for a // bunch of code paths that go through here if revocationIssuerId != "" { response.Data["issuer_id"] = revocationIssuerId } if len(fullChain) > 0 { response.Data["ca_chain"] = string(fullChain) } } return } const pathFetchHelpSyn = ` Fetch a CA, CRL, CA Chain, or non-revoked certificate. ` const pathFetchHelpDesc = ` This allows certificates to be fetched. Use /cert/:serial for JSON responses. Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding. Using "ca_chain" as the value fetches the certificate authority trust chain in PEM encoding. Otherwise, specify a serial number to fetch the specified certificate. Add "/raw" to get just the certificate in DER form, "/raw/pem" to get the PEM encoded certificate. `