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
- type Alarm
- type Archive
- type ArchivedCRL
- type DirArchive
- func (d DirArchive) AckReset(_ context.Context, issuer audit.SPKIHash, r ResetRecord) error
- func (d DirArchive) List(_ context.Context, issuer audit.SPKIHash) ([]ArchivedCRL, error)
- func (d DirArchive) PutCRL(_ context.Context, issuer audit.SPKIHash, a ArchivedCRL, der []byte) error
- func (d DirArchive) PutReset(_ context.Context, issuer audit.SPKIHash, r ResetRecord) error
- func (d DirArchive) Read(_ context.Context, issuer audit.SPKIHash, a ArchivedCRL) ([]byte, error)
- func (d DirArchive) Resets(_ context.Context, issuer audit.SPKIHash) ([]ResetRecord, error)
- type Event
- type EventKind
- type Hooks
- type IssuerHealth
- type IssuerResult
- type Options
- type Refresher
- func (r *Refresher) AckReset(ctx context.Context, issuer audit.SPKIHash, name string) error
- func (r *Refresher) Health() []IssuerHealth
- func (r *Refresher) Ready() <-chan struct{}
- func (r *Refresher) RefreshOnce(ctx context.Context) []IssuerResult
- func (r *Refresher) ResetHighWater(ctx context.Context, issuer audit.SPKIHash, req ResetRequest) error
- func (r *Refresher) Run(ctx context.Context) error
- func (r *Refresher) Seed(ctx context.Context) error
- type ResetItem
- type ResetRecord
- type ResetRequest
- type StandIn
- type URLOutcome
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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.