prober: update cert check for prober (#15919)

OCSP has been removed from the LE certs.
Use CRL verification instead.

If a cert provides a CRL, check its revocation
status, if no CRL is provided and otherwise
is valid, pass the check.

Fixes #15912

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
Co-authored-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
Mike O'Driscoll
2025-05-12 10:25:31 -04:00
committed by GitHub
parent b70c0c50fd
commit b02de31563
2 changed files with 138 additions and 83 deletions

View File

@@ -4,7 +4,6 @@
package prober
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
@@ -15,12 +14,14 @@ import (
"net/netip"
"time"
"github.com/pkg/errors"
"golang.org/x/crypto/ocsp"
"tailscale.com/util/multierr"
)
const expiresSoon = 7 * 24 * time.Hour // 7 days from now
// Lets Encrypt promises to issue certificates with CRL servers after 2025-05-07:
// https://letsencrypt.org/2024/12/05/ending-ocsp/
// https://github.com/tailscale/tailscale/issues/15912
const letsEncryptStartedStaplingCRL int64 = 1746576000 // 2025-05-07 00:00:00 UTC
// TLS returns a Probe that healthchecks a TLS endpoint.
//
@@ -106,50 +107,55 @@ func validateConnState(ctx context.Context, cs *tls.ConnectionState) (returnerr
}
}
if len(leafCert.OCSPServer) == 0 {
errs = append(errs, fmt.Errorf("no OCSP server presented in leaf cert for %v", leafCert.Subject))
if len(leafCert.CRLDistributionPoints) == 0 {
if leafCert.NotBefore.Before(time.Unix(letsEncryptStartedStaplingCRL, 0)) {
// Certificate might not have a CRL.
return
}
errs = append(errs, fmt.Errorf("no CRL server presented in leaf cert for %v", leafCert.Subject))
return
}
ocspResp, err := getOCSPResponse(ctx, leafCert.OCSPServer[0], leafCert, issuerCert)
err := checkCertCRL(ctx, leafCert.CRLDistributionPoints[0], leafCert, issuerCert)
if err != nil {
errs = append(errs, errors.Wrapf(err, "OCSP verification failed for %v", leafCert.Subject))
return
}
if ocspResp.Status == ocsp.Unknown {
errs = append(errs, fmt.Errorf("unknown OCSP verification status for %v", leafCert.Subject))
}
if ocspResp.Status == ocsp.Revoked {
errs = append(errs, fmt.Errorf("cert for %v has been revoked on %v, reason: %v", leafCert.Subject, ocspResp.RevokedAt, ocspResp.RevocationReason))
errs = append(errs, fmt.Errorf("CRL verification failed for %v: %w", leafCert.Subject, err))
}
return
}
func getOCSPResponse(ctx context.Context, ocspServer string, leafCert, issuerCert *x509.Certificate) (*ocsp.Response, error) {
reqb, err := ocsp.CreateRequest(leafCert, issuerCert, nil)
func checkCertCRL(ctx context.Context, crlURL string, leafCert, issuerCert *x509.Certificate) error {
hreq, err := http.NewRequestWithContext(ctx, "GET", crlURL, nil)
if err != nil {
return nil, errors.Wrap(err, "could not create OCSP request")
return fmt.Errorf("could not create CRL GET request: %w", err)
}
hreq, err := http.NewRequestWithContext(ctx, "POST", ocspServer, bytes.NewReader(reqb))
if err != nil {
return nil, errors.Wrap(err, "could not create OCSP POST request")
}
hreq.Header.Add("Content-Type", "application/ocsp-request")
hreq.Header.Add("Accept", "application/ocsp-response")
hresp, err := http.DefaultClient.Do(hreq)
if err != nil {
return nil, errors.Wrap(err, "OCSP request failed")
return fmt.Errorf("CRL request failed: %w", err)
}
defer hresp.Body.Close()
if hresp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ocsp: non-200 status code from OCSP server: %s", hresp.Status)
return fmt.Errorf("crl: non-200 status code from CRL server: %s", hresp.Status)
}
lr := io.LimitReader(hresp.Body, 10<<20) // 10MB
ocspB, err := io.ReadAll(lr)
crlB, err := io.ReadAll(lr)
if err != nil {
return nil, err
return err
}
return ocsp.ParseResponse(ocspB, issuerCert)
crl, err := x509.ParseRevocationList(crlB)
if err != nil {
return fmt.Errorf("could not parse CRL: %w", err)
}
if err := crl.CheckSignatureFrom(issuerCert); err != nil {
return fmt.Errorf("could not verify CRL signature: %w", err)
}
for _, revoked := range crl.RevokedCertificateEntries {
if revoked.SerialNumber.Cmp(leafCert.SerialNumber) == 0 {
return fmt.Errorf("cert for %v has been revoked on %v, reason: %v", leafCert.Subject, revoked.RevocationTime, revoked.ReasonCode)
}
}
return nil
}