mtlsx

package
v0.14.5 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Package mtlsx implements the per-route mTLS verification pipeline used by apic-generated handlers: trust store loading, issuer-label enforcement, CRL/OCSP revocation checks, and principal extraction. Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCertRevoked is returned when a CRL lists the certificate as
	// revoked, or when an OCSP responder reports the certificate status
	// as Revoked.
	ErrCertRevoked = errors.New("mtlsx: certificate revoked")
	// ErrCRLFetch is returned when none of the CRL distribution points
	// could be successfully fetched and parsed (and AllowSoftFail is
	// false).
	ErrCRLFetch = errors.New("mtlsx: CRL fetch failed")
)
View Source
var ErrEmptyBundle = errors.New("mtlsx: CA bundle contains no certificates")

ErrEmptyBundle is returned when the supplied CA file contains no PEM-encoded certificates. Returning this instead of an empty pool keeps the contract fail-closed: a typo in the bundle path or a truncated file aborts startup rather than silently accepting no peers.

View Source
var ErrIssuerNotAllowed = errors.New("mtlsx: certificate issuer not in allow-list")

ErrIssuerNotAllowed is returned by VerifyIssuer when the certificate's Issuer CommonName is not in the per-route allow-list.

View Source
var ErrUnknownIssuerLabel = fmt.Errorf("mtlsx: unknown issuer label")

ErrUnknownIssuerLabel is returned by IssuerLabelResolver.Resolve when a requested label has no mapping. Wrapped errors give the offending label in the message.

View Source
var ErrUnsafeEndpoint = errors.New("mtlsx: endpoint URL not permitted (SSRF guard)")

ErrUnsafeEndpoint is returned when an endpoint URL fails the SSRF allow-list (scheme not http/https, or host resolves to a loopback / private / link-local address).

Functions

func ContextWith

func ContextWith(ctx context.Context, p *Principal) context.Context

ContextWith returns a new context carrying p. A nil p is a no-op: the original context is returned unchanged so generated handlers can safely call this on the optional-mTLS path.

func ExtractUPN

func ExtractUPN(c *x509.Certificate) string

ExtractUPN walks the SAN extension and pulls a UPN otherName if present. Exported so the cacpiv adapter (Plan 03) can reuse the same parser without import-cycling back into mtlsx.

func SetVerifiedClientChain

func SetVerifiedClientChain(r *http.Request, chain []*x509.Certificate)

SetVerifiedClientChain attaches a verified client certificate chain to r so that mtlsx.Verifier can extract the leaf, issuer, and (for CRL/OCSP signature verification) any intermediates / root.

The chain is leaf-first: chain[0] is the client cert; chain[1] (if present) is the issuer used for CRL/OCSP checks; further entries are the rest of the path up to a root.

Both r.TLS.PeerCertificates and r.TLS.VerifiedChains are populated. The runtime code path reads the leaf from PeerCertificates[0] and the issuer from VerifiedChains[0][1]; without the second field set, CRL/OCSP signature checks would fail with "no issuer in verified chain", which is the friction this helper smooths.

Intended for use in *_test.go files. SetVerifiedClientChain does NOT perform any chain verification of its own — callers are responsible for supplying a chain that represents the state the test wishes to assert.

Closes the A-NEW-3 (VerifiedChains test model) friction noted in docs/GENERATOR_BUGS.md.

func VerifyIssuer

func VerifyIssuer(c *x509.Certificate, allowed []string) error

VerifyIssuer returns nil if the cert's Issuer.CommonName matches one of the entries in allowed (case-sensitive exact match). An empty allowed slice means "no issuer restriction" — every verified issuer is accepted.

This closes the gap documented in pkg/securex/mtls.go:31 where SupportedIssuers was passed through but never enforced.

Types

type CRLChecker

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

CRLChecker validates leaf certificates against issuer-issued CRLs.

func NewCRLChecker

func NewCRLChecker(cfg CRLConfig) (*CRLChecker, error)

NewCRLChecker validates the supplied config and returns a checker.

When CRLConfig.HTTPClient is nil, a default operator client is injected that refuses to follow HTTP redirects (so an internal CRL responder cannot proxy the fetch into AWS IMDS / on-host pprof / kube-internal services). Dial-time IP filtering is intentionally NOT applied to the operator client because legitimate operator deployments run an internal CRL responder on a private IP.

The checker additionally builds an internal "safe" client that adds a dial-time DNS-rebind guard on top of the redirect refusal. The safe client is used only when fromLeaf == true (leaf-supplied URLs), so the strict policy applies precisely where the URL is attacker-influenced (A-NEW-7).

func (*CRLChecker) Check

func (c *CRLChecker) Check(ctx context.Context, leaf, issuer *x509.Certificate) error

Check verifies that leaf is not revoked by any of the configured CRLs. When CRLConfig.Endpoints is non-empty, that list is authoritative. When empty AND CRLConfig.TrustLeafEndpoints is true, the runtime falls back to leaf.CRLDistributionPoints; otherwise the call returns ErrCRLFetch (FedRAMP fail-closed posture, SC-7).

type CRLConfig

type CRLConfig struct {
	// Endpoints is the list of CRL distribution point URLs to consult.
	// When non-empty, this list is authoritative — the runtime does NOT
	// fall back to attacker-controlled URLs embedded in the leaf cert
	// unless TrustLeafEndpoints is also set. (SSRF defense.)
	Endpoints []string
	// TrustLeafEndpoints, when true, allows fallback to
	// leaf.CRLDistributionPoints when Endpoints is empty. Default false
	// to fail closed against a hostile mTLS client supplying its own
	// CRLDP URL (FedRAMP / NIST 800-53 SC-7).
	TrustLeafEndpoints bool
	// TTL bounds how long a fetched CRL is cached before re-fetch.
	// Zero defaults to 15 minutes.
	TTL time.Duration
	// HTTPClient is the client used to fetch CRLs. nil → a default
	// client with a 10-second timeout. CRL responses are capped at
	// maxCRLBytes regardless of the client.
	HTTPClient *http.Client
	// AllowSoftFail returns nil instead of ErrCRLFetch when all
	// endpoints are unreachable. FedRAMP profiles MUST set this false.
	AllowSoftFail bool
}

CRLConfig configures a CRLChecker.

type IssuerLabelResolver

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

IssuerLabelResolver maps human-readable logical labels (e.g. "dod-id-ca-59", "partner-prod", "internal-mesh") to the real issuer CommonName strings that VerifyIssuer matches against.

Background: VerifyIssuer matches Issuer.CommonName exactly (see issuers.go). When a config-driven deployment groups CAs by logical role rather than by CN, supported_issuers does not slot in directly. This resolver bridges the gap so a deployment can keep label-based taxonomy in its config and still hand mtlsx the CN strings the runtime requires.

One label may map to many CNs (e.g., a "dod-id" label may cover "DOD ID CA-59" plus "DOD ID SW CA-66"). Resolve flattens, de-dupes, and returns a sorted slice ready to drop into VerifierConfig.SupportedIssuers.

Closes the A-NEW-3 (issuer-DN vs logical labels) friction noted in docs/GENERATOR_BUGS.md.

The zero value is ready to use; concurrent Add and Resolve are NOT safe — callers building a resolver concurrently should guard with their own sync.Mutex. The typical pattern is build-once at boot and read-many at request time, which is concurrent-safe by construction.

func NewIssuerLabelResolver

func NewIssuerLabelResolver() *IssuerLabelResolver

NewIssuerLabelResolver returns an empty resolver. Equivalent to a zero-value &IssuerLabelResolver{} — the constructor exists for readability at call sites.

func (*IssuerLabelResolver) Add

func (r *IssuerLabelResolver) Add(label string, cns ...string)

Add maps the given label to one or more issuer CommonName strings. Calling Add multiple times for the same label accumulates entries (it does not replace). Label and CN values are stored verbatim; VerifyIssuer is case-sensitive so callers must use the exact CN from the issuing CA certificate.

Empty or whitespace-only labels and CNs are ignored.

func (*IssuerLabelResolver) Has

func (r *IssuerLabelResolver) Has(label string) bool

Has reports whether the given label has at least one mapped CN.

func (*IssuerLabelResolver) Labels

func (r *IssuerLabelResolver) Labels() []string

Labels returns the sorted list of every label registered with this resolver. Useful for tests, audits, and rendering operator-facing "known labels" diagnostics.

func (*IssuerLabelResolver) Resolve

func (r *IssuerLabelResolver) Resolve(labels []string) ([]string, error)

Resolve takes a slice of logical labels and returns the flattened, de-duplicated, sorted list of issuer CommonName strings suitable for VerifierConfig.SupportedIssuers.

If any label has no mapping, Resolve returns ErrUnknownIssuerLabel with the offending label in the error message. Fail-closed is deliberate: a typo in the consumer config would otherwise silently produce an empty allow-list and accept every issuer.

An empty input slice returns (nil, nil) — equivalent to "no issuer restriction" semantics in VerifyIssuer.

type OCSPChecker

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

OCSPChecker queries OCSP responders for revocation status.

func NewOCSPChecker

func NewOCSPChecker(cfg OCSPConfig) (*OCSPChecker, error)

NewOCSPChecker constructs and validates the checker.

When OCSPConfig.HTTPClient is nil, a default operator client is injected that refuses to follow HTTP redirects (so an internal OCSP responder cannot proxy the request into a private-IP target). Dial-time IP filtering is intentionally NOT applied to the operator client because legitimate operator deployments run an internal OCSP responder on a private IP.

The checker additionally builds an internal "safe" client that adds a dial-time DNS-rebind guard on top of the redirect refusal. The safe client is used only when fromLeaf == true (leaf-supplied OCSPServer URLs), so the strict policy applies precisely where the URL is attacker-influenced (A-NEW-7).

func (*OCSPChecker) Check

func (c *OCSPChecker) Check(ctx context.Context, leaf, issuer *x509.Certificate) error

Check returns nil when the leaf is reported Good by the OCSP responder, ErrCertRevoked when Revoked, ErrCRLFetch when unreachable (unless AllowSoftFail is set).

type OCSPConfig

type OCSPConfig struct {
	Endpoints []string
	// TrustLeafEndpoints, when true, allows fallback to leaf.OCSPServer
	// URLs when Endpoints is empty. Default false to fail closed against
	// a hostile mTLS client supplying its own OCSP responder (SC-7).
	TrustLeafEndpoints bool
	TTL                time.Duration
	HTTPClient         *http.Client
	AllowSoftFail      bool
}

OCSPConfig configures the OCSPChecker.

type Principal

type Principal struct {
	CommonName     string
	IssuerCN       string
	SANEmails      []string
	SANDNS         []string
	SANURIs        []string
	UPN            string // Microsoft UPN OID 1.3.6.1.4.1.311.20.2.3
	EDIPI          string // populated by cacpiv adapter (Plan 03)
	Classification string // populated by cacpiv adapter (Plan 03)
	FASCN          []byte // populated by cacpiv adapter (Plan 03)
	AgencyCode     string // populated by cacpiv adapter (Plan 03)
	OrgCategory    int    // populated by cacpiv adapter (Plan 03)
	Fingerprint    string // sha256-hex of leaf DER
	Raw            *x509.Certificate
}

Principal is the identity extracted from a verified client certificate. Plan 03 (CAC/PIV) populates EDIPI/Classification/FASCN via the same struct; those fields stay zero when the cacpiv adapter is not wired.

func From

func From(ctx context.Context) (*Principal, bool)

From retrieves a Principal previously stored via ContextWith. The second return value distinguishes "no Principal in context" from "explicit nil-valued key" — callers that care about the difference should consult `ok`.

func PrincipalFromCertificate

func PrincipalFromCertificate(c *x509.Certificate) *Principal

PrincipalFromCertificate builds a Principal from a verified leaf cert. It never fails: missing fields stay zero. Callers MUST call this AFTER chain verification has succeeded; the function does not validate the chain or revoke status.

func (*Principal) String

func (p *Principal) String() string

String renders a human-readable identity for audit logging. Truncates the SHA-256 fingerprint to 12 hex chars for log readability.

type PrincipalAdapter

type PrincipalAdapter func(*Principal) error

PrincipalAdapter post-processes a Principal after core fields are extracted. Plan 03 (cacpiv) registers an adapter that adds EDIPI, FASC-N, and PE/NPE classification.

type TrustStore

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

TrustStore loads a PEM CA bundle into an *x509.CertPool and supports atomic reload without server restart. Used by the per-route VerifierConfig.CABundlePath and (via api.WithMTLS in Plan 02 Task 12) by the TLS listener itself.

func NewTrustStore

func NewTrustStore(path string) (*TrustStore, error)

NewTrustStore loads the bundle at path. Returns ErrEmptyBundle if no certificates are parsed (rather than silently producing an empty pool that would later accept zero peers).

func (*TrustStore) CertPool

func (ts *TrustStore) CertPool() *x509.CertPool

CertPool returns the currently active pool. The returned pointer is stable for the caller's scope; subsequent Reload calls swap in a new pool and do NOT mutate the one previously returned.

func (*TrustStore) Path

func (ts *TrustStore) Path() string

Path returns the bundle's source file (useful for logging).

func (*TrustStore) Reload

func (ts *TrustStore) Reload() error

Reload re-reads the underlying file and atomically swaps the pool. Safe for concurrent callers; serialized internally.

type Verifier

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

Verifier composes trust-store + securex.VerifyMTLS + issuer/CRL/OCSP + principal extraction into a single fail-closed pipeline.

func NewVerifier

func NewVerifier(cfg VerifierConfig) (*Verifier, error)

NewVerifier validates cfg, loads the trust store, and constructs optional revocation checkers.

func (*Verifier) TrustPool

func (v *Verifier) TrustPool() *x509.CertPool

TrustPool returns the configured trust store pool, or nil if none was configured. The listener uses this to set tls.Config.ClientCAs.

func (*Verifier) VerifyAndExtractPrincipal

func (v *Verifier) VerifyAndExtractPrincipal(ctx context.Context, r *http.Request) (*Principal, error)

VerifyAndExtractPrincipal runs the full pipeline on r and returns the extracted Principal (or an error). Steps, in order:

  1. securex.VerifyMTLS — TLS handshake state + EKU
  2. VerifyIssuer — SupportedIssuers allow-list
  3. CRL.Check — if CRLConfig set
  4. OCSP.Check — if OCSPConfig set
  5. PrincipalFromCertificate (+ optional PrincipalAdapter)

Fail-closed: any non-nil error from any step aborts the chain.

type VerifierConfig

type VerifierConfig struct {
	CABundlePath     string
	SupportedIssuers []string
	EKUValidation    bool
	Required         bool // mirror of securex.MTLSPolicy.Required
	CRLConfig        *CRLConfig
	OCSPConfig       *OCSPConfig
	// PrincipalAdapter optionally enriches the extracted Principal. Plan 03
	// (cacpiv) provides cacpiv.New(...).Apply as the canonical adapter
	// for DOD CAC / federal PIV deployments.
	PrincipalAdapter PrincipalAdapter
}

VerifierConfig captures one verifier's settings (typically one per-route configuration in the apic config file).

Jump to

Keyboard shortcuts

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