Skip to content

Commit

Permalink
extract CRLChecker, OCSPChecker
Browse files Browse the repository at this point in the history
  • Loading branch information
mmetc committed May 28, 2024
1 parent 209cceb commit c09a72d
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 152 deletions.
134 changes: 134 additions & 0 deletions pkg/apiserver/middlewares/v1/crl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package v1

import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"sync"
"time"

log "github.com/sirupsen/logrus"
)

type CRLChecker struct {
path string
fileInfo os.FileInfo
crls []*x509.RevocationList
logger *log.Entry
mu sync.RWMutex
lastChecked time.Time
}

func NewCRLChecker(crlPath string, logger *log.Entry) (*CRLChecker, error) {
cc := &CRLChecker{
path: crlPath,
logger: logger,
}

err := cc.refresh()
if err != nil {
return nil, err
}

return cc, nil
}

func (*CRLChecker) decodeCRLs(content []byte, logger *log.Entry) []*x509.RevocationList {
var crls []*x509.RevocationList

for {
block, rest := pem.Decode(content)
if block == nil {
break // no more PEM blocks
}

content = rest

crl, err := x509.ParseRevocationList(block.Bytes)
if err != nil {
logger.Errorf("could not parse a PEM block in CRL file, skipping: %s", err)
continue
}

crls = append(crls, crl)
}

return crls
}

// refresh() reads the CRL file if new or changed since the last time
func (cc *CRLChecker) refresh() error {
// noop if lastChecked is less than 5 seconds ago
if time.Since(cc.lastChecked) < 5*time.Second {
return nil
}

cc.mu.Lock()
defer cc.mu.Unlock()

cc.logger.Debugf("loading CRL file from %s", cc.path)

fileInfo, err := os.Stat(cc.path)
if err != nil {
return fmt.Errorf("could not access CRL file: %w", err)
}

// noop if the file didn't change
if cc.fileInfo != nil && fileInfo.ModTime().Equal(cc.fileInfo.ModTime()) && fileInfo.Size() == cc.fileInfo.Size() {
return nil
}

// the encoding/pem package wants bytes, not io.Reader
crlContent, err := os.ReadFile(cc.path)
if err != nil {
return fmt.Errorf("could not read CRL file: %w", err)
}

cc.crls = cc.decodeCRLs(crlContent, cc.logger)
cc.fileInfo = fileInfo
cc.lastChecked = time.Now()

return nil
}

// isRevoked checks if the client certificate is revoked by any of the CRL blocks
// It returns a boolean indicating if the certificate is revoked and a boolean indicating
// if the CRL check was successful and could be cached.
func (cc *CRLChecker) isRevoked(cert *x509.Certificate) (bool, bool) {
if cc == nil {
return false, true
}

err := cc.refresh()
if err != nil {
// we can't quit obviously, so we just log the error and continue
// but we can assume we have loaded a CRL, or it would have quit the first time
cc.logger.Errorf("while refreshing CRL: %s - will keep using CRL file read at %s", err,
cc.lastChecked.Format(time.RFC3339))
}

now := time.Now().UTC()

cc.mu.RLock()
defer cc.mu.RUnlock()

for _, crl := range cc.crls {
if now.After(crl.NextUpdate) {
cc.logger.Warn("CRL has expired, will still validate the cert against it.")
}

if now.Before(crl.ThisUpdate) {
cc.logger.Warn("CRL is not yet valid, will still validate the cert against it.")
}

for _, revoked := range crl.RevokedCertificateEntries {
if revoked.SerialNumber.Cmp(cert.SerialNumber) == 0 {
cc.logger.Warn("client certificate is revoked by CRL")
return true, true
}
}
}

return false, true
}
100 changes: 100 additions & 0 deletions pkg/apiserver/middlewares/v1/ocsp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package v1

import (
"bytes"
"crypto"
"crypto/x509"
"io"
"net/http"
"net/url"

log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ocsp"
)

type OCSPChecker struct {
logger *log.Entry
}

func NewOCSPChecker(logger *log.Entry) *OCSPChecker {
return &OCSPChecker{
logger: logger,
}
}

func (oc *OCSPChecker) query(server string, cert *x509.Certificate, issuer *x509.Certificate) (*ocsp.Response, error) {
req, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA256})
if err != nil {
oc.logger.Errorf("TLSAuth: error creating OCSP request: %s", err)
return nil, err
}

httpRequest, err := http.NewRequest(http.MethodPost, server, bytes.NewBuffer(req))
if err != nil {
oc.logger.Error("TLSAuth: cannot create HTTP request for OCSP")
return nil, err
}

ocspURL, err := url.Parse(server)
if err != nil {
oc.logger.Error("TLSAuth: cannot parse OCSP URL")
return nil, err
}

httpRequest.Header.Add("Content-Type", "application/ocsp-request")
httpRequest.Header.Add("Accept", "application/ocsp-response")
httpRequest.Header.Add("host", ocspURL.Host)

httpClient := &http.Client{}

// XXX: timeout, context?
httpResponse, err := httpClient.Do(httpRequest)
if err != nil {
oc.logger.Error("TLSAuth: cannot send HTTP request to OCSP")
return nil, err
}
defer httpResponse.Body.Close()

output, err := io.ReadAll(httpResponse.Body)
if err != nil {
oc.logger.Error("TLSAuth: cannot read HTTP response from OCSP")
return nil, err
}

ocspResponse, err := ocsp.ParseResponseForCert(output, cert, issuer)

return ocspResponse, err
}

// isRevoked checks if the client certificate is revoked by any of the OCSP servers present in the certificate.
// It returns a boolean indicating if the certificate is revoked and a boolean indicating
// if the OCSP check was successful and could be cached.
func (oc *OCSPChecker) isRevoked(cert *x509.Certificate, issuer *x509.Certificate) (bool, bool) {
if cert.OCSPServer == nil || len(cert.OCSPServer) == 0 {
oc.logger.Infof("TLSAuth: no OCSP Server present in client certificate, skipping OCSP verification")
return false, true
}

for _, server := range cert.OCSPServer {
ocspResponse, err := oc.query(server, cert, issuer)
if err != nil {
oc.logger.Errorf("TLSAuth: error querying OCSP server %s: %s", server, err)
continue
}

switch ocspResponse.Status {
case ocsp.Good:
return false, true
case ocsp.Revoked:
oc.logger.Errorf("TLSAuth: client certificate is revoked by server %s", server)
return true, true
case ocsp.Unknown:
log.Debugf("unknown OCSP status for server %s", server)
continue
}
}

log.Infof("Could not get any valid OCSP response, assuming the cert is revoked")

return true, false
}
Loading

0 comments on commit c09a72d

Please sign in to comment.