gocap

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 19 Imported by: 0

README

gocap

English | 中文

Go 语言实现的 CAP 服务端,一个基于工作量证明(PoW)的验证码替代方案。支持可插拔存储、多协议挑战格式(含 RSW 时间锁谜题)和客户端浏览器检测。

特性

  • PoW 挑战 — FNV-1a + xorshift 派生盐值与目标前缀,无状态验证
  • 多协议格式 (Format-2) — 支持 sha256-pow / rsw / instrumentation 协议组合
  • RSW 时间锁谜题 — 基于重复平方运算的量子抵抗型挑战
  • 客户端检测 — 浏览器行为分析参数生成与结果验证(AES-256-GCM 加密)
  • JWT HS256 — 内置签名与验证,可选自定义签发
  • 重放保护 — 通过 ConsumeNonce 回调支持一次性令牌
  • 可插拔存储 — 统一的 Storage 接口,内置内存实现,可接入 Redis/PostgreSQL 等
  • 哨兵错误 — 所有失败原因均可通过 errors.Is 判断

安装

go get github.com/gomodb/gocap

最低 Go 版本:1.25

快速开始

Format-1:基本 PoW 挑战
package main

import (
 "context"
 "errors"
 "log"

 "github.com/gomodb/gocap"
)

func main() {
 secret := []byte("32-byte-high-entropy-secret-for-signing!")
 ctx := context.Background()

 cap, err := gocap.NewCap(gocap.CapOptions{
  Secret:              secret,
  Storage:             gocap.NewMemoryStorage(),
  ChallengeCount:      1,
  ChallengeSize:       8,
  ChallengeDifficulty: 1,
 })
 if err != nil {
  log.Fatal(err)
 }

 // 服务端生成挑战
 challenge, err := cap.CreateChallenge(ctx)
 if err != nil {
  log.Fatal(err)
 }
 fmt.Printf("Challenge: c=%d, s=%d, d=%d\n",
  challenge.Challenge.C,
  challenge.Challenge.S,
  challenge.Challenge.D,
 )
 fmt.Printf("Token: %s\n", challenge.Token)

 // ── 客户端侧:求解 PoW ──
 // 客户端根据 challenge.Challenge 中的 c、s、d 参数暴力搜索 nonce,
 // 使 sha256(salt || nonce) 的前 d 个 hex 字符等于 target。
 // 参考实现见 https://github.com/tiagozip/cap 的前端 widget。

 // 服务端验证
 result, err := cap.RedeemChallenge(ctx, gocap.ValidateBody{
  Token:     challenge.Token,
  Solutions: solutions,
 })
 if err != nil {
  log.Fatal(err)
 }
 if !result.Success {
  log.Fatalf("验证失败: %s", result.Reason)
 }
 fmt.Printf("验证令牌: %s\n", result.Token)

 // 验证令牌有效性
 vr := cap.ValidateToken(ctx, result.Token)
 if errors.Is(vr, gocap.ErrTokenNotFound) {
  log.Fatal("令牌不存在或已过期")
 }
}
Format-2:多协议挑战
package main

import (
 "context"
 "log"

 "github.com/gomodb/gocap"
)

func main() {
 secret := []byte("32-byte-high-entropy-secret-for-signing!")
 ctx := context.Background()

 // 生成 RSW 密钥对(生产环境应在启动时生成一次并持久化)
 kp, err := gocap.GenerateRswKeypair(2048)
 if err != nil {
  log.Fatal(err)
 }

 cap, err := gocap.NewCap(gocap.CapOptions{
  Secret:    secret,
  Storage:   gocap.NewMemoryStorage(),
  Format:    2,
  Protocols: []string{"sha256-pow", "rsw"},
  Keypair:   kp,
 })
 if err != nil {
  log.Fatal(err)
 }

 // 生成多协议挑战
 challenge, err := cap.CreateChallenge(ctx)
 if err != nil {
  log.Fatal(err)
 }
 _ = challenge
}
错误判断
result, err := cap.RedeemChallenge(ctx, proof)
if err != nil {
 // 系统级错误(配置、存储等)
}
if !result.Success {
 switch {
 case errors.Is(result, gocap.ErrExpired):
  // 挑战已过期
 case errors.Is(result, gocap.ErrInvalidSolution):
  // PoW 解答错误
 case errors.Is(result, gocap.ErrInstrMissing):
  // 缺少客户端检测数据
 case errors.Is(result, gocap.ErrAlreadyRedeemed):
  // 令牌已被消费(重放攻击)
 }
}

API 总览

Cap 实例
方法 说明
NewCap(opts) 创建实例,secret 至少 16 字节
CreateChallenge(ctx) 生成挑战,返回 token + 参数
RedeemChallenge(ctx, body) 验证 PoW 解,签发验证令牌
ValidateToken(ctx, token) 检查验证令牌是否存在且未过期
哨兵错误(可通过 errors.Is 判断)
错误 触发条件
ErrMissingToken 挑战令牌为空
ErrMissingSolutions Solutions 数组为 nil
ErrInvalidToken JWT 签名无效或载荷损坏
ErrScopeMismatch scope 不匹配
ErrExpired 挑战或验证令牌过期
ErrInvalidSolutions 解的数量不匹配或包含负数
ErrInvalidSolution PoW 解不正确
ErrAlreadyRedeemed 令牌已被消费
ErrNonceStoreError ConsumeNonce 回调失败
ErrStorageNotConfigured 未配置 Storage
ErrCleanupError 存储清理失败
ErrStorageError 存储读写失败
ErrTokenNotFound 验证令牌不存在
ErrTokenExpired 验证令牌已过期
ErrInstrCorrupted 检测元数据损坏
ErrInstrExpired 检测元数据过期
ErrInstrAutomatedBrowser 检测到自动化浏览器
ErrInstrTimeout 检测超时
ErrInstrFailed 检测验证失败
ErrInstrMissing 缺少检测结果
存储接口
type Storage interface {
 SetChallengeToken(ctx, token, expiresAt) error
 GetChallengeTokenExpiry(ctx, token) (time.Time, bool, error)
 SetValidationToken(ctx, token, expiresAt) error
 GetValidationTokenExpiry(ctx, token) (time.Time, bool, error)
 CleanupExpired(ctx) error
}

内置 MemoryStorage 适用于开发测试。生产环境请实现 Storage 接口接入 Redis、PostgreSQL 等后端,参见 examples/storage/

Format-2 协议参考

协议 说明 求解方式
sha256-pow 标准 SHA-256 前缀匹配 PoW 客户端暴力搜索 nonce
rsw 基于重复平方的时间锁谜题 客户端执行 t 次连续平方运算
instrumentation 浏览器行为分析 客户端执行检测脚本并返回结果

其他 Go 实现

运行测试

go test ./... -v
# 跳过耗时测试(如 2048 位 RSA 密钥生成)
go test ./... -short

安全注意事项

  • Secret:至少 16 字节,建议 32+ 字节高强度随机值。生成:openssl rand -hex 32
  • 重放保护:通过 ConsumeNonce 回调实现(如 Redis SETNX)
  • RSW 密钥GenerateRswKeypair(2048) 约耗时数百毫秒,启动时生成一次并持久化
  • 存储MemoryStorage 重启后数据丢失,生产环境请使用持久化后端

Documentation

Overview

Package gocap provides a server-side implementation of Cap, a proof-of-work based CAPTCHA alternative. It generates cryptographic challenges that clients must solve, then verifies the solutions and issues validation tokens.

Quick Start

secret := []byte("32-byte-high-entropy-secret-for-signing")
store := gocap.NewMemoryStorage()

cap, err := gocap.NewCap(gocap.CapOptions{
    Secret:  secret,
    Storage: store,
})

// Generate a challenge
challenge, err := cap.CreateChallenge(ctx)
// Send challenge.Token, challenge.Challenge (c, s, d) to the client

// Verify the client's PoW solutions
result, err := cap.RedeemChallenge(ctx, gocap.ValidateBody{
    Token:     challenge.Token,
    Solutions: clientSolutions, // []int64 from the client
})
if errors.Is(result, gocap.ErrInvalidSolution) {
    // PoW verification failed
}

// Validate the issued token
vr := cap.ValidateToken(ctx, result.Token)
if errors.Is(vr, gocap.ErrTokenNotFound) {
    // Token not found or expired
}

Challenge Formats

Format-1 (default): A stateless SHA-256 proof-of-work challenge. The server derives salt and target values from the token itself using FNV-1a and xorshift PRNG, so no server-side storage is required for challenge verification.

Format-2 (opt-in): A multi-protocol challenge framework supporting:

  • sha256-pow: Standard PoW with random salt per item
  • rsw: Time-lock puzzles based on repeated squaring (quantum-resistant)
  • instrumentation: Client-side browser integrity checks

Sentinel Errors

All validation failures are returned as ValidateResult values that implement the error interface. Use errors.Is() to check specific failure reasons:

if errors.Is(result, gocap.ErrExpired) { /* challenge expired */ }
if errors.Is(result, gocap.ErrInvalidSolution) { /* PoW wrong */ }

Storage

The Storage interface allows plugging in custom backends for challenge and validation token persistence. A built-in MemoryStorage is provided for development and testing. For production, implement the Storage interface with Redis, PostgreSQL, or any other backend.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrMissingToken is returned when the challenge token is empty.
	ErrMissingToken = errors.New("missing_token")
	// ErrMissingSolutions is returned when the solutions array is nil.
	ErrMissingSolutions = errors.New("missing_solutions")
	// ErrInvalidToken is returned when the JWT is malformed, tampered, or
	// has an invalid signature.
	ErrInvalidToken = errors.New("invalid_token")
	// ErrScopeMismatch is returned when the requested scope doesn't match
	// the scope embedded in the challenge token.
	ErrScopeMismatch = errors.New("scope_mismatch")
	// ErrExpired is returned when the challenge or validation token has
	// exceeded its time-to-live.
	ErrExpired = errors.New("expired")
	// ErrInvalidSolutions is returned when the number of solutions doesn't
	// match the challenge count or a solution is negative.
	ErrInvalidSolutions = errors.New("invalid_solutions")
	// ErrInvalidSolution is returned when a PoW solution doesn't produce
	// a hash with the required target prefix.
	ErrInvalidSolution = errors.New("invalid_solution")
	// ErrAlreadyRedeemed is returned by the ConsumeNonce callback when the
	// challenge token signature has already been used.
	ErrAlreadyRedeemed = errors.New("already_redeemed")
	// ErrNonceStoreError is returned when the ConsumeNonce callback itself
	// fails (e.g. a storage backend error).
	ErrNonceStoreError = errors.New("nonce_store_error")
	// ErrStorageNotConfigured is returned by ValidateToken when no Storage
	// was provided to the Cap instance.
	ErrStorageNotConfigured = errors.New("storage_not_configured")
	// ErrCleanupError is returned when the storage's CleanupExpired call fails.
	ErrCleanupError = errors.New("cleanup_error")
	// ErrStorageError is returned when a storage Get/Set operation fails.
	ErrStorageError = errors.New("storage_error")
	// ErrTokenNotFound is returned when the validation token is not found
	// in storage.
	ErrTokenNotFound = errors.New("token_not_found")
	// ErrTokenExpired is returned when the validation token exists but has
	// passed its expiration time.
	ErrTokenExpired = errors.New("token_expired")
	// ErrInstrCorrupted is returned when the instrumentation metadata
	// cannot be decrypted or parsed.
	ErrInstrCorrupted = errors.New("instr_corrupted")
	// ErrInstrExpired is returned when the instrumentation metadata has
	// exceeded its expiration time.
	ErrInstrExpired = errors.New("instr_expired")
	// ErrInstrAutomatedBrowser is returned when the instrumentation
	// detected an automated browser and BlockAutomatedBrowsers is enabled.
	ErrInstrAutomatedBrowser = errors.New("instr_automated_browser")
	// ErrInstrTimeout is returned when the instrumentation timed out.
	ErrInstrTimeout = errors.New("instr_timeout")
	// ErrInstrFailed is returned when the instrumentation result
	// verification fails unexpectedly.
	ErrInstrFailed = errors.New("instr_failed")
	// ErrInstrMissing is returned when instrumentation is required but
	// no instrumentation result was provided.
	ErrInstrMissing = errors.New("instr_missing")
	// ErrShortSecret is returned by NewCap when Secret is shorter than 16 bytes.
	ErrShortSecret = errors.New("secret must be at least 16 bytes")
	// ErrFormat2NoProtocols is returned by NewCap when Format is 2 but no
	// protocols are specified.
	ErrFormat2NoProtocols = errors.New("format-2 requires at least one protocol")
	// ErrRSWBitsMustBeEven is returned by GenerateRswKeypair when bits is odd.
	ErrRSWBitsMustBeEven = errors.New("rsw bits must be even")
	// ErrRSWKeypairRequired is returned when format-2 rsw protocol is used
	// without providing a keypair.
	ErrRSWKeypairRequired = errors.New("format-2 rsw requires opts.Keypair")
)

Sentinel errors for challenge validation and token verification. Use errors.Is(result, ErrXXX) to check the failure reason.

Functions

func VerifyInstrumentationResult

func VerifyInstrumentationResult(
	meta *InstrumentationMeta,
	payload *InstrumentationResult,
) (bool, string)

VerifyInstrumentationResult checks whether the client's instrumentation response is valid. It verifies that the ID matches and that each variable in the client's state has the expected value.

func VerifyRswSolution

func VerifyRswSolution(expectedYHex, claimedYHex string) bool

VerifyRswSolution verifies that the claimed y solution matches the expected y. Comparison is case-insensitive and ignores leading zeros and optional 0x prefix.

Types

type Cap

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

Cap is an instance-based API for creating and redeeming challenges. Create one via NewCap and reuse it across requests.

func NewCap

func NewCap(opts CapOptions) (*Cap, error)

NewCap creates a Cap instance with the given options. Secret must be at least 16 bytes. When Format is 2, at least one protocol must be specified in Protocols.

func (*Cap) CreateChallenge

func (c *Cap) CreateChallenge(ctx context.Context) (ChallengeResult, error)

CreateChallenge creates challenge parameters and a signed challenge token. When Format is 2, it returns a multi-protocol challenge; otherwise a format-1 PoW challenge.

func (*Cap) RedeemChallenge

func (c *Cap) RedeemChallenge(ctx context.Context, proof ValidateBody) (ValidateResult, error)

RedeemChallenge verifies PoW solutions and returns a validation result.

On success (result.Success == true), result.Token and result.Expires are set. The token can later be verified with ValidateToken.

On validation failure (result.Success == false), result.Reason describes the error and errors.Is can identify the specific reason.

Internal errors (storage, config) are returned as the error value.

func (*Cap) ValidateToken

func (c *Cap) ValidateToken(ctx context.Context, token string) ValidateResult

ValidateToken reports whether a validation token exists and is not expired. Returns a ValidateResult that can be checked with errors.Is:

vr := cap.ValidateToken(ctx, token)
if errors.Is(vr, ErrTokenNotFound) { ... }

type CapOptions

type CapOptions struct {
	// Secret is used to sign and verify challenge tokens via HMAC-SHA256.
	// Must be at least 16 bytes; 32+ recommended.
	Secret []byte
	// Storage persists challenge and validation tokens. If nil, token
	// verification via ValidateToken will fail with storage_not_configured.
	Storage Storage
	// ChallengeCount sets the number of PoW items (format-1, default 50).
	ChallengeCount int
	// ChallengeSize sets the salt length in hex characters (format-1,
	// default 32).
	ChallengeSize int
	// ChallengeDifficulty sets the target prefix hex length (format-1,
	// default 4).
	ChallengeDifficulty int
	// ChallengeTTL controls challenge token lifetime (default 10 minutes).
	ChallengeTTL time.Duration
	// Scope binds the challenge to a logical site or action.
	Scope string
	// Extra is embedded in the challenge token payload as the "x" field.
	Extra map[string]any
	// TokenTTL controls validation token lifetime (default 20 minutes).
	TokenTTL time.Duration
	// ConsumeNonce provides replay protection. See ValidateOptions.
	ConsumeNonce func(ctx context.Context, signatureHex string, ttl time.Duration) (bool, error)
	// SignToken overrides the default validation token format. See
	// ValidateOptions.
	SignToken func(data RedeemTokenData) (string, error)
	// Format selects the challenge format. 0 or 1 for format-1 (default),
	// 2 for the multi-protocol format-2.
	Format int
	// Protocols lists the protocols for format-2 challenges. Required when
	// Format is 2.
	Protocols []string
	// Keypair is the RSA-style keypair required for the "rsw" protocol.
	Keypair *RswKeypair
	// RSWT is the sequential squaring count for RSW puzzles (default 75000).
	RSWT int
	// Instrumentation enables format-1 client-side browser checks.
	Instrumentation bool
	// InstrumentationOpts fine-tunes instrumentation behaviour.
	InstrumentationOpts *InstrumentationOptions
}

CapOptions configures a Cap instance. All fields are optional except Secret, which must be at least 16 bytes.

type ChallengeResult

type ChallengeResult struct {
	// Challenge contains the format-1 PoW parameters (omitted for format-2).
	Challenge ChallengeSpec `json:"challenge"`
	// Token is the signed JWT carrying the challenge parameters.
	Token string `json:"token"`
	// Expires is the challenge expiration time in Unix milliseconds.
	Expires int64 `json:"expires"`
	// Instrumentation is an opaque blob for client-side browser checks
	// (format-1 only, base64-encoded deflated JS in the JS reference impl).
	Instrumentation string `json:"instrumentation,omitzero"`
	// Format is 2 when the result uses the multi-protocol format, 0 or
	// omitted for format-1.
	Format int `json:"format,omitzero"`
	// Challenges is the protocol-specific challenge list (format-2 only).
	Challenges []V2Challenge `json:"challenges,omitzero"`
}

ChallengeResult is the challenge payload returned by CreateChallenge.

type ChallengeSpec

type ChallengeSpec struct {
	// C is the number of PoW items the client must solve.
	C int `json:"c"`
	// S is the salt length in hex characters.
	S int `json:"s"`
	// D is the target prefix difficulty in hex characters.
	D int `json:"d"`
}

ChallengeSpec describes the format-1 PoW parameters sent to the client.

type GenerateOptions

type GenerateOptions struct {
	// ChallengeCount sets the number of PoW items (default 50).
	ChallengeCount int
	// ChallengeSize sets the salt length in hex characters (default 32).
	ChallengeSize int
	// ChallengeDifficulty sets the target prefix length in hex characters
	// (default 4).
	ChallengeDifficulty int
	// Expires sets the challenge lifetime (default 10 minutes).
	Expires time.Duration
	// Scope binds the challenge to a logical site or action.
	Scope string
	// Extra is embedded in the challenge token payload under key "x".
	Extra map[string]any
	// Storage persists challenge and validation tokens.
	Storage Storage
	// Instrumentation enables format-1 client-side browser checks when true.
	Instrumentation bool
	// InstrumentationOpts fine-tunes instrumentation behaviour.
	InstrumentationOpts *InstrumentationOptions
	// Format selects the challenge format. 0 or 1 for format-1 (default),
	// 2 for the multi-protocol format-2.
	Format int
	// Protocols lists the protocols to include in a format-2 challenge.
	// Valid values: "sha256-pow", "rsw", "instrumentation".
	Protocols []string
	// Keypair is the RSA-style keypair needed for the "rsw" protocol.
	Keypair *RswKeypair
	// RSWT is the number of sequential squarings for RSW puzzles
	// (default 75000).
	RSWT int
}

GenerateOptions configures challenge generation. Most fields map directly to CapOptions fields. Zero values use defaults.

type InstrPayload

type InstrPayload struct {
	// I is the instrumentation challenge ID.
	I string `json:"i"`
	// State maps variable names to their computed values.
	State map[string]int `json:"state"`
	// Ts is the client timestamp in Unix milliseconds.
	Ts int64 `json:"ts,omitzero"`
}

InstrPayload is the client-side instrumentation result payload.

type InstrumentationMeta

type InstrumentationMeta struct {
	ID                     string   `json:"id"`
	ExpectedVals           []int    `json:"expectedVals"`
	Vars                   []string `json:"vars"`
	BlockAutomatedBrowsers bool     `json:"blockAutomatedBrowsers"`
	Expires                int64    `json:"expires"`
}

InstrumentationMeta holds the metadata needed to verify a client-side instrumentation result. It is encrypted and embedded in the JWT payload when instrumentation is enabled.

func GenerateInstrumentation

func GenerateInstrumentation(opts InstrumentationOptions) (*InstrumentationMeta, string, error)

GenerateInstrumentation generates random instrumentation parameters. It returns the metadata (to be encrypted and embedded in the JWT) and a JSON blob describing the parameters for the client-side widget.

type InstrumentationOptions

type InstrumentationOptions struct {
	// BlockAutomatedBrowsers, when true, causes the server to reject
	// requests where the instrumentation was blocked.
	BlockAutomatedBrowsers bool
	// ObfuscationLevel controls the complexity of client-side code
	// obfuscation (1-10, default 3). Higher values produce more
	// obfuscated but larger scripts.
	ObfuscationLevel int
	// TTLMs is the instrumentation challenge lifetime in milliseconds.
	// Defaults to 5 minutes when not set.
	TTLMs int64
}

InstrumentationOptions configures instrumentation generation behaviour.

type InstrumentationResult

type InstrumentationResult struct {
	// I is the instrumentation challenge ID, matching InstrumentationMeta.ID.
	I string `json:"i"`
	// State maps each variable name to its computed client-side value.
	State map[string]int `json:"state"`
	// Ts is the client timestamp in Unix milliseconds.
	Ts int64 `json:"ts,omitzero"`
}

InstrumentationResult is the client-side instrumentation result payload, submitted back to the server for verification.

type MemoryStorage

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

MemoryStorage is an in-memory Storage implementation backed by maps. Concurrent reads use RLock; writes and cleanup use an exclusive Lock. Suitable for development and testing; for production, use a persistent backend via the Storage interface.

func NewMemoryStorage

func NewMemoryStorage() *MemoryStorage

NewMemoryStorage creates a new in-memory Storage implementation.

func (*MemoryStorage) CleanupExpired

func (s *MemoryStorage) CleanupExpired(_ context.Context) error

CleanupExpired removes expired entries from the in-memory store.

func (*MemoryStorage) GetChallengeTokenExpiry

func (s *MemoryStorage) GetChallengeTokenExpiry(
	_ context.Context,
	token string,
) (time.Time, bool, error)

GetChallengeTokenExpiry returns a stored challenge token's expiration time.

func (*MemoryStorage) GetValidationTokenExpiry

func (s *MemoryStorage) GetValidationTokenExpiry(
	_ context.Context,
	token string,
) (time.Time, bool, error)

GetValidationTokenExpiry returns a stored validation token's expiration time.

func (*MemoryStorage) SetChallengeToken

func (s *MemoryStorage) SetChallengeToken(
	_ context.Context,
	token string,
	expiresAt time.Time,
) error

SetChallengeToken stores a challenge token in memory.

func (*MemoryStorage) SetValidationToken

func (s *MemoryStorage) SetValidationToken(
	_ context.Context,
	token string,
	expiresAt time.Time,
) error

SetValidationToken stores a validation token in memory.

type RedeemTokenData

type RedeemTokenData struct {
	// Scope is the original challenge scope, or nil if unset.
	Scope *string `json:"scope"`
	// Expires is the redeem token expiration time in Unix milliseconds.
	Expires int64 `json:"expires"`
	// Iat is the original challenge issue-at time in Unix milliseconds.
	Iat int64 `json:"iat,omitzero"`
}

RedeemTokenData contains the data passed to the SignToken callback.

type RswKeypair

type RswKeypair struct {
	N    *big.Int
	P    *big.Int
	Q    *big.Int
	Bits int
}

RswKeypair holds the RSA-style modulus and its prime factors for RSW time-lock puzzles. Generate once at startup with GenerateRswKeypair and reuse across requests. Keep the private factors (P, Q) secret.

func DeserializeRswKeypair

func DeserializeRswKeypair(s *SerializedRswKeypair) (*RswKeypair, error)

DeserializeRswKeypair restores a keypair from its serialized form. Returns an error if the serialized data is nil or contains invalid big integer strings.

func GenerateRswKeypair

func GenerateRswKeypair(bits ...int) (*RswKeypair, error)

GenerateRswKeypair generates an RSA-style keypair with the given bit size for use with RSW time-lock puzzles. bits must be even; default is 2048. Generation is expensive (hundreds of milliseconds to seconds); generate once at startup and reuse.

type RswMintResult

type RswMintResult struct {
	X_hex string `json:"x_hex"`
	Y_hex string `json:"y_hex"`
}

RswMintResult is the output of a single RSW mint operation. X_hex is the challenge value sent to the client; Y_hex is the expected solution kept server-side.

type RswMinter

type RswMinter struct {
	N            *big.Int
	T            int
	G            *big.Int
	H            *big.Int
	ModulusBytes int
	N_hex        string
	G_hex        string
	H_hex        string
	// contains filtered or unexported fields
}

RswMinter creates RSW time-lock puzzle challenges using repeated squaring. Build one with BuildRswMinter and reuse it for multiple Mint calls.

func BuildRswMinter

func BuildRswMinter(args struct {
	N *big.Int
	P *big.Int
	Q *big.Int
	T int
	G *big.Int
},
) (*RswMinter, error)

BuildRswMinter builds a minter that creates RSW puzzles using the given keypair and squaring count t. The minter uses CRT-optimized modular exponentiation for efficient puzzle generation.

func (*RswMinter) Mint

func (m *RswMinter) Mint() (*RswMintResult, error)

Mint creates a new RSW puzzle and returns the challenge x_hex and expected solution y_hex. The caller sends x_hex to the client and keeps y_hex server-side for later verification.

type SerializedRswKeypair

type SerializedRswKeypair struct {
	N    string `json:"N"`
	P    string `json:"p"`
	Q    string `json:"q"`
	Bits int    `json:"bits,omitzero"`
}

SerializedRswKeypair is a JSON-safe representation of RswKeypair, suitable for storing in environment variables, config files, or databases.

func SerializeRswKeypair

func SerializeRswKeypair(kp *RswKeypair) *SerializedRswKeypair

SerializeRswKeypair serializes a keypair for JSON storage or transmission. The returned struct uses decimal string encoding for big integers.

type Storage

type Storage interface {
	// SetChallengeToken stores a challenge token with its expiration time.
	SetChallengeToken(ctx context.Context, token string, expiresAt time.Time) error

	// GetChallengeTokenExpiry returns the expiration time for a stored
	// challenge token. The bool indicates whether the token was found.
	GetChallengeTokenExpiry(ctx context.Context, token string) (time.Time, bool, error)

	// SetValidationToken stores a validation token with its expiration time.
	SetValidationToken(ctx context.Context, token string, expiresAt time.Time) error

	// GetValidationTokenExpiry returns the expiration time for a stored
	// validation token. The bool indicates whether the token was found.
	GetValidationTokenExpiry(ctx context.Context, token string) (time.Time, bool, error)

	// CleanupExpired removes expired challenge and validation tokens.
	CleanupExpired(ctx context.Context) error
}

Storage is the persistence interface for challenge and validation tokens. Implementations must be safe for concurrent use.

All methods receive a context.Context for cancellation and tracing. Expired entries should be removed lazily via CleanupExpired.

type V2Challenge

type V2Challenge struct {
	// Protocol identifies the challenge type: "sha256-pow", "rsw", or
	// "instrumentation".
	Protocol string `json:"protocol"`
	// Payload carries protocol-specific parameters (salt/target for PoW,
	// N/x/t for RSW, blob for instrumentation).
	Payload map[string]any `json:"payload"`
}

V2Challenge represents a single protocol-specific challenge in format-2.

type V2Solution

type V2Solution struct {
	// Protocol identifies the challenge type this solution is for: must
	// match the corresponding V2Challenge.Protocol.
	Protocol string `json:"protocol"`
	// Nonce is the PoW solution nonce for "sha256-pow" protocol.
	Nonce any `json:"nonce,omitzero"`
	// Y is the RSW solution for the "rsw" protocol.
	Y string `json:"y,omitzero"`
	// Blocked indicates the instrumentation was blocked. Set for
	// "instrumentation" protocol as an alternative to Instr/Timeout.
	Blocked *bool `json:"blocked,omitzero"`
	// Timeout indicates the instrumentation timed out. Set for
	// "instrumentation" protocol as an alternative to Instr/Blocked.
	Timeout *bool `json:"timeout,omitzero"`
	// Instr carries the full instrumentation result for "instrumentation"
	// protocol instead of using Blocked/Timeout shortcuts.
	Instr any `json:"instr,omitzero"`
}

V2Solution is a protocol-specific solution for format-2. Only the fields relevant to the protocol need to be set.

type ValidateBody

type ValidateBody struct {
	// Token is the challenge token issued by CreateChallenge.
	Token string `json:"token"`
	// Solutions contains the PoW nonces for format-1 challenges.
	Solutions []int64 `json:"solutions"`
	// V2Sol contains protocol-specific solutions for format-2 challenges.
	V2Sol []V2Solution `json:"v2Sol,omitzero"`
	// Instr carries the instrumentation result payload (format-1).
	Instr *InstrPayload `json:"instr,omitzero"`
	// Blocked reports that instrumentation determined the page was blocked.
	Blocked bool `json:"instr_blocked,omitzero"`
	// Timeout reports that instrumentation timed out.
	Timeout bool `json:"instr_timeout,omitzero"`
}

ValidateBody is the request body for RedeemChallenge.

type ValidateOptions

type ValidateOptions struct {
	// Scope must match the original challenge scope when set.
	Scope string
	// TokenTTL controls the lifetime of the issued validation token
	// (default 20 minutes).
	TokenTTL time.Duration
	// Storage persists the issued validation token for later verification.
	Storage Storage
	// ConsumeNonce provides replay protection. The callback receives the
	// hex-encoded HMAC signature of the challenge token and its remaining
	// TTL. Return true to claim the nonce (first use), false for replay.
	ConsumeNonce func(ctx context.Context, signatureHex string, ttl time.Duration) (bool, error)
	// SignToken overrides the default validation token format. If nil, a
	// random id:verToken format is used. The callback receives the scope,
	// expiration, and original issue time.
	SignToken func(data RedeemTokenData) (string, error)
}

ValidateOptions configures challenge validation behaviour.

type ValidateResult

type ValidateResult struct {
	// Success reports whether validation passed.
	Success bool `json:"success"`
	// Reason describes the failure reason when Success is false. It matches
	// one of the ErrXXX sentinel error strings.
	Reason string `json:"reason,omitzero"`
	// ErrorStr carries a lower-level error message (e.g. a storage error).
	ErrorStr string `json:"error,omitzero"`
	// InstrErr is true when the failure is instrumentation-related.
	InstrErr bool `json:"instr_error,omitzero"`
	// Token is the issued validation token on success.
	Token string `json:"token,omitzero"`
	// TokenKey is an alternative lookup key for the default token format.
	// Clients may store this instead of the full token for privacy.
	TokenKey *string `json:"tokenKey,omitzero"`
	// Expires is the validation token expiration in Unix milliseconds.
	Expires int64 `json:"expires,omitzero"`
	// Scope is the validated scope, if any.
	Scope *string `json:"scope"`
	// Iat is the original challenge issue-at time in Unix milliseconds.
	Iat *int64 `json:"iat,omitzero"`
	// contains filtered or unexported fields
}

ValidateResult is the outcome of RedeemChallenge or ValidateToken.

It implements the error interface, so callers can use errors.Is/As:

if errors.Is(result, ErrExpired) { ... }

When Success is false, Error() returns the Reason string and Unwrap() returns the corresponding sentinel error.

func (ValidateResult) Error

func (r ValidateResult) Error() string

Error implements the error interface. It returns the Reason string, or "success" when the validation passed.

func (ValidateResult) Unwrap

func (r ValidateResult) Unwrap() error

Unwrap returns the sentinel error, enabling errors.Is(r, ErrExpired) etc.

Jump to

Keyboard shortcuts

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