challenge

package
v1.8.0-rc1 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 39 Imported by: 0

Documentation

Overview

Package challenge implements the AppSec WAF challenge mode: a PoW-gated landing page, a fingerprint collection bundle, and the surrounding key rotation + cookie machinery. This file holds the runtime orchestration (lifecycle, HTTP entry points, template rendering, hook plumbing) and delegates specialized concerns to sibling files in the same package:

  • keyring.go / crypto.go / ticket.go — per-epoch HKDF keys, AES-GCM cookie seal/unseal, ticket signing
  • static_bundle.go — public fpscanner/JS bundle
  • dynamic_module.go — sensitive per-epoch sign-key module
  • obfuscator.go — wazero wrapper around the JS obfuscator
  • fingerprint*.go — fingerprint wire shape + helpers + mismatch report
  • config.go / secret.go — YAML config + master-secret handling

Package challenge dynamic_module.go handles the **sensitive** per-epoch sign-key module — the small JS that embeds the current epoch's HMAC key (~30 lines, expanded to ~10-30KB after obfuscation). This is the actual cryptographic material protected by the challenge runtime; obfuscation here is doing real work, not cosmetic byte variance.

The module is rebuilt whenever the keyring advances to a new epoch (default cadence: 5 minutes via keyringDefaultRotation). To give per-visitor variance in how the same key is embedded, the runtime keeps a small pool of cryptoPoolSize variants per epoch — each variant is an independent obfuscation of the same input. Default pool size is 1, preserving the historical single-variant-per-epoch behavior; operators that want per-visitor variance for the sensitive code can raise WithCryptoObfuscationPoolSize.

For the **non-sensitive** challenge code (the build-time-obfuscated crypto/glue served inline) and the separately-served, unobfuscated fpscanner bundle, see static_bundle.go.

spent_set.go is the single-use store that eliminates challenge replay: each validated submission burns its per-challenge nonce `r`, so a replay fails.

Package challenge static_bundle.go handles the **challenge code** — the crypto/glue JavaScript injected inline on the challenge page. It is obfuscated once at build time (initial_bundle.js.gz) and loaded verbatim at startup; no runtime re-obfuscation. The public fpscanner is served separately and unobfuscated at ChallengeFPScannerPath. For the sensitive per-epoch HMAC sign key, see dynamic_module.go.

Index

Constants

View Source
const (
	ChallengeJSPath        = "/crowdsec-internal/challenge/challenge.js"
	ChallengeSubmitPath    = "/crowdsec-internal/challenge/submit"
	ChallengePowWorkerPath = "/crowdsec-internal/challenge/pow-worker.js"
	ChallengeFPScannerPath = "/crowdsec-internal/challenge/fpscanner.js"
)

Internal URL paths the challenge runtime intercepts. Bouncers MUST forward these to the WAF unmodified; they are served by the appsec dispatcher (pkg/appsec/appsec.go) rather than by the protected origin.

View Source
const (
	SeverityHigh   = "high"
	SeverityMedium = "medium"
	SeverityLow    = "low"
)

Severity labels. Mirror the fpscanner library's fastBotDetectionDetails[*].severity strings so the vocabulary is consistent end-to-end.

View Source
const (
	ReasonCDP                      = "cdp"
	ReasonWebdriver                = "webdriver"
	ReasonWebdriverWritable        = "webdriver_writable"
	ReasonSelenium                 = "selenium"
	ReasonPlaywright               = "playwright"
	ReasonWebdriverIframe          = "webdriver_iframe"
	ReasonWebdriverWorker          = "webdriver_worker"
	ReasonHeadlessScreenResolution = "headless_screen_resolution"
	ReasonMissingChromeObject      = "missing_chrome_object"
	ReasonImpossibleMemory         = "impossible_memory"
	ReasonHighCPUCount             = "high_cpu_count"
	ReasonMismatchWebGLWorker      = "mismatch_webgl_worker"
	ReasonMismatchPlatformIframe   = "mismatch_platform_iframe"
	ReasonMismatchPlatformWorker   = "mismatch_platform_worker"
	ReasonPlatformMismatch         = "platform_mismatch"
	ReasonGPUMismatch              = "gpu_mismatch"
	ReasonBotUserAgent             = "bot_user_agent"
	ReasonInconsistentEtsl         = "inconsistent_etsl"
	ReasonUAMobile                 = "ua_mobile"
	ReasonUTCTimezone              = "utc_timezone"
	ReasonAcceptLanguage           = "accept_language"
	ReasonSwiftshaderRenderer      = "swiftshader_renderer"
	ReasonMismatchLanguages        = "mismatch_languages"
	ReasonTimezoneCountry          = "timezone_country"
)

Stable reason keys. One per entry in libDetections / customDetections below. Kept as exported constants so rule authors can `Has("platform_mismatch")` without typos.

View Source
const (
	PowDifficultyDisabled   = 0   // no PoW required, nonce "0" always valid
	PowDifficultyLow        = 10  // ~1024 avg iterations ≈ 0.2-2s
	PowDifficultyMedium     = 12  // ~4096 avg iterations ≈ 1-8s
	PowDifficultyHigh       = 15  // ~32768 avg iterations ≈ 7-60s
	PowDifficultyImpossible = 256 // full SHA-256 width: clients cannot solve, server always rejects

)

PoW difficulty levels in leading zero bits. Pure JS SHA-256 through the obfuscator runs ~500-5000 ops/sec, so keep these conservative.

View Source
const ChallengeCookieName = "__crowdsec_challenge"

ChallengeCookieName is the name of the sealed cookie carrying the successfully-validated fingerprint between requests.

View Source
const DefaultChallengeCSP = "" /* 135-byte string literal not displayed */

DefaultChallengeCSP is the Content-Security-Policy header used on the challenge page when the operator hasn't configured a custom one. Allows inline script/style (the challenge runtime injects both) and blob workers (the PoW worker is loaded from a blob URL).

View Source
const MaxAllowlistReasonLen = 256

MaxAllowlistReasonLen caps the reason string operators pass to GrantChallengeCookie. The reason travels inside every Set-Cookie + Cookie header round-trip until the cookie expires; bounding it keeps the cookie well under the 4 KB browser limit even with the AES-GCM tag + base64 expansion.

View Source
const MaxCookieLen = 4096

MaxCookieLen is the DEFAULT per-cookie size (RFC 6265 §6.1: 4096 bytes). Can be configured via Config.MaxCookieSize and we reject anything bigger.

Variables

View Source
var (
	ErrChallengeFields     = errors.New("missing required fields in challenge response")
	ErrChallengeTicket     = errors.New("invalid ticket in challenge response")
	ErrChallengeDifficulty = errors.New("challenge difficulty is impossible")
	ErrChallengePoW        = errors.New("invalid proof-of-work in challenge response")
	ErrChallengeHMAC       = errors.New("invalid HMAC in challenge response")
	ErrChallengePayload    = errors.New("invalid challenge response payload")
)

Sentinel errors (reasons) returned by ValidateChallengeResponse.

View Source
var (
	ErrCookieMalformed     = errors.New("malformed cookie")
	ErrCookieSignature     = errors.New("invalid cookie signature")
	ErrCookiePayload       = errors.New("invalid cookie payload")
	ErrCookieExpired       = errors.New("cookie expired")
	ErrCookieVersion       = errors.New("unknown cookie version")
	ErrAllowlistReasonSize = errors.New("allowlist reason exceeds maximum length")
	ErrCookieTooLarge      = errors.New("cookie exceeds maximum size")
)
View Source
var FPScannerJS = challengejs.FPScannerJS

FPScannerJS is the public, unobfuscated fpscanner bundle served at ChallengeFPScannerPath. Re-exported so the dispatcher can serve it via the challenge package alongside PowWorkerJS.

View Source
var GrantRedirectBody string

grantRedirectBody is the body of the 307 from GrantChallengeCookie — a no-JS fallback for HTTP clients that don't auto-follow Location. Static, so no per-request parsing is needed.

View Source
var PowWorkerJS string

PowWorkerJS is the JavaScript PoW worker shipped to the browser at ChallengePowWorkerPath. Served as-is — obfuscation here would only slow down the per-visitor PoW loop without adding security value.

Functions

func DifficultyFromLevel

func DifficultyFromLevel(level string) (int, error)

DifficultyFromLevel resolves a named level ("low", "medium", "high") to a PoW difficulty in leading zero bits. Case-insensitive.

func KnownReasons

func KnownReasons() []string

KnownReasons returns the full set of reason keys the aggregator may emit. Order is not guaranteed; callers that need deterministic ordering should walk libDetections / customDetections directly.

func ParseConfiguredSecret

func ParseConfiguredSecret(value string) ([]byte, error)

ParseConfiguredSecret accepts a configured master secret as either a hex string (preferred — encodes raw bytes unambiguously) or a raw passphrase (fallback for human-edited configs). The result is at least minSecretBytes.

func SeverityFor

func SeverityFor(reason string) string

SeverityFor returns the severity tagged to a reason key, or "" for unknown reasons.

Types

type ChallengeRuntime

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

ChallengeRuntime is the per-process state for the challenge mode: wazero instance, signing/cookie keys, obfuscation pools, fingerprint key rotation. One instance is shared across all appsec runners (see pkg/acquisition/modules/appsec/config.go). Construct via NewChallengeRuntime.

func NewChallengeRuntime

func NewChallengeRuntime(ctx context.Context, opts ...Option) (*ChallengeRuntime, error)

func (*ChallengeRuntime) Close

func (c *ChallengeRuntime) Close(ctx context.Context) error

func (*ChallengeRuntime) Difficulty

func (c *ChallengeRuntime) Difficulty() int

Difficulty returns the current default PoW difficulty (in leading zero bits).

func (*ChallengeRuntime) GetChallengePage

func (c *ChallengeRuntime) GetChallengePage(ctx context.Context, userAgent string, difficulty int) (string, error)

GetChallengePage renders the challenge HTML page with the given PoW difficulty. If difficulty is 0, the default difficulty is used.

func (*ChallengeRuntime) ObfuscateJS

func (c *ChallengeRuntime) ObfuscateJS(ctx context.Context, inputJS string) (string, error)

ObfuscateJS runs the input source through the embedded `javascript-obfuscator` wasm module and returns the obfuscated output. Thread-safe: wazero allows concurrent module instantiations from the same compiled module, which is what makes the dynamic-module singleflight pattern work.

func (*ChallengeRuntime) SealAllowlistCookie

func (c *ChallengeRuntime) SealAllowlistCookie(request *http.Request, reason string, ttlOverride *time.Duration) (*cookie.AppsecCookie, error)

SealAllowlistCookie mints an allowlist-bypass cookie (no fingerprint, with the operator reason) so GrantChallengeCookie can let trusted bots skip the challenge UI while still hitting on_challenge rules via fingerprint.Allowlisted. not_after honors c.cookieTTL unless ttlOverride (>0) is given; reason is bounded by MaxAllowlistReasonLen (crypto.go).

func (*ChallengeRuntime) SetDifficulty

func (c *ChallengeRuntime) SetDifficulty(level string) error

SetDifficulty sets the default PoW difficulty from a named level.

func (*ChallengeRuntime) ValidCookie

func (c *ChallengeRuntime) ValidCookie(ck *http.Cookie, userAgent string) (*CookieData, error)

ValidCookie unseals and validates a challenge cookie: envelope (version, AES-GCM tag), not_after expiry, and UA-pinning (a stolen cookie is useless to a different client). On any failure (tampered/expired/UA-mismatch/unknown version) it returns an error and the caller should treat the request as cookieless.

func (*ChallengeRuntime) ValidateChallengeResponse

func (c *ChallengeRuntime) ValidateChallengeResponse(request *http.Request, body []byte) (*cookie.AppsecCookie, FingerprintData, int, error)

ValidateChallengeResponse parses a submit POST and runs the full chain: freshness + PoW-salt authenticity + difficulty, PoW solution, the submission signature `sig` (keyed by the never-transmitted s = HMAC(K_epoch, r)), a single-use burn of `r` (replay protection), and fingerprint de-obfuscation. On success it returns the sealed cookie, decoded FingerprintData, and the proven PoW difficulty; failures return a generic error so the caller doesn't leak which stage failed.

type Config

type Config struct {
	// MasterSecret is the long-lived secret all per-epoch HMAC keys and the
	// cookie-sealing AES key derive from. In a distributed deployment every
	// instance MUST share the same value to sign/verify each other's
	// challenges. If unset, the runtime generates an ephemeral random secret
	// at startup — fine for a single instance, but restarts then invalidate
	// outstanding cookies.
	MasterSecret *string `yaml:"master_secret"`

	// KeyRotationInterval is the per-epoch key advance period. All instances
	// in a distributed setup MUST agree on it to derive identical keys.
	// Defaults to 5m.
	KeyRotationInterval *time.Duration `yaml:"key_rotation_interval"`

	// MaxLiveEpochs is how many past epochs (besides the current one) the
	// keyring keeps accepting, so in-flight submissions aren't invalidated at
	// a rotation boundary. Bounds ticket-forgery exposure to
	// MaxLiveEpochs × KeyRotationInterval. Defaults to 3.
	MaxLiveEpochs *int `yaml:"max_live_epochs"`

	// CookieTTL is how long a successful-challenge cookie stays valid.
	// Decoupled from the keyring window (enforced by a not_after stamp inside
	// the sealed cookie, not key eviction) so cookies can be long-lived while
	// per-epoch keys rotate tightly. Defaults to 12h.
	CookieTTL *time.Duration `yaml:"cookie_ttl"`

	// MaxCookieSize caps the challenge cookie's encoded size, enforced on both
	// seal and open. It bounds the memory allocated from the (attacker-supplied)
	// fingerprint envelope, closing an over-allocation DoS. Defaults to
	// MaxCookieLen (4096, the per-cookie size browsers guarantee); raise it only
	// if a non-browser client tolerates larger cookies.
	MaxCookieSize *int `yaml:"max_cookie_size"`

	// CryptoObfuscationPoolSize is how many obfuscations of the per-epoch
	// sign-key module to keep per live epoch. Each variant embeds the same key
	// with different byte layout, giving per-visitor variance. Defaults to 1.
	CryptoObfuscationPoolSize *int `yaml:"crypto_obfuscation_pool_size"`

	// SpentSetMaxEntries caps the replay-protection LRU. A deep DoS backstop;
	// steady-state stays far below it. Defaults to spentSetDefaultMaxEntries.
	SpentSetMaxEntries *int `yaml:"spent_set_max_entries"`

	// LogLevel sets the challenge runtime's own log verbosity, independent of
	// the global level. Note: `panic` is not supported — logrus.PanicLevel is 0,
	// which SubLogger (pkg/logging/sublogger.go) treats as "inherit the parent level".
	LogLevel *log.Level `yaml:"log_level,omitempty"`
}

Config carries the YAML-configurable challenge runtime settings.

func (*Config) MergeFrom

func (c *Config) MergeFrom(other *Config)

MergeFrom overlays the non-nil fields of other onto c, field by field, so multiple appsec-configs can each contribute a disjoint subset (last non-nil wins). A nil receiver or argument is a no-op.

type CookieData

type CookieData struct {
	Fingerprint     FingerprintData
	PowDifficulty   int
	Allowlisted     bool
	AllowlistReason string
}

CookieData bundles the decoded fingerprint with cookie-envelope metadata for re-challenge decisions. Allowlisted/AllowlistReason mark cookies minted by SealAllowlistCookie; they are zero for real-submission cookies.

type CookieEnvelope

type CookieEnvelope struct {
	Envelope        *pb.ChallengeCookie
	Allowlisted     bool
	AllowlistReason string
	NotAfter        int64
}

CookieEnvelope bundles the proto payload with the header fields that openCookie pulls out of the AEAD-sealed plaintext: allowlist marker, allowlist reason, expiration. Returned by openCookie so callers can route allowlist cookies differently from real-submission ones without re-parsing the plaintext.

type FingerprintData

type FingerprintData struct {
	Signals                 fingerprintSignals                 `json:"signals"`
	FSID                    string                             `json:"fsid"`
	Nonce                   string                             `json:"nonce"`
	Time                    int64                              `json:"time"`
	URL                     string                             `json:"url"`
	FastBotDetection        FlexBool                           `json:"fastBotDetection"`
	FastBotDetectionDetails fingerprintFastBotDetectionDetails `json:"fastBotDetectionDetails"`
	Bot                     fingerprintBotAlias                `json:"-"`

	// Allowlisted is true on cookies minted by GrantChallengeCookie (operator
	// bypass for trusted bots like Googlebot) — these cookies never went
	// through a real challenge submission and carry no measured signals.
	// AllowlistReason is the operator-supplied free-form string identifying
	// why the bypass was granted, exposed to on_challenge expressions so
	// per-route policy can distinguish bypass categories.
	//
	// Both fields live in the cookie's AEAD plaintext header (see
	// crypto.go), NOT in the protobuf envelope. They are populated by
	// ValidCookie / GrantChallengeCookie and zero for normal cookies.
	Allowlisted     bool   `json:"-"`
	AllowlistReason string `json:"-"`
}

FingerprintData is the deserialized payload produced by the JS fingerprint bundle and (after a successful challenge) carried inside the sealed challenge cookie. It is the value exposed to rule authors via the `fingerprint` variable in expr environments. The struct mirrors the JSON wire shape one-to-one; FlexInt/FlexBool primitives tolerate the bundle's occasional "error string instead of value" outputs without aborting the whole submission.

func (*FingerprintData) AcceptLanguageMismatch

func (f *FingerprintData) AcceptLanguageMismatch(req *http.Request) bool

AcceptLanguageMismatch reports whether the request's Accept-Language header disagrees with the fingerprint's navigator.language. Both are derived from the same browser preference, so on a real browser they should agree at the base-language level.

Returns false when either side is empty or unparseable — never a false positive on missing data.

Deliberately different from the library's MismatchLanguages detection, which only compares navigator.languages[0] to navigator.language inside the browser; this helper brings the HTTP header into the comparison.

func (*FingerprintData) BotSignalCount

func (f *FingerprintData) BotSignalCount() int

BotSignalCount returns the number of fast-bot-detection signals that fired.

func (*FingerprintData) BotSignals

func (f *FingerprintData) BotSignals() []string

BotSignals returns the names of every library-native bot-detection signal that fired on this fingerprint (e.g. "webdriver", "cdp", "headless_screen_resolution", "platform_mismatch"). The slice is empty when nothing fired. Ordering matches libDetections and is therefore stable across calls, which keeps log output deterministic.

Only signals carried directly by f.Bot are returned: the custom CrowdSec mismatches in customDetections need request/geo context that is not on FingerprintData and are surfaced separately via MismatchReport.

func (*FingerprintData) CPUCount

func (f *FingerprintData) CPUCount() int

CPUCount returns navigator.hardwareConcurrency as a native int.

func (*FingerprintData) ComputeMismatchReport

func (f *FingerprintData) ComputeMismatchReport(req *http.Request, country string) *MismatchReport

ComputeMismatchReport walks the library-native bot alias plus the CrowdSec custom helpers and returns every fired signal in a stable order. It is the pure computation backing EvaluateMismatches; callers wanting caching + observability should use the env closure registered in GetOnChallengeEnv instead.

`country` is the ISO-3166 alpha-2 code of the client's geolocated country, or "" when unknown. Pass `exprhelpers.IPToCountry(...)`'s return value (or equivalent) — resolved once at the call site so this method stays free of side-effects.

Iteration order is libDetections first (in the order declared there), then customDetections. Reason / severity / accessor binding for every check lives in one place — fingerprint_mismatch_data.go.

func (*FingerprintData) HasAutomationSignal

func (f *FingerprintData) HasAutomationSignal() bool

HasAutomationSignal returns true if any automation-framework signal fired (webdriver, selenium, CDP, playwright, bot user-agent regex).

func (*FingerprintData) HasBotSignal

func (f *FingerprintData) HasBotSignal() bool

HasBotSignal returns true if any fast-bot-detection signal fired.

func (*FingerprintData) HasHeadlessSignal

func (f *FingerprintData) HasHeadlessSignal() bool

HasHeadlessSignal returns true if any headless-browser signal fired. Also folds in inconsistent-etsl, which fires when the TLS-level `etsl` integer disagrees with the claimed browser family — characteristic of patched / forged headless environments.

func (*FingerprintData) HasImpossibleDeviceSignal

func (f *FingerprintData) HasImpossibleDeviceSignal() bool

HasImpossibleDeviceSignal returns true if the reported device specs are outside plausible bounds (memory / CPU count).

func (*FingerprintData) HasMismatchSignal

func (f *FingerprintData) HasMismatchSignal() bool

HasMismatchSignal returns true if any cross-context or cross-API mismatch signal fired (iframe/worker webdriver, platform, WebGL in worker, UA-vs- navigator platform, GPU, languages).

func (*FingerprintData) IsBot

func (f *FingerprintData) IsBot() bool

IsBot returns the library's fast-bot verdict as a native bool, so rules can write `fingerprint.IsBot()` instead of `fingerprint.FastBotDetection.Bool() == true`.

func (*FingerprintData) IsMobile

func (f *FingerprintData) IsMobile() bool

IsMobile returns true if the browser advertises a mobile form factor (via UA client hints).

func (*FingerprintData) Language

func (f *FingerprintData) Language() string

Language returns the browser's primary language.

func (*FingerprintData) LogAccepted

func (f *FingerprintData) LogAccepted(logger *log.Entry, level log.Level, clientIP, bouncerIP, msg string, verbosity ...FingerprintLogVerbosity)

LogAccepted emits a single structured log line for a fingerprint we are accepting (valid cookie, successful submission, allowlist grant).

clientIP is the real visitor address (typically request.ClientIP, set by the bouncer via X-Forwarded-For or equivalent) and is logged as "source". bouncerIP is the connection-level peer of the appsec listener (typically request.RemoteAddrNormalized) and is logged as "bouncer". Both are needed: operators correlate visitor behavior on "source", but "bouncer" is what they use to debug which gateway forwarded the request (multi-WAF setups, misconfigured X-Forwarded-For chains, etc.).

The caller picks the level — typically Debug for per-request sites and Info for rarer events. verbosity defaults to FingerprintLogMinimal.

func (*FingerprintData) LogRejected

func (f *FingerprintData) LogRejected(logger *log.Entry, level log.Level, clientIP, bouncerIP, reason, msg string, verbosity ...FingerprintLogVerbosity)

LogRejected emits a single structured log line for a fingerprint we are rejecting.

reason is the operator-facing rejection cause and is always included regardless of verbosity. clientIP is logged as "source", bouncerIP as "bouncer" — see LogAccepted for the rationale. Only positive information is included — negative facts ("not headless", "0 mismatches") are omitted because they are noise on a reject log.

func (*FingerprintData) Memory

func (f *FingerprintData) Memory() int

Memory returns navigator.deviceMemory as a native int.

func (*FingerprintData) Platform

func (f *FingerprintData) Platform() string

Platform returns the browser-reported platform, preferring the high-entropy client-hint value and falling back to navigator.platform.

func (*FingerprintData) Timezone

func (f *FingerprintData) Timezone() string

Timezone returns the browser-reported IANA timezone.

func (*FingerprintData) TimezoneCountryMismatch

func (f *FingerprintData) TimezoneCountryMismatch(country string) bool

TimezoneCountryMismatch reports whether the fingerprint's timezone disagrees with the client's country of origin (typically resolved via IPToCountry from the client IP).

SOFT SIGNAL. Travelers whose OS timezone hasn't auto-adjusted and VPN users will legitimately trigger this. Rule authors should combine it with other signals (High()/Medium() counts, or reason-specific checks) before using it alone to ban.

Returns false when either the timezone is unknown to our IANA table or the country is empty.

func (*FingerprintData) ToProto

func (f *FingerprintData) ToProto() *pb.FingerprintData

func (*FingerprintData) UAMobileMismatch

func (f *FingerprintData) UAMobileMismatch() bool

UAMobileMismatch reports whether the fingerprint's user-agent claims a mobile form factor while the reported inner viewport width is implausibly wide for a real device. Catches the "UA switcher set to Android while actually on a desktop" pattern.

Returns false (no signal) when either the UA doesn't claim mobile or the inner width is zero/missing.

func (*FingerprintData) UnmarshalJSON

func (f *FingerprintData) UnmarshalJSON(data []byte) error

func (*FingerprintData) UserAgent

func (f *FingerprintData) UserAgent() string

UserAgent returns the user-agent reported by the browser.

type FingerprintLogVerbosity

type FingerprintLogVerbosity int

FingerprintLogVerbosity controls how much of a fingerprint is included in accept/reject log entries.

const (
	// FingerprintLogMinimal: source, fsid, ua, platform, is_bot, signals,
	// allowlisted (and allowlist_reason when allowlisted). This is the
	// default when verbosity is omitted.
	FingerprintLogMinimal FingerprintLogVerbosity = iota
	// FingerprintLogInfo: minimal + is_mobile and the category roll-ups
	// (automation/headless/mismatch/impossible_device) when they fired.
	// This is the default tier surfaced to expr helpers as the string
	// "info".
	FingerprintLogInfo
	// FingerprintLogVerbose: info + timezone, language, cpu_count,
	// memory, url, nonce, fp_time.
	FingerprintLogVerbose
)

type FlexBool

type FlexBool bool

FlexBool handles JSON values that may be either a boolean or a string.

func (FlexBool) Bool

func (fb FlexBool) Bool() bool

func (*FlexBool) UnmarshalJSON

func (fb *FlexBool) UnmarshalJSON(data []byte) error

type FlexBrandVersions

type FlexBrandVersions []fingerprintBrandVersion

FlexBrandVersions handles a JSON value that should be []fingerprintBrandVersion but may be a string (error) from the fingerprint library.

func (*FlexBrandVersions) UnmarshalJSON

func (f *FlexBrandVersions) UnmarshalJSON(data []byte) error

type FlexInt

type FlexInt int

FlexInt handles JSON values that may be either a number or a string. The fingerprint JS library returns a string (error message) instead of a number when it fails to collect a value.

func (FlexInt) Int

func (fi FlexInt) Int() int

func (*FlexInt) UnmarshalJSON

func (fi *FlexInt) UnmarshalJSON(data []byte) error

type KeyRing

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

KeyRing produces keys deterministically from a shared master secret. Two instances configured with the same masterSecret derive bit-identical keys — the property that lets distributed (multi-WAF) deployments sign and verify each other's challenges and cookies without coordination.

Two key families with different lifetimes:

  • Per-epoch signing key (rotates on rotationInterval). Used for ticket HMAC and PoW MAC. Live window: [currentEpoch - maxLive + 1 ... currentEpoch + clockSkew] Epochs outside the window are rejected, bounding ticket forgery exposure to maxLive * rotationInterval.

  • Long-lived master cookie key (no rotation). Used for AES-GCM cookie sealing. Cookie expiration is enforced by an explicit not_after timestamp inside the sealed envelope, NOT by key eviction — so cookie TTL can be much larger than the ticket window without widening ticket forgery exposure.

func NewKeyRing

func NewKeyRing(masterSecret []byte, rotationInterval time.Duration, maxLive int) (*KeyRing, error)

NewKeyRing constructs a KeyRing. masterSecret must be at least minSecretBytes long (callers should already have validated this via WithMasterSecret); the rotation interval must be at least keyringMinRotation. maxLive defaults to keyringDefaultMaxLive when zero.

func (*KeyRing) Current

func (k *KeyRing) Current() (int64, []byte)

Current returns the epoch and signing key that should be used to sign new outbound material right now.

func (*KeyRing) CurrentEpoch

func (k *KeyRing) CurrentEpoch() int64

CurrentEpoch returns the epoch identifier for the current wall-clock time. Equal across all instances with synchronized clocks.

func (*KeyRing) LiveEpochs

func (k *KeyRing) LiveEpochs() []int64

LiveEpochs returns every epoch currently in the live window, oldest first. Used by callers that need to enumerate the acceptable ticket-signing epochs.

func (*KeyRing) MasterCookieKey

func (k *KeyRing) MasterCookieKey() []byte

MasterCookieKey returns the long-lived AES-key-input for cookie sealing. Does not depend on epoch; same value for the lifetime of the master secret. Cookie expiration is enforced via an explicit not_after timestamp inside the sealed envelope (see crypto.go).

func (*KeyRing) SignKey

func (k *KeyRing) SignKey(epoch int64) ([]byte, bool)

SignKey returns the HMAC key for an epoch if it's within the live window; returns (nil, false) otherwise.

type MismatchReport

type MismatchReport struct {
	Signals []MismatchSignal
}

MismatchReport aggregates the fired mismatch signals for a single fingerprint evaluation. Returned by ComputeMismatchReport; typically accessed through the cached EvaluateMismatches closure in the on_challenge expr env.

func (*MismatchReport) BySeverity

func (r *MismatchReport) BySeverity(sev string) int

BySeverity returns the count of signals at the requested severity level.

func (*MismatchReport) Count

func (r *MismatchReport) Count() int

Count returns the total number of fired signals.

func (*MismatchReport) Empty

func (r *MismatchReport) Empty() bool

Empty reports whether no signal fired.

func (*MismatchReport) Has

func (r *MismatchReport) Has(reason string) bool

Has reports whether a signal with the given reason key is present.

func (*MismatchReport) High

func (r *MismatchReport) High() int

High returns the count of signals tagged with the "high" severity.

func (*MismatchReport) Low

func (r *MismatchReport) Low() int

Low returns the count of signals tagged with the "low" severity.

func (*MismatchReport) Medium

func (r *MismatchReport) Medium() int

Medium returns the count of signals tagged with the "medium" severity.

func (*MismatchReport) Reasons

func (r *MismatchReport) Reasons() []string

Reasons returns the stable-ordered list of fired reason keys.

func (*MismatchReport) String

func (r *MismatchReport) String() string

String renders the report as "reason1(sev),reason2(sev)" for log lines.

type MismatchSignal

type MismatchSignal struct {
	Reason   string
	Severity string
}

MismatchSignal is a single fired reason/severity pair on the report.

type Option

type Option func(*runtimeOptions)

Option configures a ChallengeRuntime at construction time.

func BuildOptions

func BuildOptions(c *Config, parent *log.Entry) ([]Option, error)

BuildOptions translates a (possibly nil) merged Config into the WithXxx Option list for NewChallengeRuntime; unset fields are omitted so the runtime uses its built-in defaults. Returns an error if MasterSecret is set but invalid. parent (may be nil) is the logger the "challenge" sublogger derives from, at the configured log_level or parent's level.

func WithCookieTTL

func WithCookieTTL(ttl time.Duration) Option

WithCookieTTL sets challenge-cookie validity (see Config.CookieTTL); zero/negative is ignored.

func WithCryptoObfuscationPoolSize

func WithCryptoObfuscationPoolSize(n int) Option

WithCryptoObfuscationPoolSize sets the per-epoch sign-key obfuscation pool size (see Config.CryptoObfuscationPoolSize); values below 1 are ignored.

func WithLogger

func WithLogger(logger *log.Entry) Option

func WithMasterSecret

func WithMasterSecret(secret []byte) Option

WithMasterSecret sets the long-lived shared secret (see Config.MasterSecret).

func WithMaxCookieLen

func WithMaxCookieLen(n int) Option

WithMaxCookieLen sets the cookie size ceiling (see Config.MaxCookieSize); zero/negative is ignored, leaving the MaxCookieLen default in effect.

func WithMaxLiveEpochs

func WithMaxLiveEpochs(n int) Option

WithMaxLiveEpochs sets how many past epochs the keyring keeps accepting (see Config.MaxLiveEpochs).

func WithRotationInterval

func WithRotationInterval(d time.Duration) Option

WithRotationInterval sets the per-epoch key rotation period (see Config.KeyRotationInterval).

func WithSpentSetMaxEntries

func WithSpentSetMaxEntries(n int) Option

WithSpentSetMaxEntries caps the replay-protection LRU (see Config.SpentSetMaxEntries); values below 1 are ignored.

Directories

Path Synopsis
js
cmd/bundle command
cmd/initialbundle command
initialbundle is a build-time tool that produces a pre-obfuscated initial challenge bundle, embedded into the Go binary.
initialbundle is a build-time tool that produces a pre-obfuscated initial challenge bundle, embedded into the Go binary.
cmd/obfuscate command

Jump to

Keyboard shortcuts

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