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 }