From 1d06b27bc176d46323b4fe7d6cbc105ba0cd873c Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Mon, 4 Apr 2022 09:27:18 -0400 Subject: [PATCH] Refactor raw PKI fetch endpoints Various endpoints (/ca, /crl, /ca_chain, /certs) all use the same core handling logic with a complicated per-path detection logic of which to return. Refactor out the common response formatting code, but let each API handler be distinct to provide the right storage reference. This refactors just the raw path (/ca, /crl, /ca_chain, and /cert/*/raw{,/pem}). Signed-off-by: Alexander Scheel --- builtin/logical/pki/backend.go | 1 + builtin/logical/pki/path_fetch.go | 211 ++++++++++++++++++++++-------- 2 files changed, 154 insertions(+), 58 deletions(-) diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index c0a1a0916c8b..e264cb865318 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -105,6 +105,7 @@ func Backend(conf *logical.BackendConfig) *backend { pathRotateCRL(&b), pathFetchCA(&b), pathFetchCAChain(&b), + pathFetchCAChainRaw(&b), pathFetchCRL(&b), pathFetchCRLViaCertPath(&b), pathFetchValidRaw(&b), diff --git a/builtin/logical/pki/path_fetch.go b/builtin/logical/pki/path_fetch.go index 634964e9dcb6..cfea76434d41 100644 --- a/builtin/logical/pki/path_fetch.go +++ b/builtin/logical/pki/path_fetch.go @@ -17,7 +17,7 @@ func pathFetchCA(b *backend) *framework.Path { Pattern: `ca(/pem)?`, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathFetchRead, + logical.ReadOperation: b.pathFetchCARawHandler, }, HelpSynopsis: pathFetchHelpSyn, @@ -25,10 +25,22 @@ func pathFetchCA(b *backend) *framework.Path { } } +func (b *backend) pathFetchCARawHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var serial, pemType, contentType string + serial = "ca" + contentType = "application/pkix-cert" + if req.Path == "ca/pem" { + pemType = "CERTIFICATE" + contentType = "application/pem-certificate-chain" + } + + return b.pathFetchReadRaw(ctx, req, data, serial, pemType, contentType) +} + // Returns the CA chain func pathFetchCAChain(b *backend) *framework.Path { return &framework.Path{ - Pattern: `(cert/)?ca_chain`, + Pattern: `cert/ca_chain`, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.pathFetchRead, @@ -39,13 +51,34 @@ func pathFetchCAChain(b *backend) *framework.Path { } } +func pathFetchCAChainRaw(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `ca_chain`, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathFetchCAChainRawHandler, + }, + + HelpSynopsis: pathFetchHelpSyn, + HelpDescription: pathFetchHelpDesc, + } +} + +func (b *backend) pathFetchCAChainRawHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var serial, pemType, contentType string + serial = "ca_chain" + contentType = "application/pkix-cert" + + return b.pathFetchReadRaw(ctx, req, data, serial, pemType, contentType) +} + // Returns the CRL in raw format func pathFetchCRL(b *backend) *framework.Path { return &framework.Path{ Pattern: `crl(/pem)?`, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathFetchRead, + logical.ReadOperation: b.pathFetchCRLRawHandler, }, HelpSynopsis: pathFetchHelpSyn, @@ -53,6 +86,18 @@ func pathFetchCRL(b *backend) *framework.Path { } } +func (b *backend) pathFetchCRLRawHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var serial, pemType, contentType string + serial = "crl" + contentType = "application/pkix-crl" + if req.Path == "crl/pem" { + pemType = "X509 CRL" + contentType = "application/x-pem-file" + } + + return b.pathFetchReadRaw(ctx, req, data, serial, pemType, contentType) +} + // Returns any valid (non-revoked) cert in raw format. func pathFetchValidRaw(b *backend) *framework.Path { return &framework.Path{ @@ -66,7 +111,7 @@ hyphen-separated octal`, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathFetchRead, + logical.ReadOperation: b.pathFetchCertificateRawHandler, }, HelpSynopsis: pathFetchHelpSyn, @@ -74,6 +119,19 @@ hyphen-separated octal`, } } +func (b *backend) pathFetchCertificateRawHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var serial, pemType, contentType string + + serial = data.Get("serial").(string) + contentType = "application/pkix-cert" + if strings.HasSuffix(req.Path, "/pem") { + pemType = "CERTIFICATE" + contentType = "application/pem-certificate-chain" + } + + return b.pathFetchReadRaw(ctx, req, data, serial, pemType, contentType) +} + // 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 { @@ -135,8 +193,19 @@ func (b *backend) pathFetchCertList(ctx context.Context, req *logical.Request, d return logical.ListResponse(entries), nil } +func marshalPem(pemType string, certificate []byte) []byte { + block := pem.Block{ + Type: pemType, + Bytes: certificate, + } + + // This is convoluted on purpose to ensure that we don't have trailing + // newlines via various paths + return []byte(strings.TrimSpace(string(pem.EncodeToMemory(&block)))) +} + func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { - var serial, pemType, contentType string + var serial, pemType string var certEntry, revokedEntry *logical.StorageEntry var funcErr error var certificate []byte @@ -146,41 +215,15 @@ func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data Data: map[string]interface{}{}, } - // 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. switch { - case req.Path == "ca" || req.Path == "ca/pem": - serial = "ca" - contentType = "application/pkix-cert" - if req.Path == "ca/pem" { - pemType = "CERTIFICATE" - contentType = "application/pem-certificate-chain" - } - case req.Path == "ca_chain" || req.Path == "cert/ca_chain": + case 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": - serial = "crl" - contentType = "application/pkix-crl" - if req.Path == "crl/pem" { - pemType = "X509 CRL" - contentType = "application/x-pem-file" - } case req.Path == "cert/crl": serial = "crl" pemType = "X509 CRL" - 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" @@ -245,15 +288,8 @@ func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data } 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)))) + certificate = marshalPem(pemType, certificate) } revokedEntry, funcErr = fetchCertBySerial(ctx, req, "revoked/", serial) @@ -278,24 +314,6 @@ func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data 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 @@ -315,6 +333,83 @@ reply: return } +func (b *backend) readCertByAlias(ctx context.Context, req *logical.Request, alias string) ([]byte, error) { + // ca_chain as an alias needs special handling. + if alias == "ca_chain" { + caInfo, err := fetchCAInfo(ctx, b, req) + if err != nil { + return nil, err + } + + caChain := caInfo.GetCAChain() + var certStr string + for _, ca := range caChain { + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: ca.Bytes, + } + certStr = strings.Join([]string{certStr, strings.TrimSpace(string(pem.EncodeToMemory(&block)))}, "\n") + } + + return []byte(strings.TrimSpace(certStr)), nil + } + + certEntry, err := fetchCertBySerial(ctx, req, req.Path, alias) + if err != nil { + return nil, err + } + if certEntry == nil { + return nil, nil + } + + return certEntry.Value, nil +} + +func (b *backend) pathFetchReadRaw(ctx context.Context, req *logical.Request, data *framework.FieldData, serial string, pemType string, contentType string) (response *logical.Response, retErr error) { + var certificate []byte + + // Errors don't cause an immediate exit, because the raw + // paths still need to return raw output, even if it is empty. + // The error will instead be logged (in the event the log surfaces + // warnings). + certificate, retErr = b.readCertByAlias(ctx, req, serial) + if retErr != nil { + goto reply + } + + if len(pemType) != 0 { + certificate = marshalPem(pemType, certificate) + } + +reply: + response = &logical.Response{ + Data: map[string]interface{}{ + logical.HTTPContentType: contentType, + logical.HTTPRawBody: certificate, + }, + } + + if len(certificate) > 0 { + response.Data[logical.HTTPStatusCode] = 200 + } else { + response.Data[logical.HTTPStatusCode] = 204 + } + + 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; call /crl/rotate to create a non-empty CRL", "error", retErr) + + // Return a 500 response to indicate something went wrong with + // this request. + response.Data[logical.HTTPStatusCode] = 500 + } + } + + retErr = nil + + return +} + const pathFetchHelpSyn = ` Fetch a CA, CRL, CA Chain, or non-revoked certificate. `