auth

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Apr 15, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Client cert ledger — persists metadata about issued client certs so we can revoke them by fingerprint. Plain JSON on disk; the volume of records is bounded by the number of Culvert nodes an operator enrolls (typically <100) so this is fine without a real database.

File layout (/data/clients.json):

{
  "version": 1,
  "clients": [
    { "fingerprint": "sha256:...", "name": "", "common_name": "Sluice Client",
      "issued_at_unix": 1713110400, "not_after_unix": 1744646400,
      "revoked_at_unix": 0, "revoke_reason": "" }
  ]
}

name is empty in v0.2 — we don't collect a label at Enroll time. Culvert stores its own label locally and uses fingerprint as the correlation key.

FingerprintTracker keeps the current server cert fingerprint plus an optional "rotated-out" fingerprint that clients should continue to accept during a migration grace window. This lets an operator swap the server cert without forcing every Culvert node to re-enroll.

The tracker is in-memory only. On daemon restart the rotated fingerprint (if any) is lost — operators should complete rotations before restarting, or bake a grace window long enough to outlast a restart. In practice the default 24h grace and typical restart times make this a non-issue.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BootstrapServerCerts

func BootstrapServerCerts(certFile, keyFile, caFile string, hosts []string) (caCertPEM, serverCertPEM []byte, err error)

BootstrapServerCerts ensures a CA and server certificate exist at the given paths. If any file is missing, it generates a new CA (or reuses an existing one), issues a fresh server cert for the provided hosts, and writes all three files with mode 0600. Idempotent: if everything already exists, it returns (caCert, serverCert) unchanged.

Returns the PEM-encoded CA cert and the PEM-encoded server cert for any caller that needs to log fingerprints or derive the enrollment manager.

func CertCommonName added in v0.2.0

func CertCommonName(certPEM []byte) (string, error)

CertCommonName extracts the Subject Common Name from a PEM cert.

func CertFingerprintSHA256

func CertFingerprintSHA256(certPEM []byte) (string, error)

CertFingerprintSHA256 returns the SHA-256 fingerprint of a PEM-encoded certificate as a lowercase hex string prefixed with "sha256:". This is what the operator pastes into Culvert's admin UI for TOFU verification.

func CertNotAfter added in v0.2.0

func CertNotAfter(certPEM []byte) (time.Time, error)

CertNotAfter decodes a PEM cert and returns its NotAfter timestamp. Exported because the CLI's `sluice cert expiry` needs to compute days-remaining without re-implementing x509 parsing.

func GenerateCA

func GenerateCA() (certPEM, keyPEM []byte, err error)

GenerateCA creates a new self-signed CA certificate and private key. Returns PEM-encoded cert and key.

func GenerateClientCert

func GenerateClientCert(caCertPEM, caKeyPEM []byte) (certPEM, keyPEM []byte, err error)

GenerateClientCert creates a client certificate signed by the given CA with a generic "Sluice Client" common name.

func GenerateClientCertForCN added in v0.2.0

func GenerateClientCertForCN(caCertPEM, caKeyPEM []byte, commonName string) (certPEM, keyPEM []byte, err error)

GenerateClientCertForCN creates a client certificate with a specific Common Name. Used by RenewClient so the renewed cert keeps the same identity as the presented cert.

func GenerateServerCert

func GenerateServerCert(caCertPEM, caKeyPEM []byte, hosts []string) (certPEM, keyPEM []byte, err error)

GenerateServerCert creates a server certificate signed by the given CA. Returns PEM-encoded cert and key.

func LoadCAKey

func LoadCAKey(caFile string) ([]byte, error)

LoadCAKey reads the CA private key sibling of the CA cert file.

func LoadTLSConfig

func LoadTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error)

LoadTLSConfig creates a tls.Config for a gRPC server requiring mTLS. All clients MUST present a valid client cert (RequireAndVerifyClientCert).

func LoadTLSConfigOptionalClient

func LoadTLSConfigOptionalClient(certFile, keyFile, caFile string) (*tls.Config, error)

LoadTLSConfigOptionalClient returns a tls.Config that verifies client certs when presented but does not require them. Used for gRPC servers that must accept both authenticated (Sanitize, Health) and unauthenticated (Enroll) RPCs on the same port — the per-RPC interceptor enforces auth.

Types

type ClientLedger added in v0.2.0

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

ClientLedger is the persisted set of issued + revoked client certs. Access is serialized via a mutex; all mutating operations fsync to disk before returning so the in-memory set never diverges from /data/clients.json.

func NewClientLedger added in v0.2.0

func NewClientLedger(path string) (*ClientLedger, error)

NewClientLedger loads (or creates) the ledger at path. If the file doesn't exist, an empty ledger is returned and persisted on first write.

func (*ClientLedger) ActiveCount added in v0.2.0

func (l *ClientLedger) ActiveCount() int

ActiveCount returns the number of non-revoked, non-expired records.

func (*ClientLedger) Add added in v0.2.0

func (l *ClientLedger) Add(r ClientRecord) error

Add records a newly-issued cert. Called from Enroll + RenewCert paths. Fingerprint collisions overwrite (matches renewal semantics).

func (*ClientLedger) Get added in v0.2.0

func (l *ClientLedger) Get(fingerprint string) (ClientRecord, bool)

Get returns a copy of the record for fingerprint, or ok=false if unknown.

func (*ClientLedger) IsRevoked added in v0.2.0

func (l *ClientLedger) IsRevoked(fingerprint string) bool

IsRevoked is the hot-path check called from the mTLS interceptor on every RPC. Read-lock only; ledger writes are rare.

func (*ClientLedger) List added in v0.2.0

func (l *ClientLedger) List() []ClientRecord

List returns a snapshot of all records (copies, safe to mutate).

func (*ClientLedger) Revoke added in v0.2.0

func (l *ClientLedger) Revoke(fingerprint, reason string) (bool, error)

Revoke marks a fingerprint as revoked. Returns true if the fingerprint was previously active (i.e. a meaningful revocation), false if unknown or already-revoked (idempotent behaviour the proto documents).

func (*ClientLedger) RevokeAll added in v0.2.0

func (l *ClientLedger) RevokeAll(reason string) (int, error)

RevokeAll is the CA-rotation nuke: marks every record as revoked. Returns the count of newly-revoked (not already-revoked) entries.

type ClientRecord added in v0.2.0

type ClientRecord struct {
	Fingerprint   string `json:"fingerprint"`     // "sha256:" + hex, unique key
	Name          string `json:"name,omitempty"`  // reserved for future use
	CommonName    string `json:"common_name"`     // x509 CN at issue time
	IssuedAtUnix  int64  `json:"issued_at_unix"`  // cert NotBefore
	NotAfterUnix  int64  `json:"not_after_unix"`  // cert NotAfter
	RevokedAtUnix int64  `json:"revoked_at_unix"` // 0 if not revoked
	RevokeReason  string `json:"revoke_reason,omitempty"`
}

ClientRecord is one row in the ledger.

func (ClientRecord) Active added in v0.2.0

func (r ClientRecord) Active() bool

Active reports whether this cert is currently valid for auth: unrevoked and unexpired.

func (ClientRecord) IsExpired added in v0.2.0

func (r ClientRecord) IsExpired() bool

IsExpired reports whether the cert is past its NotAfter.

func (ClientRecord) IsRevoked added in v0.2.0

func (r ClientRecord) IsRevoked() bool

IsRevoked reports whether this record has been revoked.

type EnrollmentManager

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

EnrollmentManager handles one-time enrollment tokens for Culvert integration. Tokens are stored as SHA-256 hashes at rest with a TTL (default 24h). Tokens are single-use: on successful Enroll, the entry is removed.

func NewEnrollmentManager

func NewEnrollmentManager(caCert, caKey []byte, logger *slog.Logger) (*EnrollmentManager, error)

NewEnrollmentManager creates a manager. If caCert/caKey are nil, it generates a new CA.

func (*EnrollmentManager) CACert

func (m *EnrollmentManager) CACert() []byte

CACert returns the PEM-encoded CA certificate used to sign enrolled clients.

func (*EnrollmentManager) CAKey

func (m *EnrollmentManager) CAKey() []byte

CAKey returns the PEM-encoded CA private key. Handle with care.

func (*EnrollmentManager) Count

func (m *EnrollmentManager) Count() int

Count returns the number of currently-valid tokens (for telemetry).

func (*EnrollmentManager) Enroll

func (m *EnrollmentManager) Enroll(token string) (caCert, clientCert, clientKey []byte, err error)

Enroll consumes a token and returns CA cert + client cert + client key. Returns an error if the token is invalid, consumed, or expired. Successful enrollment removes the token entry (consume-and-delete).

func (*EnrollmentManager) GenerateToken

func (m *EnrollmentManager) GenerateToken() (string, error)

GenerateToken creates a new one-time enrollment token. Returns the plaintext token to the caller (the only time it's visible) and stores only the hash.

func (*EnrollmentManager) Ledger added in v0.2.0

func (m *EnrollmentManager) Ledger() *ClientLedger

Ledger returns the wired ledger (may be nil).

func (*EnrollmentManager) RenewClient added in v0.2.0

func (m *EnrollmentManager) RenewClient(commonName string) (clientCert, clientKey []byte, notAfter time.Time, err error)

RenewClient mints a fresh client certificate for an already-enrolled caller. The caller is identified by its presented cert's Common Name (passed via commonName). Returns the new cert + key plus the cert's NotAfter so callers can report days_until_expiry without re-parsing.

This does NOT revoke the presented cert — operators who want to revoke the old cert after a successful renewal must call ledger.Revoke explicitly.

func (*EnrollmentManager) RevokeAll

func (m *EnrollmentManager) RevokeAll()

RevokeAll clears every outstanding token (emergency rotation).

func (*EnrollmentManager) SetLedger added in v0.2.0

func (m *EnrollmentManager) SetLedger(l *ClientLedger)

SetLedger wires a persistent client cert ledger to the manager. Subsequent Enroll / RenewClient calls will record issued certs.

func (*EnrollmentManager) SetTTL

func (m *EnrollmentManager) SetTTL(ttl time.Duration)

SetTTL overrides the token time-to-live. Must be > 0.

func (*EnrollmentManager) ValidToken

func (m *EnrollmentManager) ValidToken(token string) bool

ValidToken reports whether a plaintext token is known and unconsumed (and unexpired).

type FingerprintTracker added in v0.2.0

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

FingerprintTracker is concurrency-safe.

func NewFingerprintTracker added in v0.2.0

func NewFingerprintTracker(current string) *FingerprintTracker

NewFingerprintTracker creates a tracker with a single current fingerprint.

func (*FingerprintTracker) Accepts added in v0.2.0

func (t *FingerprintTracker) Accepts(fp string) bool

Accepts reports whether fp (sha256:... hex) matches EITHER the current fingerprint OR the rotated-out previous fingerprint during its grace window. Used by any code path that needs to verify a pinned fingerprint (nothing on the server uses this today, but the helper is here for symmetry with what Culvert's client implements).

func (*FingerprintTracker) Current added in v0.2.0

func (t *FingerprintTracker) Current() string

Current returns the current server cert fingerprint.

func (*FingerprintTracker) Rotate added in v0.2.0

func (t *FingerprintTracker) Rotate(newCurrent string, grace time.Duration)

Rotate records a new current fingerprint and keeps the old one acceptable for `grace`. Passing grace <= 0 drops the previous fingerprint immediately (hard cutover — forces re-enrollment).

func (*FingerprintTracker) Snapshot added in v0.2.0

func (t *FingerprintTracker) Snapshot() (current, previous string, previousUntilUnix int64)

Snapshot returns the current fingerprint, the rotated-out previous fingerprint (or empty string), and the unix timestamp at which the previous fingerprint stops being acceptable (0 = no active rotation).

Callers handling Health responses should use Snapshot; callers handling TLS verification should use Accepts.