364 lines
9.7 KiB
Go
364 lines
9.7 KiB
Go
|
// Package revoke provides functionality for checking the validity of
|
||
|
// a cert. Specifically, the temporal validity of the certificate is
|
||
|
// checked first, then any CRL and OCSP url in the cert is checked.
|
||
|
package revoke
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto"
|
||
|
"crypto/x509"
|
||
|
"encoding/base64"
|
||
|
"encoding/pem"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
neturl "net/url"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||
|
"git.wntrmute.dev/kyle/goutils/log"
|
||
|
"golang.org/x/crypto/ocsp"
|
||
|
)
|
||
|
|
||
|
// Originally from CFSSL, mostly written by me originally, and licensed under:
|
||
|
|
||
|
/*
|
||
|
Copyright (c) 2014 CloudFlare Inc.
|
||
|
|
||
|
Redistribution and use in source and binary forms, with or without
|
||
|
modification, are permitted provided that the following conditions
|
||
|
are met:
|
||
|
|
||
|
Redistributions of source code must retain the above copyright notice,
|
||
|
this list of conditions and the following disclaimer.
|
||
|
|
||
|
Redistributions in binary form must reproduce the above copyright notice,
|
||
|
this list of conditions and the following disclaimer in the documentation
|
||
|
and/or other materials provided with the distribution.
|
||
|
|
||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
*/
|
||
|
|
||
|
// I've modified it for use in my own code e.g. by removing the CFSSL errors
|
||
|
// and replacing them with sane ones.
|
||
|
|
||
|
// HTTPClient is an instance of http.Client that will be used for all HTTP requests.
|
||
|
var HTTPClient = http.DefaultClient
|
||
|
|
||
|
// HardFail determines whether the failure to check the revocation
|
||
|
// status of a certificate (i.e. due to network failure) causes
|
||
|
// verification to fail (a hard failure).
|
||
|
var HardFail = false
|
||
|
|
||
|
// CRLSet associates a PKIX certificate list with the URL the CRL is
|
||
|
// fetched from.
|
||
|
var CRLSet = map[string]*x509.RevocationList{}
|
||
|
var crlLock = new(sync.Mutex)
|
||
|
|
||
|
// We can't handle LDAP certificates, so this checks to see if the
|
||
|
// URL string points to an LDAP resource so that we can ignore it.
|
||
|
func ldapURL(url string) bool {
|
||
|
u, err := neturl.Parse(url)
|
||
|
if err != nil {
|
||
|
log.Warningf("error parsing url %s: %v", url, err)
|
||
|
return false
|
||
|
}
|
||
|
if u.Scheme == "ldap" {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// revCheck should check the certificate for any revocations. It
|
||
|
// returns a pair of booleans: the first indicates whether the certificate
|
||
|
// is revoked, the second indicates whether the revocations were
|
||
|
// successfully checked.. This leads to the following combinations:
|
||
|
//
|
||
|
// - false, false: an error was encountered while checking revocations.
|
||
|
// - false, true: the certificate was checked successfully, and it is not revoked.
|
||
|
// - true, true: the certificate was checked successfully, and it is revoked.
|
||
|
// - true, false: failure to check revocation status causes verification to fail
|
||
|
func revCheck(cert *x509.Certificate) (revoked, ok bool, err error) {
|
||
|
for _, url := range cert.CRLDistributionPoints {
|
||
|
if ldapURL(url) {
|
||
|
log.Infof("skipping LDAP CRL: %s", url)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if revoked, ok, err := certIsRevokedCRL(cert, url); !ok {
|
||
|
log.Warning("error checking revocation via CRL")
|
||
|
if HardFail {
|
||
|
return true, false, err
|
||
|
}
|
||
|
return false, false, err
|
||
|
} else if revoked {
|
||
|
log.Info("certificate is revoked via CRL")
|
||
|
return true, true, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if revoked, ok, err := certIsRevokedOCSP(cert, HardFail); !ok {
|
||
|
log.Warning("error checking revocation via OCSP")
|
||
|
if HardFail {
|
||
|
return true, false, err
|
||
|
}
|
||
|
return false, false, err
|
||
|
} else if revoked {
|
||
|
log.Info("certificate is revoked via OCSP")
|
||
|
return true, true, err
|
||
|
}
|
||
|
|
||
|
return false, true, nil
|
||
|
}
|
||
|
|
||
|
// fetchCRL fetches and parses a CRL.
|
||
|
func fetchCRL(url string) (*x509.RevocationList, error) {
|
||
|
resp, err := HTTPClient.Get(url)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode >= 300 {
|
||
|
return nil, errors.New("failed to retrieve CRL")
|
||
|
}
|
||
|
|
||
|
body, err := crlRead(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return x509.ParseRevocationList(body)
|
||
|
}
|
||
|
|
||
|
func getIssuer(cert *x509.Certificate) *x509.Certificate {
|
||
|
var issuer *x509.Certificate
|
||
|
var err error
|
||
|
for _, issuingCert := range cert.IssuingCertificateURL {
|
||
|
issuer, err = fetchRemote(issuingCert)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
return issuer
|
||
|
|
||
|
}
|
||
|
|
||
|
// check a cert against a specific CRL. Returns the same bool pair
|
||
|
// as revCheck, plus an error if one occurred.
|
||
|
func certIsRevokedCRL(cert *x509.Certificate, url string) (revoked, ok bool, err error) {
|
||
|
crlLock.Lock()
|
||
|
crl, ok := CRLSet[url]
|
||
|
if ok && crl == nil {
|
||
|
ok = false
|
||
|
delete(CRLSet, url)
|
||
|
}
|
||
|
crlLock.Unlock()
|
||
|
|
||
|
var shouldFetchCRL = true
|
||
|
if ok {
|
||
|
if time.Now().After(crl.ThisUpdate) {
|
||
|
shouldFetchCRL = false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
issuer := getIssuer(cert)
|
||
|
|
||
|
if shouldFetchCRL {
|
||
|
var err error
|
||
|
crl, err = fetchCRL(url)
|
||
|
if err != nil {
|
||
|
log.Warningf("failed to fetch CRL: %v", err)
|
||
|
return false, false, err
|
||
|
}
|
||
|
|
||
|
// check CRL signature
|
||
|
if issuer != nil {
|
||
|
err = crl.CheckSignatureFrom(issuer)
|
||
|
if err != nil {
|
||
|
log.Warningf("failed to verify CRL: %v", err)
|
||
|
return false, false, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
crlLock.Lock()
|
||
|
CRLSet[url] = crl
|
||
|
crlLock.Unlock()
|
||
|
}
|
||
|
|
||
|
for _, revoked := range crl.RevokedCertificates {
|
||
|
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
|
||
|
log.Info("Serial number match: intermediate is revoked.")
|
||
|
return true, true, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false, true, err
|
||
|
}
|
||
|
|
||
|
// VerifyCertificate ensures that the certificate passed in hasn't
|
||
|
// expired and checks the CRL for the server.
|
||
|
func VerifyCertificate(cert *x509.Certificate) (revoked, ok bool) {
|
||
|
revoked, ok, _ = VerifyCertificateError(cert)
|
||
|
return revoked, ok
|
||
|
}
|
||
|
|
||
|
// VerifyCertificateError ensures that the certificate passed in hasn't
|
||
|
// expired and checks the CRL for the server.
|
||
|
func VerifyCertificateError(cert *x509.Certificate) (revoked, ok bool, err error) {
|
||
|
if !time.Now().Before(cert.NotAfter) {
|
||
|
msg := fmt.Sprintf("Certificate expired %s\n", cert.NotAfter)
|
||
|
log.Info(msg)
|
||
|
return true, true, fmt.Errorf(msg)
|
||
|
} else if !time.Now().After(cert.NotBefore) {
|
||
|
msg := fmt.Sprintf("Certificate isn't valid until %s\n", cert.NotBefore)
|
||
|
log.Info(msg)
|
||
|
return true, true, fmt.Errorf(msg)
|
||
|
}
|
||
|
return revCheck(cert)
|
||
|
}
|
||
|
|
||
|
func fetchRemote(url string) (*x509.Certificate, error) {
|
||
|
resp, err := HTTPClient.Get(url)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
in, err := remoteRead(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
p, _ := pem.Decode(in)
|
||
|
if p != nil {
|
||
|
return certlib.ParseCertificatePEM(in)
|
||
|
}
|
||
|
|
||
|
return x509.ParseCertificate(in)
|
||
|
}
|
||
|
|
||
|
var ocspOpts = ocsp.RequestOptions{
|
||
|
Hash: crypto.SHA1,
|
||
|
}
|
||
|
|
||
|
func certIsRevokedOCSP(leaf *x509.Certificate, strict bool) (revoked, ok bool, e error) {
|
||
|
var err error
|
||
|
|
||
|
ocspURLs := leaf.OCSPServer
|
||
|
if len(ocspURLs) == 0 {
|
||
|
// OCSP not enabled for this certificate.
|
||
|
return false, true, nil
|
||
|
}
|
||
|
|
||
|
issuer := getIssuer(leaf)
|
||
|
|
||
|
if issuer == nil {
|
||
|
return false, false, nil
|
||
|
}
|
||
|
|
||
|
ocspRequest, err := ocsp.CreateRequest(leaf, issuer, &ocspOpts)
|
||
|
if err != nil {
|
||
|
return revoked, ok, err
|
||
|
}
|
||
|
|
||
|
for _, server := range ocspURLs {
|
||
|
resp, err := sendOCSPRequest(server, ocspRequest, leaf, issuer)
|
||
|
if err != nil {
|
||
|
if strict {
|
||
|
return revoked, ok, err
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// There wasn't an error fetching the OCSP status.
|
||
|
ok = true
|
||
|
|
||
|
if resp.Status != ocsp.Good {
|
||
|
// The certificate was revoked.
|
||
|
revoked = true
|
||
|
}
|
||
|
|
||
|
return revoked, ok, err
|
||
|
}
|
||
|
return revoked, ok, err
|
||
|
}
|
||
|
|
||
|
// sendOCSPRequest attempts to request an OCSP response from the
|
||
|
// server. The error only indicates a failure to *fetch* the
|
||
|
// certificate, and *does not* mean the certificate is valid.
|
||
|
func sendOCSPRequest(server string, req []byte, leaf, issuer *x509.Certificate) (*ocsp.Response, error) {
|
||
|
var resp *http.Response
|
||
|
var err error
|
||
|
if len(req) > 256 {
|
||
|
buf := bytes.NewBuffer(req)
|
||
|
resp, err = HTTPClient.Post(server, "application/ocsp-request", buf)
|
||
|
} else {
|
||
|
reqURL := server + "/" + neturl.QueryEscape(base64.StdEncoding.EncodeToString(req))
|
||
|
resp, err = HTTPClient.Get(reqURL)
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
return nil, errors.New("failed to retrieve OSCP")
|
||
|
}
|
||
|
|
||
|
body, err := ocspRead(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
switch {
|
||
|
case bytes.Equal(body, ocsp.UnauthorizedErrorResponse):
|
||
|
return nil, errors.New("OSCP unauthorized")
|
||
|
case bytes.Equal(body, ocsp.MalformedRequestErrorResponse):
|
||
|
return nil, errors.New("OSCP malformed")
|
||
|
case bytes.Equal(body, ocsp.InternalErrorErrorResponse):
|
||
|
return nil, errors.New("OSCP internal error")
|
||
|
case bytes.Equal(body, ocsp.TryLaterErrorResponse):
|
||
|
return nil, errors.New("OSCP try later")
|
||
|
case bytes.Equal(body, ocsp.SigRequredErrorResponse):
|
||
|
return nil, errors.New("OSCP signature required")
|
||
|
}
|
||
|
|
||
|
return ocsp.ParseResponseForCert(body, leaf, issuer)
|
||
|
}
|
||
|
|
||
|
var crlRead = io.ReadAll
|
||
|
|
||
|
// SetCRLFetcher sets the function to use to read from the http response body
|
||
|
func SetCRLFetcher(fn func(io.Reader) ([]byte, error)) {
|
||
|
crlRead = fn
|
||
|
}
|
||
|
|
||
|
var remoteRead = io.ReadAll
|
||
|
|
||
|
// SetRemoteFetcher sets the function to use to read from the http response body
|
||
|
func SetRemoteFetcher(fn func(io.Reader) ([]byte, error)) {
|
||
|
remoteRead = fn
|
||
|
}
|
||
|
|
||
|
var ocspRead = io.ReadAll
|
||
|
|
||
|
// SetOCSPFetcher sets the function to use to read from the http response body
|
||
|
func SetOCSPFetcher(fn func(io.Reader) ([]byte, error)) {
|
||
|
ocspRead = fn
|
||
|
}
|