github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiserver/middlewares/v1/tls_auth.go (about)

     1  package v1
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"crypto/x509"
     7  	"encoding/pem"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"time"
    14  
    15  	"github.com/gin-gonic/gin"
    16  	log "github.com/sirupsen/logrus"
    17  	"golang.org/x/crypto/ocsp"
    18  )
    19  
    20  type TLSAuth struct {
    21  	AllowedOUs      []string
    22  	CrlPath         string
    23  	revocationCache map[string]cacheEntry
    24  	cacheExpiration time.Duration
    25  	logger          *log.Entry
    26  }
    27  
    28  type cacheEntry struct {
    29  	revoked   bool
    30  	timestamp time.Time
    31  }
    32  
    33  func (ta *TLSAuth) ocspQuery(server string, cert *x509.Certificate, issuer *x509.Certificate) (*ocsp.Response, error) {
    34  	req, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA256})
    35  	if err != nil {
    36  		ta.logger.Errorf("TLSAuth: error creating OCSP request: %s", err)
    37  		return nil, err
    38  	}
    39  
    40  	httpRequest, err := http.NewRequest(http.MethodPost, server, bytes.NewBuffer(req))
    41  	if err != nil {
    42  		ta.logger.Error("TLSAuth: cannot create HTTP request for OCSP")
    43  		return nil, err
    44  	}
    45  
    46  	ocspURL, err := url.Parse(server)
    47  	if err != nil {
    48  		ta.logger.Error("TLSAuth: cannot parse OCSP URL")
    49  		return nil, err
    50  	}
    51  
    52  	httpRequest.Header.Add("Content-Type", "application/ocsp-request")
    53  	httpRequest.Header.Add("Accept", "application/ocsp-response")
    54  	httpRequest.Header.Add("host", ocspURL.Host)
    55  
    56  	httpClient := &http.Client{}
    57  
    58  	httpResponse, err := httpClient.Do(httpRequest)
    59  	if err != nil {
    60  		ta.logger.Error("TLSAuth: cannot send HTTP request to OCSP")
    61  		return nil, err
    62  	}
    63  	defer httpResponse.Body.Close()
    64  
    65  	output, err := io.ReadAll(httpResponse.Body)
    66  	if err != nil {
    67  		ta.logger.Error("TLSAuth: cannot read HTTP response from OCSP")
    68  		return nil, err
    69  	}
    70  
    71  	ocspResponse, err := ocsp.ParseResponseForCert(output, cert, issuer)
    72  
    73  	return ocspResponse, err
    74  }
    75  
    76  func (ta *TLSAuth) isExpired(cert *x509.Certificate) bool {
    77  	now := time.Now().UTC()
    78  
    79  	if cert.NotAfter.UTC().Before(now) {
    80  		ta.logger.Errorf("TLSAuth: client certificate is expired (NotAfter: %s)", cert.NotAfter.UTC())
    81  		return true
    82  	}
    83  
    84  	if cert.NotBefore.UTC().After(now) {
    85  		ta.logger.Errorf("TLSAuth: client certificate is not yet valid (NotBefore: %s)", cert.NotBefore.UTC())
    86  		return true
    87  	}
    88  
    89  	return false
    90  }
    91  
    92  // isOCSPRevoked checks if the client certificate is revoked by any of the OCSP servers present in the certificate.
    93  // It returns a boolean indicating if the certificate is revoked and a boolean indicating if the OCSP check was successful and could be cached.
    94  func (ta *TLSAuth) isOCSPRevoked(cert *x509.Certificate, issuer *x509.Certificate) (bool, bool) {
    95  	if cert.OCSPServer == nil || len(cert.OCSPServer) == 0 {
    96  		ta.logger.Infof("TLSAuth: no OCSP Server present in client certificate, skipping OCSP verification")
    97  		return false, true
    98  	}
    99  
   100  	for _, server := range cert.OCSPServer {
   101  		ocspResponse, err := ta.ocspQuery(server, cert, issuer)
   102  		if err != nil {
   103  			ta.logger.Errorf("TLSAuth: error querying OCSP server %s: %s", server, err)
   104  			continue
   105  		}
   106  
   107  		switch ocspResponse.Status {
   108  		case ocsp.Good:
   109  			return false, true
   110  		case ocsp.Revoked:
   111  			ta.logger.Errorf("TLSAuth: client certificate is revoked by server %s", server)
   112  			return true, true
   113  		case ocsp.Unknown:
   114  			log.Debugf("unknow OCSP status for server %s", server)
   115  			continue
   116  		}
   117  	}
   118  
   119  	log.Infof("Could not get any valid OCSP response, assuming the cert is revoked")
   120  
   121  	return true, false
   122  }
   123  
   124  // isCRLRevoked checks if the client certificate is revoked by the CRL present in the CrlPath.
   125  // It returns a boolean indicating if the certificate is revoked and a boolean indicating if the CRL check was successful and could be cached.
   126  func (ta *TLSAuth) isCRLRevoked(cert *x509.Certificate) (bool, bool) {
   127  	if ta.CrlPath == "" {
   128  		ta.logger.Info("no crl_path, skipping CRL check")
   129  		return false, true
   130  	}
   131  
   132  	crlContent, err := os.ReadFile(ta.CrlPath)
   133  	if err != nil {
   134  		ta.logger.Errorf("could not read CRL file, skipping check: %s", err)
   135  		return false, false
   136  	}
   137  
   138  	crlBinary, rest := pem.Decode(crlContent)
   139  	if len(rest) > 0 {
   140  		ta.logger.Warn("CRL file contains more than one PEM block, ignoring the rest")
   141  	}
   142  
   143  	crl, err := x509.ParseRevocationList(crlBinary.Bytes)
   144  	if err != nil {
   145  		ta.logger.Errorf("could not parse CRL file, skipping check: %s", err)
   146  		return false, false
   147  	}
   148  
   149  	now := time.Now().UTC()
   150  
   151  	if now.After(crl.NextUpdate) {
   152  		ta.logger.Warn("CRL has expired, will still validate the cert against it.")
   153  	}
   154  
   155  	if now.Before(crl.ThisUpdate) {
   156  		ta.logger.Warn("CRL is not yet valid, will still validate the cert against it.")
   157  	}
   158  
   159  	for _, revoked := range crl.RevokedCertificateEntries {
   160  		if revoked.SerialNumber.Cmp(cert.SerialNumber) == 0 {
   161  			ta.logger.Warn("client certificate is revoked by CRL")
   162  			return true, true
   163  		}
   164  	}
   165  
   166  	return false, true
   167  }
   168  
   169  func (ta *TLSAuth) isRevoked(cert *x509.Certificate, issuer *x509.Certificate) (bool, error) {
   170  	sn := cert.SerialNumber.String()
   171  	if cacheValue, ok := ta.revocationCache[sn]; ok {
   172  		if time.Now().UTC().Sub(cacheValue.timestamp) < ta.cacheExpiration {
   173  			ta.logger.Debugf("TLSAuth: using cached value for cert %s: %t", sn, cacheValue.revoked)
   174  			return cacheValue.revoked, nil
   175  		}
   176  
   177  		ta.logger.Debugf("TLSAuth: cached value expired, removing from cache")
   178  		delete(ta.revocationCache, sn)
   179  	} else {
   180  		ta.logger.Tracef("TLSAuth: no cached value for cert %s", sn)
   181  	}
   182  
   183  	revokedByOCSP, cacheOCSP := ta.isOCSPRevoked(cert, issuer)
   184  
   185  	revokedByCRL, cacheCRL := ta.isCRLRevoked(cert)
   186  
   187  	revoked := revokedByOCSP || revokedByCRL
   188  
   189  	if cacheOCSP && cacheCRL {
   190  		ta.revocationCache[sn] = cacheEntry{
   191  			revoked:   revoked,
   192  			timestamp: time.Now().UTC(),
   193  		}
   194  	}
   195  
   196  	return revoked, nil
   197  }
   198  
   199  func (ta *TLSAuth) isInvalid(cert *x509.Certificate, issuer *x509.Certificate) (bool, error) {
   200  	if ta.isExpired(cert) {
   201  		return true, nil
   202  	}
   203  
   204  	revoked, err := ta.isRevoked(cert, issuer)
   205  	if err != nil {
   206  		//Fail securely, if we can't check the revocation status, let's consider the cert invalid
   207  		//We may change this in the future based on users feedback, but this seems the most sensible thing to do
   208  		return true, fmt.Errorf("could not check for client certification revocation status: %w", err)
   209  	}
   210  
   211  	return revoked, nil
   212  }
   213  
   214  func (ta *TLSAuth) SetAllowedOu(allowedOus []string) error {
   215  	for _, ou := range allowedOus {
   216  		//disallow empty ou
   217  		if ou == "" {
   218  			return fmt.Errorf("empty ou isn't allowed")
   219  		}
   220  
   221  		//drop & warn on duplicate ou
   222  		ok := true
   223  
   224  		for _, validOu := range ta.AllowedOUs {
   225  			if validOu == ou {
   226  				ta.logger.Warningf("dropping duplicate ou %s", ou)
   227  
   228  				ok = false
   229  			}
   230  		}
   231  
   232  		if ok {
   233  			ta.AllowedOUs = append(ta.AllowedOUs, ou)
   234  		}
   235  	}
   236  
   237  	return nil
   238  }
   239  
   240  func (ta *TLSAuth) ValidateCert(c *gin.Context) (bool, string, error) {
   241  	//Checks cert validity, Returns true + CN if client cert matches requested OU
   242  	var clientCert *x509.Certificate
   243  
   244  	if c.Request.TLS == nil || len(c.Request.TLS.PeerCertificates) == 0 {
   245  		//do not error if it's not TLS or there are no peer certs
   246  		return false, "", nil
   247  	}
   248  
   249  	if len(c.Request.TLS.VerifiedChains) > 0 {
   250  		validOU := false
   251  		clientCert = c.Request.TLS.VerifiedChains[0][0]
   252  
   253  		for _, ou := range clientCert.Subject.OrganizationalUnit {
   254  			for _, allowedOu := range ta.AllowedOUs {
   255  				if allowedOu == ou {
   256  					validOU = true
   257  					break
   258  				}
   259  			}
   260  		}
   261  
   262  		if !validOU {
   263  			return false, "", fmt.Errorf("client certificate OU (%v) doesn't match expected OU (%v)",
   264  				clientCert.Subject.OrganizationalUnit, ta.AllowedOUs)
   265  		}
   266  
   267  		revoked, err := ta.isInvalid(clientCert, c.Request.TLS.VerifiedChains[0][1])
   268  		if err != nil {
   269  			ta.logger.Errorf("TLSAuth: error checking if client certificate is revoked: %s", err)
   270  			return false, "", fmt.Errorf("could not check for client certification revocation status: %w", err)
   271  		}
   272  
   273  		if revoked {
   274  			return false, "", fmt.Errorf("client certificate for CN=%s OU=%s is revoked", clientCert.Subject.CommonName, clientCert.Subject.OrganizationalUnit)
   275  		}
   276  
   277  		ta.logger.Debugf("client OU %v is allowed vs required OU %v", clientCert.Subject.OrganizationalUnit, ta.AllowedOUs)
   278  
   279  		return true, clientCert.Subject.CommonName, nil
   280  	}
   281  
   282  	return false, "", fmt.Errorf("no verified cert in request")
   283  }
   284  
   285  func NewTLSAuth(allowedOus []string, crlPath string, cacheExpiration time.Duration, logger *log.Entry) (*TLSAuth, error) {
   286  	ta := &TLSAuth{
   287  		revocationCache: map[string]cacheEntry{},
   288  		cacheExpiration: cacheExpiration,
   289  		CrlPath:         crlPath,
   290  		logger:          logger,
   291  	}
   292  
   293  	err := ta.SetAllowedOu(allowedOus)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	return ta, nil
   299  }