revocation

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: BSD-3-Clause Imports: 19 Imported by: 0

Documentation

Overview

Package revocation checks certificate chains against certificate revocation lists (CRLs, RFC 5280 section 5). Neither crypto/tls nor tlsx checks revocation; they leave it to the application's VerifyConnection hook. This package is that hook. It depends only on the standard library and golang.org/x/crypto/cryptobyte, and works with crypto/tls, tlsx and other stacks that hand over verified chains, such as pion/dtls.

Use

Configure one Source per CA whose certificates should be checked, with the CA certificate and a CRL file or URL, and create a Checker:

chk, err := revocation.New(revocation.Config{
	Sources: []revocation.Source{
		{Issuer: deviceCA, URL: "http://pki.example/device-ca.crl"},
		{Issuer: rootCA, File: "/etc/pki/root.crl"},
	},
})
if err != nil { ... }
defer chk.Close()

With crypto/tls, assign its method to the Config:

cfg := &tls.Config{
	ClientAuth:       tls.RequireAndVerifyClientCert,
	ClientCAs:        pool,
	VerifyConnection: chk.VerifyConnection,
}

With tlsx, whose ConnectionState is its own type, pass the verified chains:

cfg.VerifyConnection = func(cs tlsx.ConnectionState) error {
	return chk.CheckChains(cs.VerifiedChains)
}

VerifyPeerCertificate has the signature of Config.VerifyPeerCertificate in crypto/tls, tlsx and pion/dtls, and CheckChain checks one chain from any source.

What is checked

Every certificate of each verified chain except the trust anchor: the leaf and every intermediate, each against the CRLs of the certificate that issued it. A CRL is used only if its issuer name equals the CA's subject, its signature verifies with the CA's key (and the CA may sign CRLs), its thisUpdate is not in the future and it has a nextUpdate. Every chain must pass, so a revoked intermediate on one path is refused even if another path avoids it.

Failure policy

FailClosed is the default (the zero Policy) for every Source: when the CRL was never loaded, could not be read or verified, or is past its nextUpdate, certificates of that CA are refused with an error matching ErrUnavailable. FailOpen lets them through instead and must be set on the Source explicitly; it still refuses certificates listed in the last good CRL. A CA with no Source is not checked at all; Status lists it as "not configured". Revoked certificates are refused with an error matching ErrRevoked, including those on hold (certificateHold).

Refresh

New loads every source once. Unless Config.DisableRefresh is set, each source is then refreshed in the background every RefreshInterval, and RefreshMargin before its CRL's nextUpdate if that is sooner. Failures back off from RetryMin to RetryMax. A source keeps its last good CRL until a newer one verifies. A CRL is replaced only by one with a higher CRL number; a lower number is refused as a rollback, and the same number is taken as the same CRL. (For CRLs without numbers, thisUpdate is compared.) The rollback check is in memory: it does not survive a restart. HTTP fetches use Config.HTTPClient, a per-fetch timeout and a size limit (MaxCRLSize). Close stops refreshing.

Status and events

Status reports, per source, the last fetch and success times, the CRL's thisUpdate, nextUpdate and number, its number of entries, the last error and whether it is stale, and lists CAs without a source. Config.OnEvent receives loads, failed refreshes, revoked certificates and unavailable CRLs, for metrics or logging.

Limits

  • No OCSP. It is planned; CertChecker (Config.Extra) is the extension point where it would plug in.
  • No delta CRLs. A CRL with the delta CRL indicator is refused, so a delta is never taken for a complete CRL; the freshest CRL extension of a base CRL is ignored.
  • Issuing distribution points: a CRL scoped to user or CA certificates or to distribution point URIs (partitioned CRLs) is applied to the certificates it covers; a certificate that no loaded CRL of its CA covers is treated as unavailable. Reason partitions, indirect CRLs and distribution point names that are not URIs are refused.
  • Unknown critical CRL or entry extensions make a CRL unusable.
  • A CRL must be issued by the CA itself; a separate CRL signing key certified by the CA is not supported.
  • No conditional HTTP requests (ETag, If-Modified-Since).
Example

A crypto/tls server that refuses revoked client certificates.

package main

import (
	"crypto/tls"
	"crypto/x509"
	"log"
	"time"

	"github.com/cB-Sigma/tlsx/revocation"
)

var deviceCA, rootCA *x509.Certificate
var clientCAs *x509.CertPool

func main() {
	chk, err := revocation.New(revocation.Config{
		Sources: []revocation.Source{
			// FailClosed (the default): no usable CRL, no connection.
			{Issuer: deviceCA, URL: "http://pki.example/device-ca.crl"},
			// FailOpen must be chosen explicitly.
			{Issuer: rootCA, File: "/etc/pki/root.crl", Policy: revocation.FailOpen},
		},
		RefreshInterval: 30 * time.Minute,
		OnEvent: func(e revocation.Event) {
			if e.Err != nil {
				log.Printf("revocation %s: %s: %v", e.Kind, e.Source, e.Err)
			}
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer chk.Close()

	cfg := &tls.Config{
		ClientAuth:       tls.RequireAndVerifyClientCert,
		ClientCAs:        clientCAs,
		VerifyConnection: chk.VerifyConnection,
	}
	_ = cfg

	for _, s := range chk.Status() {
		log.Printf("%s: %s, next update %s, %d revoked, error %q",
			s.Name, s.State, s.NextUpdate, s.Revoked, s.LastError)
	}
}
Example (Tlsx)

The same check with tlsx, whose ConnectionState is its own type.

package main

import (
	"crypto/x509"
	"log"

	"github.com/cB-Sigma/tlsx"
	"github.com/cB-Sigma/tlsx/revocation"
)

var deviceCA *x509.Certificate // the trusted CAs, loaded elsewhere
var clientCAs *x509.CertPool

func main() {
	chk, err := revocation.New(revocation.Config{
		Sources: []revocation.Source{{Issuer: deviceCA, File: "/etc/pki/device-ca.crl"}},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer chk.Close()

	cfg := &tlsx.Config{
		ClientAuth: tlsx.RequireAndVerifyClientCert,
		ClientCAs:  clientCAs,
		VerifyConnection: func(cs tlsx.ConnectionState) error {
			return chk.CheckChains(cs.VerifiedChains)
		},
	}
	_ = cfg
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrRevoked = errors.New("revocation: certificate is revoked")

ErrRevoked is matched by the errors of revoked certificates.

View Source
var ErrUnavailable = errors.New("revocation: no usable CRL")

ErrUnavailable is matched by the errors of certificates refused because a CRL could not be used under FailClosed.

Functions

func ReasonString

func ReasonString(code int) string

ReasonString names a CRL reason code of RFC 5280 5.3.1.

Types

type CertChecker

type CertChecker interface {
	CheckCertificate(cert, issuer *x509.Certificate) error
}

CertChecker is an extension point for revocation mechanisms other than CRLs. It is called for each certificate and its issuer in a chain, after the CRL check has passed; a non-nil error refuses the certificate.

tlsx v0.2.0 has no implementation. OCSP is planned for a later version and would be added here; the interface may change before v1.

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker checks certificate chains against CRLs. It is safe for concurrent use. Create it with New and stop it with Close.

func New

func New(cfg Config) (*Checker, error)

New validates cfg, loads every source once and, unless DisableRefresh is set, starts refreshing them in the background. A source that fails to load is not an error of New: its state is in Status and its Policy decides what checks do. New returns an error only for an invalid configuration.

func (*Checker) CheckChain

func (c *Checker) CheckChain(chain []*x509.Certificate) error

CheckChain checks a certificate chain, leaf first, such as one of the verified chains of crypto/x509's Verify. Each certificate is checked against the CRLs of its issuer, the next certificate in the chain. A self-issued last certificate is the trust anchor and is not checked. If the last certificate is not self-issued, its issuer is looked up among the Sources' Issuers.

A certificate whose issuer has no Source is not checked (Status lists the issuer as not configured). CheckChain does not verify signatures or validity periods: pass it a chain that was verified.

A refused certificate gives an error that matches ErrRevoked (a *RevokedError) or ErrUnavailable (an *UnavailableError) with errors.Is.

func (*Checker) CheckChains

func (c *Checker) CheckChains(chains [][]*x509.Certificate) error

CheckChains checks each chain with CheckChain. Every chain must pass: a certificate revoked on one path to a trusted root is refused even if another path avoids the revoked CA.

func (*Checker) Close

func (c *Checker) Close() error

Close stops background refreshing and waits for running fetches to end. Checks keep working afterwards with the CRLs already loaded, which eventually go stale. Close is idempotent and always returns nil.

func (*Checker) Refresh

func (c *Checker) Refresh(ctx context.Context) error

Refresh fetches every source now, as a background refresh would, and returns the errors of the sources that failed, joined. A failed source keeps its last good CRL.

func (*Checker) Status

func (c *Checker) Status() []SourceStatus

Status returns the state of every source, in configuration order, followed by the CAs without a source (Config.Unchecked, then those met in chains, at most 64).

func (*Checker) VerifyConnection

func (c *Checker) VerifyConnection(cs tls.ConnectionState) error

VerifyConnection checks the peer's certificate chains. It has the signature of crypto/tls's Config.VerifyConnection and can be assigned to it directly. With tlsx, whose ConnectionState is a different type, use

cfg.VerifyConnection = func(cs tlsx.ConnectionState) error {
	return checker.CheckChains(cs.VerifiedChains)
}

It checks cs.VerifiedChains; if there are none (the peer's chain was not verified), it checks cs.PeerCertificates as presented. With no peer certificates it returns nil: whether a certificate is required is the ClientAuth setting's decision.

func (*Checker) VerifyPeerCertificate

func (c *Checker) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error

VerifyPeerCertificate has the signature of Config.VerifyPeerCertificate in crypto/tls, tlsx and pion/dtls. It checks verifiedChains; if there are none, it parses rawCerts and checks them as presented.

type Config

type Config struct {
	// Sources lists the CRLs to use, one or more per CA. Several sources
	// for one CA are allowed, for example partitioned CRLs with an
	// issuing distribution point.
	Sources []Source

	// Unchecked lists CA certificates that are trusted but have no CRL.
	// Their certificates are not checked; Status reports them as not
	// configured. CAs met in chains with no Source are reported too.
	Unchecked []*x509.Certificate

	// HTTPClient fetches URL sources. The default is an http.Client with
	// FetchTimeout as its timeout.
	HTTPClient *http.Client
	// FetchTimeout bounds each fetch, file or HTTP. Default 30 seconds.
	FetchTimeout time.Duration
	// MaxCRLSize is the largest CRL accepted, in bytes. Default 16 MiB.
	MaxCRLSize int64

	// RefreshInterval is the time between refreshes of a source. Default
	// one hour.
	RefreshInterval time.Duration
	// RefreshMargin makes a source refresh this long before its CRL's
	// nextUpdate time, if that comes before the next periodic refresh.
	// Default 15 minutes.
	RefreshMargin time.Duration
	// RetryMin and RetryMax bound the back-off after a failed refresh,
	// or after a refresh within the margin that found no newer CRL. The
	// delay doubles from RetryMin up to RetryMax. Defaults 30 seconds and
	// 15 minutes.
	RetryMin, RetryMax time.Duration
	// DisableRefresh turns off background refreshing; sources are then
	// loaded by New and by Refresh only.
	DisableRefresh bool

	// ClockSkew is the tolerance applied to a CRL's thisUpdate (not in
	// the future) and nextUpdate (not in the past). Default zero.
	ClockSkew time.Duration

	// Now returns the current time. Default time.Now. It is for tests.
	Now func() time.Time

	// OnEvent, if set, is called for each Event, for metrics or logging.
	// It is called synchronously, with no locks held, and must not block.
	OnEvent func(Event)

	// Extra holds further checks run after the CRL check (see
	// CertChecker).
	Extra []CertChecker
}

Config configures a Checker. Only Sources is required.

type Event

type Event struct {
	Kind    EventKind
	Source  string
	Time    time.Time
	Err     error    // EventRefreshFailed, EventRevoked, EventUnavailable
	Number  *big.Int // CRL number, EventLoaded and EventUnchanged
	Serial  *big.Int // certificate serial, EventRevoked and EventUnavailable
	Allowed bool     // EventUnavailable under FailOpen
}

Event is passed to Config.OnEvent.

type EventKind

type EventKind int

EventKind classifies an Event.

const (
	// EventLoaded: a source accepted a new CRL.
	EventLoaded EventKind = iota
	// EventUnchanged: a refresh found the same CRL number again.
	EventUnchanged
	// EventRefreshFailed: a refresh failed (fetch, parse, signature,
	// rollback); the source keeps its last good CRL.
	EventRefreshFailed
	// EventRevoked: a check refused a revoked certificate.
	EventRevoked
	// EventUnavailable: a check found no usable CRL. Allowed says
	// whether FailOpen let the certificate through.
	EventUnavailable
)

func (EventKind) String

func (k EventKind) String() string

type Policy

type Policy int

Policy says what a check does when a CA's CRL cannot be used: it was never loaded, it could not be read or verified, or it is past its nextUpdate time.

const (
	// FailClosed refuses the certificate when the CRL cannot be used.
	// It is the zero value, and so the default for every Source.
	FailClosed Policy = iota
	// FailOpen accepts the certificate when the CRL cannot be used. A
	// certificate listed in the last good CRL is still refused, even if
	// that CRL is stale.
	FailOpen
)

func (Policy) String

func (p Policy) String() string

type RevokedError

type RevokedError struct {
	Subject   string   // the revoked certificate's subject
	Serial    *big.Int // its serial number
	Issuer    string   // its issuer's subject
	Source    string   // the Source whose CRL lists it
	RevokedAt time.Time
	Reason    int // CRL reason code (RFC 5280 5.3.1); 0 if absent
}

RevokedError reports a certificate listed in its issuer's CRL.

func (*RevokedError) Error

func (e *RevokedError) Error() string

func (*RevokedError) Is

func (e *RevokedError) Is(target error) bool

Is makes errors.Is(err, ErrRevoked) true.

type Source

type Source struct {
	// Issuer is the CA certificate that issues the CRL, and whose
	// certificates the CRL covers. The CRL's issuer name must equal the
	// Issuer's subject and its signature must verify with the Issuer's
	// key. A CA certificate re-issued with the same subject and key
	// shares the Source.
	Issuer *x509.Certificate

	// File is the path of a CRL file, DER or PEM ("X509 CRL").
	File string
	// URL is an http or https URL serving the CRL, DER or PEM.
	URL string

	// Policy applies when this CRL cannot be used. The zero value is
	// FailClosed; FailOpen must be set explicitly.
	Policy Policy

	// Name identifies the source in Status, events and errors. The
	// default is the Issuer's subject.
	Name string
}

Source is the CRL of one CA. Exactly one of File and URL is set.

type SourceStatus

type SourceStatus struct {
	Name     string
	Location string // file path or URL; empty when not configured
	Issuer   string // the CA's subject
	Policy   Policy
	State    State
	// Stale is true when a CRL is loaded and past its nextUpdate.
	Stale bool

	LastFetch   time.Time // last refresh attempt
	LastSuccess time.Time // last refresh that fetched a valid CRL
	NextRefresh time.Time // next background refresh
	LastError   string    // error of the last attempt; empty if it succeeded

	ThisUpdate, NextUpdate time.Time // of the loaded CRL
	Number                 *big.Int  // CRL number of the loaded CRL, if any
	Revoked                int       // entries in the loaded CRL
}

SourceStatus describes one source, or one CA without a source.

type State

type State string

State is the state of a source in Status.

const (
	StateOK            State = "ok"             // a current CRL is loaded
	StateStale         State = "stale"          // the loaded CRL is past its nextUpdate
	StateMissing       State = "missing"        // no CRL could be loaded yet
	StateNotConfigured State = "not configured" // a CA with no Source; not checked
)

type UnavailableError

type UnavailableError struct {
	Subject string // the certificate's subject
	Source  string // the Source that could not be used
	Err     error  // why
}

UnavailableError reports a certificate that could not be checked because its issuer's CRL is missing, unreadable or stale.

func (*UnavailableError) Error

func (e *UnavailableError) Error() string

func (*UnavailableError) Is

func (e *UnavailableError) Is(target error) bool

Is makes errors.Is(err, ErrUnavailable) true.

func (*UnavailableError) Unwrap

func (e *UnavailableError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL