Documentation
¶
Overview ¶
Package pow is a proof-of-work challenge: a client proves it spent CPU time before a call that a rate rule found suspicious is let through. It is the auth.RateChallenger of an app that wants a challenge with no third party in the loop (no captcha provider, no user tracking), so an auth.RateRule with Action auth.RateChallenge asks the client for work instead of refusing it.
The scheme is hashcash. The server issues a random challenge (POST /auth/pow/challenge, or Plugin.Challenge from Go) with a difficulty D. The client finds a decimal nonce such that SHA-256 of "<challenge>:<nonce>" starts with at least D zero bits, and sends "<challenge>.<nonce>" in the ProofHeader of the call that was answered 428 auth.challenge_required. A challenge is single use, expires (DefaultChallengeTTL) and is spent only by a proof that carries enough work; Solve does the search in Go, for tests and for a CLI or desktop client.
What it is for: it makes a flood of sign-in guesses cost the sender CPU time per guess instead of nothing, which a human behind a busy shared address barely notices once. What it is not: a bot detector. A determined attacker with a GPU farm pays the cost and carries on, so keep the rate limits and use it where a captcha is not welcome.
Index ¶
- Constants
- Variables
- func Solve(ctx context.Context, ch Challenge) (string, error)
- type Challenge
- type Option
- type Plugin
- func (p *Plugin) Challenge(ctx context.Context) (Challenge, error)
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) PassChallenge(ctx context.Context, c *auth.Call) error
- func (p *Plugin) RateLimits() []auth.RateRule
Constants ¶
const ( // DefaultDifficulty is 20 zero bits: about a million hashes, a fraction of // a second in Go and a second or two in a browser. //ds:def id=pow-defaultdifficulty-2vk9wq6n owner=@khanakia stability=stable DefaultDifficulty = 20 // MaxDifficulty caps Difficulty. 2^32 hashes is beyond a phone and a // browser tab; a higher setting is a lock-out, not a challenge. MaxDifficulty = 32 // DefaultChallengeTTL is how long a challenge can be solved and used. //ds:def id=pow-defaultchallengettl-8nr3xk5m owner=@khanakia stability=stable DefaultChallengeTTL = 5 * time.Minute // MaxChallengeTTL caps ChallengeTTL: an unsolved challenge is a row in the // database until it expires or a sweep removes it. MaxChallengeTTL = time.Hour )
Defaults and bounds.
const AlgorithmSHA256 = "sha256"
AlgorithmSHA256 names the only hash this plugin issues, so a client can refuse a challenge for an algorithm it does not know.
const AnnotationVerified = "pow.verified"
AnnotationVerified is the auth.Call annotation set once the call's proof was accepted: a proof is single use, so a call that two rules challenge (a rate rule and a geo rule) must not spend it twice. The value is "true", never the proof.
const EndpointChallenge auth.EndpointID = "pow.challenge"
EndpointChallenge is the operation that issues a challenge, exported so a rate rule or a hook can name it. A wire name, stable across releases.
const PluginName = "pow"
PluginName is the plugin's permanent identity and route prefix.
const ProofHeader = "X-Pow-Proof"
ProofHeader is the request header the proof travels in.
const PurposeChallenge auth.Purpose = "pow.challenge"
PurposeChallenge is the verification purpose of a challenge. Its Identifier is the difficulty it was issued at, so a proof is judged by the difficulty the challenge carried, whatever the plugin is configured with by then.
Variables ¶
var ( // ErrProofInvalid: the proof is malformed, or does not carry the work the // challenge asked for. ErrProofInvalid = auth.NewError("pow.proof_invalid", http.StatusForbidden, "The proof of work was not accepted.") // ErrChallengeInvalid: the challenge is unknown, expired or already used. // The three are one answer, like auth.ErrTokenInvalid; the client asks for // a new challenge. ErrChallengeInvalid = auth.NewError("pow.challenge_invalid", http.StatusBadRequest, "The challenge has expired. Request a new one.") )
Functions ¶
func Solve ¶
Solve finds the proof for ch: it tries nonces 0, 1, 2, … until the hash has ch.Difficulty leading zero bits, and returns the value for the ProofHeader. It stops with the context's error when ctx ends, so a client can bound the search. It is the reference solver, for tests, a CLI and a desktop app; a browser uses the same loop in script.
Types ¶
type Challenge ¶
type Challenge struct {
// Challenge is the random value to hash, hex text.
Challenge string `json:"challenge"`
// Difficulty is the number of leading zero bits the hash must have.
Difficulty int `json:"difficulty"`
// Algorithm is AlgorithmSHA256.
Algorithm string `json:"algorithm"`
// ExpiresAt is when the challenge stops working.
ExpiresAt time.Time `json:"expiresAt"`
}
Challenge is what the server issues and the client solves.
type Option ¶
type Option func(*Plugin)
Option configures the plugin.
func ChallengeTTL ¶
ChallengeTTL sets how long a challenge works (default DefaultChallengeTTL), at most MaxChallengeTTL.
func Difficulty ¶
Difficulty sets the leading zero bits a proof needs, 1 to MaxDifficulty (default DefaultDifficulty). Each bit doubles the client's expected work and the server's cost stays one hash.
func WithHeader ¶
WithHeader renames the header the proof arrives in (default ProofHeader). It must be a valid HTTP header name.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the proof-of-work challenger. Build it with New.
func (*Plugin) Challenge ¶
Challenge issues a challenge: the typed form of POST /auth/pow/challenge.
func (*Plugin) Operations ¶
Operations lists the challenge route.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) PassChallenge ¶
PassChallenge is auth.RateChallenger. A Go call passes (the app's own code is not the flood); a call with no proof is auth.ErrChallengeRequired (428), so the client knows to fetch a challenge, solve it and send the call again; a proof is checked by verify.
func (*Plugin) RateLimits ¶
RateLimits bounds challenge requests per address.