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
- Variables
- func DifficultyFromLevel(level string) (int, error)
- func KnownReasons() []string
- func ParseConfiguredSecret(value string) ([]byte, error)
- func SeverityFor(reason string) string
- type ChallengeRuntime
- func (c *ChallengeRuntime) Close(ctx context.Context) error
- func (c *ChallengeRuntime) Difficulty() int
- func (c *ChallengeRuntime) GetChallengePage(ctx context.Context, userAgent string, difficulty int) (string, error)
- func (c *ChallengeRuntime) ObfuscateJS(ctx context.Context, inputJS string) (string, error)
- func (c *ChallengeRuntime) SealAllowlistCookie(request *http.Request, reason string, ttlOverride *time.Duration) (*cookie.AppsecCookie, error)
- func (c *ChallengeRuntime) SetDifficulty(level string) error
- func (c *ChallengeRuntime) ValidCookie(ck *http.Cookie, userAgent string) (*CookieData, error)
- func (c *ChallengeRuntime) ValidateChallengeResponse(request *http.Request, body []byte) (*cookie.AppsecCookie, FingerprintData, int, error)
- type Config
- type CookieData
- type CookieEnvelope
- type FingerprintData
- func (f *FingerprintData) AcceptLanguageMismatch(req *http.Request) bool
- func (f *FingerprintData) BotSignalCount() int
- func (f *FingerprintData) BotSignals() []string
- func (f *FingerprintData) CPUCount() int
- func (f *FingerprintData) ComputeMismatchReport(req *http.Request, country string) *MismatchReport
- func (f *FingerprintData) HasAutomationSignal() bool
- func (f *FingerprintData) HasBotSignal() bool
- func (f *FingerprintData) HasHeadlessSignal() bool
- func (f *FingerprintData) HasImpossibleDeviceSignal() bool
- func (f *FingerprintData) HasMismatchSignal() bool
- func (f *FingerprintData) IsBot() bool
- func (f *FingerprintData) IsMobile() bool
- func (f *FingerprintData) Language() string
- func (f *FingerprintData) LogAccepted(logger *log.Entry, level log.Level, clientIP, bouncerIP, msg string, ...)
- func (f *FingerprintData) LogRejected(logger *log.Entry, level log.Level, clientIP, bouncerIP, reason, msg string, ...)
- func (f *FingerprintData) Memory() int
- func (f *FingerprintData) Platform() string
- func (f *FingerprintData) Timezone() string
- func (f *FingerprintData) TimezoneCountryMismatch(country string) bool
- func (f *FingerprintData) ToProto() *pb.FingerprintData
- func (f *FingerprintData) UAMobileMismatch() bool
- func (f *FingerprintData) UnmarshalJSON(data []byte) error
- func (f *FingerprintData) UserAgent() string
- type FingerprintLogVerbosity
- type FlexBool
- type FlexBrandVersions
- type FlexInt
- type KeyRing
- type MismatchReport
- func (r *MismatchReport) BySeverity(sev string) int
- func (r *MismatchReport) Count() int
- func (r *MismatchReport) Empty() bool
- func (r *MismatchReport) Has(reason string) bool
- func (r *MismatchReport) High() int
- func (r *MismatchReport) Low() int
- func (r *MismatchReport) Medium() int
- func (r *MismatchReport) Reasons() []string
- func (r *MismatchReport) String() string
- type MismatchSignal
- type Option
- func BuildOptions(c *Config, parent *log.Entry) ([]Option, error)
- func WithCookieTTL(ttl time.Duration) Option
- func WithCryptoObfuscationPoolSize(n int) Option
- func WithLogger(logger *log.Entry) Option
- func WithMasterSecret(secret []byte) Option
- func WithMaxCookieLen(n int) Option
- func WithMaxLiveEpochs(n int) Option
- func WithRotationInterval(d time.Duration) Option
- func WithSpentSetMaxEntries(n int) Option
Constants ¶
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.
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.
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.
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.
const ChallengeCookieName = "__crowdsec_challenge"
ChallengeCookieName is the name of the sealed cookie carrying the successfully-validated fingerprint between requests.
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).
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.
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 ¶
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.
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") )
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.
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.
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 ¶
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 ¶
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 ¶
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) 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 ¶
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.
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) UnmarshalJSON ¶
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) UnmarshalJSON ¶
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 ¶
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 ¶
Current returns the epoch and signing key that should be used to sign new outbound material right now.
func (*KeyRing) CurrentEpoch ¶
CurrentEpoch returns the epoch identifier for the current wall-clock time. Equal across all instances with synchronized clocks.
func (*KeyRing) LiveEpochs ¶
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 ¶
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).
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 ¶
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 ¶
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 ¶
WithCookieTTL sets challenge-cookie validity (see Config.CookieTTL); zero/negative is ignored.
func WithCryptoObfuscationPoolSize ¶
WithCryptoObfuscationPoolSize sets the per-epoch sign-key obfuscation pool size (see Config.CryptoObfuscationPoolSize); values below 1 are ignored.
func WithLogger ¶
func WithMasterSecret ¶
WithMasterSecret sets the long-lived shared secret (see Config.MasterSecret).
func WithMaxCookieLen ¶
WithMaxCookieLen sets the cookie size ceiling (see Config.MaxCookieSize); zero/negative is ignored, leaving the MaxCookieLen default in effect.
func WithMaxLiveEpochs ¶
WithMaxLiveEpochs sets how many past epochs the keyring keeps accepting (see Config.MaxLiveEpochs).
func WithRotationInterval ¶
WithRotationInterval sets the per-epoch key rotation period (see Config.KeyRotationInterval).
func WithSpentSetMaxEntries ¶
WithSpentSetMaxEntries caps the replay-protection LRU (see Config.SpentSetMaxEntries); values below 1 are ignored.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
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
|
|