forked from crowdsecurity/crowdsec
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
255 additions
and
152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.