Help us shape the future of Go! Take the 2026 Go Developer Survey

crlrefresh

package
v0.23.2 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package crlrefresh keeps the status snapshot of a certificate-bound approver (audit.CertApprover) supplied with the newest verified CRLs of the issuing CAs its policy pins, and archives every CRL it accepts (docs/design/crl-refresher.md, docs/guides/certificate-approvers.md).

It does the network and file I/O that package audit must not: it fetches only from URLs the operator configured (audit.CRLSources), verifies each CRL against its own issuer's certificate with audit.VerifyCRL before adding it, refuses a rollback or an equivocation, and never makes status look fresher than the CRLs say, because freshness is read only from each CRL's own dates (audit.StatusFresh, the status rule's own test). The verdicts stay in audit.CertIdentity.EvaluateStatus.

Index

Constants

View Source
const (
	MarkerRollback = "rollback"
	MarkerConflict = "conflict"
	// MarkerSetAside marks a CRL a source served after an operator set it aside.
	MarkerSetAside = "setaside"
)

Markers on a CRL archived but not added to the snapshot: a rollback (its number is below the issuer's high-water mark) or a conflict (the mark's number with another TBSCertList). Load never seeds them; an auditor reads them as what the CRL source served that the series does not contain.

Variables

This section is empty.

Functions

This section is empty.

Types

type Alarm

type Alarm struct {
	Issuer    audit.SPKIHash
	Active    bool          // the alarm is on
	Critical  bool          // a gate would not accept the newest CRL now (gates are pausing)
	Age       time.Duration // now - thisUpdate of the newest CRL; zero when none is held
	Threshold time.Duration // the age beyond which the alarm is on
	Limit     time.Duration
	At        time.Time
	Detail    string
}

Alarm is a change in an issuer's alarm state.

type Archive

type Archive interface {
	// PutCRL stores der for issuer. meta.Marker is empty for a CRL of the series.
	PutCRL(ctx context.Context, issuer audit.SPKIHash, a ArchivedCRL, der []byte) error
	// List returns the issuer's CRL files, newest number first.
	List(ctx context.Context, issuer audit.SPKIHash) ([]ArchivedCRL, error)
	// Read returns the bytes of a listed file.
	Read(ctx context.Context, issuer audit.SPKIHash, a ArchivedCRL) ([]byte, error)
	// PutReset appends a reset record; Resets lists them (with Acked set) oldest first; AckReset marks
	// one acknowledged by an operator.
	PutReset(ctx context.Context, issuer audit.SPKIHash, r ResetRecord) error
	Resets(ctx context.Context, issuer audit.SPKIHash) ([]ResetRecord, error)
	AckReset(ctx context.Context, issuer audit.SPKIHash, r ResetRecord) error
}

Archive is the dated archive of the CRLs the refresher accepts, and its rollback memory. Put must be durable and idempotent by hash.

type ArchivedCRL

type ArchivedCRL struct {
	Name       string
	Number     *big.Int
	ThisUpdate time.Time
	Marker     string
	// Source and Fetched record where and when the refresher got it (provenance, never evidence).
	Source  string
	Fetched time.Time
}

ArchivedCRL describes one CRL file of the archive. The number and time come from the file name and are not trusted: Load verifies a CRL before it takes anything from it.

type DirArchive

type DirArchive string

DirArchive is a Archive in a directory: <dir>/crl/<issuer key hash>/<number>-<thisUpdate>.crl, with a .meta.json beside each file (source and fetch time), and RESET-<time>.json for resets. A file is written to a temporary name, synced, renamed, and the directory synced, so a reader sees all of it or none; names derive from the content, so several replicas may share the directory. The directory is created 0700 and an existing one that is writable by others, or on Unix not owned by the process user, is refused: the archive is the refresher's rollback memory.

func (DirArchive) AckReset

func (d DirArchive) AckReset(_ context.Context, issuer audit.SPKIHash, r ResetRecord) error

AckReset implements Archive.

func (DirArchive) List

func (d DirArchive) List(_ context.Context, issuer audit.SPKIHash) ([]ArchivedCRL, error)

List implements Archive.

func (DirArchive) PutCRL

func (d DirArchive) PutCRL(_ context.Context, issuer audit.SPKIHash, a ArchivedCRL, der []byte) error

PutCRL implements Archive.

func (DirArchive) PutReset

func (d DirArchive) PutReset(_ context.Context, issuer audit.SPKIHash, r ResetRecord) error

PutReset implements Archive.

func (DirArchive) Read

func (d DirArchive) Read(_ context.Context, issuer audit.SPKIHash, a ArchivedCRL) ([]byte, error)

Read implements Archive.

func (DirArchive) Resets

func (d DirArchive) Resets(_ context.Context, issuer audit.SPKIHash) ([]ResetRecord, error)

Resets implements Archive.

type Event

type Event struct {
	Kind     EventKind
	Issuer   audit.SPKIHash
	Feed     string // "base" or "delta"
	URL      string
	Detail   string
	Number   *big.Int // the CRL's number
	Held     *big.Int // the issuer's high-water mark, for a rollback or conflict
	Hash     [32]byte
	Err      error
	At       time.Time
	Outcomes []URLOutcome // per URL, for a fetched or failed attempt
}

Event is one thing the refresher did or refused. URLs carry no query or fragment; no event holds a response body.

type EventKind

type EventKind string

EventKind names what an Event reports.

const (
	EventStartup          EventKind = "startup"
	EventFetched          EventKind = "fetched"
	EventUnchanged        EventKind = "unchanged"
	EventFetchFailed      EventKind = "fetch-failed"
	EventRollback         EventKind = "rollback"
	EventConflict         EventKind = "conflict"
	EventBadSignature     EventKind = "bad-signature"
	EventDistrusted       EventKind = "distrusted"
	EventUnsupportedCRL   EventKind = "unsupported-crl"
	EventFutureDated      EventKind = "future-dated"
	EventStaleOnArrival   EventKind = "stale-on-arrival"
	EventArchiveError     EventKind = "archive-error"
	EventArchiveDropped   EventKind = "archive-dropped"
	EventArchiveCorrupt   EventKind = "archive-corrupt"
	EventClockSkew        EventKind = "clock-skew"
	EventHighWaterReset   EventKind = "high-water-reset"
	EventSetAsideServed   EventKind = "set-aside-served"
	EventSourcesChanged   EventKind = "sources"
	EventPanic            EventKind = "panic"
	EventSeedStopped      EventKind = "seed-stopped"
	EventUnpairedDelta    EventKind = "unpaired-delta"
	EventRedirectRefused  EventKind = "redirect-refused"
	EventOversizeResponse EventKind = "oversize"
)

The events. Rollback, conflict, bad-signature, distrusted, future-dated, unsupported, archive-dropped and high-water-reset are security-relevant and should reach the operator.

type Hooks

type Hooks struct {
	OnEvent func(Event)
	OnAlarm func(Alarm)
}

Hooks are called from the refresher's goroutines, possibly concurrently for different issuers. They must not block; a panic in one is recovered.

type IssuerHealth

type IssuerHealth struct {
	Issuer              audit.SPKIHash
	URLs, DeltaURLs     []string // the effective list, without query or fragment
	LastAttempt         time.Time
	LastSuccess         time.Time // the last attempt that confirmed the newest CRL and found it fresh
	NewestNumber        *big.Int
	NewestThisUpdate    time.Time
	NewestNextUpdate    time.Time // the earlier of base and paired delta
	Age                 time.Duration
	NextExpected        time.Time // thisUpdate + declared skew + period
	NextAttempt         time.Time
	ConsecutiveFailures int
	LastError           string
	ArchiveBacklog      int
	Paired              bool // a delta is held and paired with its base (false when no deltas are configured)
	Alarm, Critical     bool
	Limit               time.Duration
	AlarmAge            time.Duration
	Loop                time.Time // the last time this issuer's loop ran: a watchdog for a dead loop
	UnackedResets       int
}

IssuerHealth is the state of one issuer, from Health.

type IssuerResult

type IssuerResult struct {
	Issuer audit.SPKIHash
	Err    error
}

IssuerResult is the outcome of one issuer in RefreshOnce.

type Options

type Options struct {
	// Issuers are the certificates of the pinned intermediates (the policy pins them by key only). Each
	// must match a pinned key and chain to an anchor; a pinned intermediate with no certificate here is
	// refused. They are the only certificates a CRL signature is checked against besides the anchors.
	Issuers []*x509.Certificate
	// DeltaPeriodFor is the declared delta CRL period of each issuer that has DeltaURLs.
	DeltaPeriodFor map[audit.SPKIHash]time.Duration
	// StandIn is set when the approver was built from a declaration file (audit.StatusDecl.Approver) and
	// not by the gate: the start-up events then say whether the approver's policy digest can be compared
	// with the gate's. Nil means the approver is the gate's own.
	StandIn *StandIn
	// Client is the application's HTTP client (mTLS, a proxy, a resolver). Nil builds the library's: no
	// proxy, no compression, timeouts on every stage. The library cannot enforce proxy or DNS rules on a
	// client it did not build; it still bounds the body, the context and the redirects.
	Client *http.Client
	// Archive stores accepted CRLs and the rollback memory. Nil runs without: a restart forgets rollback
	// protection, and the refresher says so at start.
	Archive Archive
	// Hooks receives events and alarms.
	Hooks Hooks
	// Sleep waits for d or the context; nil uses a timer. Rand returns [0,1); nil uses math/rand/v2.
	// Both exist so a test can drive the schedule without a real clock.
	Sleep func(ctx context.Context, d time.Duration) error
	Rand  func() float64
	// MaxArchiveQueueBytes bounds the memory held for CRLs the archive refused (default 64 MiB); the
	// oldest are dropped and reported.
	MaxArchiveQueueBytes int
}

Options configures New.

type Refresher

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

Refresher keeps the audit.StatusSnapshot of a audit.CertApprover supplied with the newest verified CRLs of the issuing CAs the certificate policy pins, and archives every CRL it accepts (docs/design/crl-refresher.md).

It fetches only from URLs the operator configured, never from a client-supplied chain and never in the request path. It verifies a CRL against the issuer's own certificate before adding it, never replaces a newer CRL with an older one, and cannot make status look fresher than the CRLs say: freshness is read from each CRL's thisUpdate and nextUpdate, never from a transport header. A failed refresh changes nothing, so a dead refresher or CA ends in the gate's existing outcome (a pause).

It is not part of the verifier: audit.CertApprover stays free of I/O.

func New

func New(a *audit.CertApprover, sources audit.CRLSources, opt Options) (*Refresher, error)

New builds the refresher for approver's snapshot. Every issuer reachable through an accepted chain (the anchors and the pinned intermediates) needs a source and a certificate; the approver's declared values (the CA period, the status ages, the overlap, the skew) drive the schedule and the alarm, so there is no second copy to drift. It returns an error wrapping agent.ErrConfig when the approver has no status snapshot or clock, a source is missing or malformed, an issuer certificate does not match a pinned key, or the declared values leave no margin for an alarm.

func (*Refresher) AckReset

func (r *Refresher) AckReset(ctx context.Context, issuer audit.SPKIHash, name string) error

AckReset marks a high-water reset record of the issuer (by its name, as Health and the start-up event give it) as acknowledged by an operator: the start-up event for it stops.

func (*Refresher) Health

func (r *Refresher) Health() []IssuerHealth

Health returns the state of every issuer, ordered by key.

func (*Refresher) Ready

func (r *Refresher) Ready() <-chan struct{}

Ready is closed once every issuer holds a fresh CRL. Nothing requires waiting for it: until then the gates pause, as they do without a refresher.

func (*Refresher) RefreshOnce

func (r *Refresher) RefreshOnce(ctx context.Context) []IssuerResult

RefreshOnce runs one attempt for every feed of every issuer (seeding from the archive first when the snapshot is empty) and returns each issuer's outcome: an error when no valid CRL was obtained or the newest one is not fresh. It is the cron form and the test hook.

func (*Refresher) ResetHighWater

func (r *Refresher) ResetHighWater(ctx context.Context, issuer audit.SPKIHash, req ResetRequest) error

ResetHighWater is the operator's way out of a poisoned high-water mark: a CA that issued one CRL with a mistaken huge number, or was restored from a backup and restarted its numbering, would otherwise leave every later real CRL a rollback. It sets aside the CRLs named (or the archived CRL at the mark), appends a durable RESET record (issuer, what is set aside, the serials that no remaining CRL lists, reason, operator, time) to the archive, removes them from the snapshot and recomputes the mark from the rest. The set-aside files stay in the archive, and a source that keeps serving one has it archived with a marker and never added. A running refresher on a shared archive (another process) applies the record at its next attempt. It needs an archive, because the reset must survive a restart.

A CRL set aside may be the newest real one. If it lists a revocation no remaining CRL lists, the reset would make status look better, so it is refused unless AcceptLostListings is set.

func (*Refresher) Run

func (r *Refresher) Run(ctx context.Context) error

Run seeds from the archive, then fetches until ctx is cancelled and the in-flight requests end. Each issuer has its own goroutine; a panic in one is recovered, reported and restarted with a backoff. Run returns nil on cancel. It may be called once at a time.

func (*Refresher) Seed

func (r *Refresher) Seed(ctx context.Context) error

Seed loads what the live rule needs from the archive into the snapshot, newest first, and takes the high-water marks from the CRLs that verify. A mark never comes from a file name alone: a stray or planted file with a huge number has no CRL behind it and is reported as corrupt. CRLs a reset record sets aside and CRLs marked rollback or conflict are never loaded. Seeding does not touch the snapshot's eviction bookkeeping, and stops when the snapshot is full.

type ResetItem

type ResetItem struct {
	Hash   string `json:"hash"`
	Number string `json:"number"`
}

ResetItem is one CRL a reset sets aside.

type ResetRecord

type ResetRecord struct {
	Name     string      `json:"-"`
	Issuer   string      `json:"issuer"`
	SetAside []ResetItem `json:"set_aside"`
	// LostSerials are the certificate serials (decimal) the CRLs set aside listed and no CRL left in the
	// snapshot lists: the revocations the reset dropped from the live gate, for an auditor.
	LostSerials []string  `json:"lost_serials,omitempty"`
	Reason      string    `json:"reason"`
	Operator    string    `json:"operator"`
	Time        time.Time `json:"time"`
	Acked       bool      `json:"-"`
}

ResetRecord is the durable record of an operator's reset of an issuer's high-water mark: the CRLs it sets aside, who did it and why. Load computes the mark over the archive without the CRLs a record names. The CRLs stay in the archive and are listed for audit.

type ResetRequest

type ResetRequest struct {
	// Reason and Operator are required and recorded in the archive.
	Reason, Operator string
	// SetAside are the SHA-256 hashes of the archived CRLs to set aside. With none, the archived CRL at
	// the current mark.
	SetAside [][32]byte
	// AcceptLostListings allows the reset when a CRL set aside lists a certificate that no CRL left in the
	// snapshot lists: the reset would drop that revocation from the live gate until the CA publishes it
	// again. Without it such a reset is refused; with it the serials are written into the record.
	AcceptLostListings bool
}

ResetRequest is an operator's request to reset an issuer's high-water mark.

type StandIn added in v0.21.0

type StandIn struct{ Missing []string }

StandIn says what a declaration file lacked of the values the policy digest covers (audit.StatusDecl.MissingForDigest). With none missing the stand-in approver's digest equals the gate's for the same declaration.

type URLOutcome

type URLOutcome struct {
	URL string `json:"url"` // without query and fragment
	// Kind is "ok" (fetched; reported alone only when the attempt succeeded), "not-modified", "oversize",
	// "timeout", "redirect-refused", "http-status", "error", "not-a-crl", "refused" (fetched but not accepted:
	// a rollback, conflict, set-aside, future-dated, unverifiable or wrong-kind CRL; Detail says why) or
	// "stale" (fetched, archived and added, but already past its nextUpdate).
	Kind   string `json:"kind"`
	Status int    `json:"status,omitempty"`
	Detail string `json:"detail,omitempty"`
}

URLOutcome is the result of asking one URL in one attempt, for the event.

Jump to

Keyboard shortcuts

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