Documentation
¶
Index ¶
Constants ¶
const AlgorithmSHA256LeadingZeros = "sha256-leading-zeros-v1"
AlgorithmSHA256LeadingZeros names the only proof-of-work scheme we issue today: find a solution such that SHA-256(challenge || ":" || solution) has at least `difficulty` leading zero bits. The name travels in the challenge response and clients reject values they don't recognize rather than guessing, which is what makes a future scheme additive instead of breaking.
const DefaultDifficulty = 0
DefaultDifficulty is where the dial sits normally. Zero: the mechanism ships mandatory and the work ships at nothing, so raising the price of an anonymous identity later needs no client release.
const MaxDifficulty = 26
MaxDifficulty is the absolute ceiling on the work we will ever ask a client for. Clients are expected to honour any difficulty up to this and to surface an error above it, so a server-side bug can't wedge an overlay in a hash loop. Enforced at mint time regardless of what the difficulty function returns.
It is a refusal threshold, not a usable setting. 2^26 expected hashes is well over a minute in prism's Python client, and challengeTTL gives the whole handshake 60 seconds — so a difficulty near this ceiling doesn't converge at all: the client solves, gets ErrChallengeExpired (the TTL is checked before the work), fetches another and repeats. The band that actually works is roughly up to 22; anything beyond that needs challengeTTL raised in the same change.
Variables ¶
var ( // ErrInvalidConfig is returned at construction time, never per-request. ErrInvalidConfig = errors.New("invalid proof-of-work configuration") ErrMalformedChallenge = errors.New("malformed challenge") ErrBadSignature = errors.New("bad challenge signature") ErrChallengeExpired = errors.New("challenge expired") ErrIPMismatch = errors.New("challenge was issued to a different ip") ErrUserIDMismatch = errors.New("challenge was issued to a different user id") ErrInsufficientWork = errors.New("solution does not meet the required difficulty") // ErrUnsupportedAlgorithm means we signed it but cannot check it: a newer // revision minted it under a scheme this one doesn't implement. ErrUnsupportedAlgorithm = errors.New("challenge uses an unsupported algorithm") )
Functions ¶
func GenerateSigningKey ¶
GenerateSigningKey returns a fresh base64-encoded signing key. Development only. A key generated at startup dies with the process, which invalidates every outstanding challenge on restart — a 60-second window that a local client just retries through, but not something to run in production, where the key is a secret so that it also survives a revision rollout.
func ParseSigningKeys ¶
ParseSigningKeys decodes base64 signing keys from config. The first key signs every challenge we mint and all of them are accepted on the way back in, so rotation is: prepend the new key, deploy, drop the old one a TTL later. Blank entries are skipped — the config format is newline-delimited and gets edited by hand.
func RejectionReason ¶
RejectionReason maps an error from ParseChallenge or Check to a bounded label safe to use as a metric attribute. A dial with no gauge isn't tunable, and the split by cause is what tells a difficulty change apart from a client bug.
Types ¶
type Challenge ¶
Challenge is a minted proof-of-work challenge, ready to be handed to a client. Value is opaque to the client; everything the client needs in order to solve it is spelled out in the other fields.
type DifficultyFunc ¶
type DifficultyFunc func(DifficultyInput) int
DifficultyFunc is evaluated once per minted challenge. A function rather than a constant on purpose: per-IP issuance volume, blocklist state and global load are all things we'd want to price in without shipping anything to clients.
func BuildDifficultyFunc ¶
func BuildDifficultyFunc(globalFloor int) (DifficultyFunc, error)
BuildDifficultyFunc returns the difficulty policy. Today every caller gets the global floor: the signals worth escalating on want real issuance-per-IP data to calibrate against, and there is no live client producing any yet. They plug in here, and they may only raise the number — BuildIssueChallenge clamps the result to MaxDifficulty.
type DifficultyInput ¶
type DifficultyInput struct {
// IPHash is the sha256 of the caller's IP.
IPHash string
// ClientType is the *normalized* client type (ports.Client.Type), not
// the raw header. It is client-supplied either way, so it may only ever
// raise the cost, never lower it below the global floor.
ClientType string
}
DifficultyInput is everything the difficulty policy may key on. All of it is knowable server-side at challenge time, which is the point — nothing about the dial should need a client release.
type IssueChallenge ¶
IssueChallenge mints a challenge bound to the userId the caller intends to log in as and to their ip hash. clientType must be the *normalized* client type: it is client-supplied, so it may only ever raise the cost, never lower it.
func BuildIssueChallenge ¶
func BuildIssueChallenge(keys [][]byte, difficultyFor DifficultyFunc, nowFunc func() time.Time) (IssueChallenge, error)
BuildIssueChallenge returns the challenge minting half of the scheme.
type ParseChallenge ¶
type ParseChallenge func(challenge string) (SignedChallenge, error)
ParseChallenge recovers a challenge we minted from the blob a client presents. Every check it makes is a pure function of that blob, so a value coming back carries observations that hold however the verdict goes, and a failure means there was nothing to observe. Anything reading the clock or caller input is on SignedChallenge.Check.
func BuildParseChallenge ¶
func BuildParseChallenge(keys [][]byte, nowFunc func() time.Time) (ParseChallenge, error)
BuildParseChallenge returns the parsing half of the verifying side.
type SignedChallenge ¶
type SignedChallenge interface {
// Difficulty is read from the signed payload, never re-derived: the dial
// can move between mint and login, so re-deriving would report a
// difficulty the client was never asked for.
Difficulty() int
// Age is minted-until-now, so it spans both round trips and the client's
// own solve. Negative when our clock stepped backwards, and unbounded
// above once past challengeTTL.
Age() time.Duration
// Check verifies freshness, the bindings and the work. Every failure
// wraps one of the sentinel errors above.
//
// userID must be the raw value that becomes the session's identity_key,
// compared byte for byte against what was minted. Normalizing on one
// side only would reject correct solutions.
Check(solution string, userID string, ipHash string) error
}
SignedChallenge is a challenge signed by one of our keys, naming a scheme we implement. Not a verdict on the caller's solution — that is Check.
An interface only so callers can fake it; nothing outside this package can build one.