mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-21 02:17:36 +00:00
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:
@@ -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
|
||||
// Let’s 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
|
||||
}
|
||||
|
Reference in New Issue
Block a user