Documentation
¶
Overview ¶
Package vkeystore resolves konareef circuit verification keys (vkeys) for the konareef Go verifier.
Two-tier resolution model per PRD 4 § 4.6 (C.6):
- Tier 1 (primary, byte source): HTTPS GET to https://paygate-zk.{publisherDomain}/.well-known/circuits/{circuit_id}/vkey where publisherDomain is the publisher BASE domain (e.g. `example.com`). The `paygate-zk.` host prefix is added by the Tier-1 backend itself; callers MUST NOT pre-pend it.
- Tier 2 (best-effort v1; HASH-ONLY cross-check, NEVER a byte fallback): on-chain anchor lookup. Yields the committed `vkey_sha256`, not the vkey bytes — used to cross-validate a vkey obtained from Tier 1. Cannot supply vkey bytes when Tier 1 fails.
Every fetched vkey is hashed and compared against the caller-supplied pin (`ResolveRequest.VkeySha256`). A mismatch yields ErrCircuitPinMismatch and the vkey bytes MUST NOT be used. If Tier 1 fails AND there is no fresh cache hit, the resolver yields ErrVkeyUnavailable regardless of whether a Tier-2 backend is configured (no anchor-only byte fallback).
Local cache layout (mirrors P1.7.1 storage pattern):
${KONAREEF_STATE_DIR:-~/.konareef}/vkeys/{circuit_id}/{vkey_sha256}/
vkey.bin -- the verified vkey bytes
meta.json -- {circuit_id, vkey_sha256, source, resolved_at}
Default TTL is 24h, configurable via Resolver.CacheTTL.
Index ¶
- Variables
- func Sha256Hex(body []byte) string
- func ValidateCircuitID(id string) error
- func ValidatePublisherDomain(domain string) error
- func ValidateVkeySha256(pin string) error
- func VerifyPin(want string, body []byte) error
- type AnchorLookupFunc
- type LocalCache
- type ResolveRequest
- type Resolver
- type Source
- type Tier1HTTPSBackend
- type Tier2AnchorBackend
- type Vkey
Constants ¶
This section is empty.
Variables ¶
var ErrAnchorNotFound = errors.New("vkeystore: on-chain anchor not found")
ErrAnchorNotFound means no on-chain anchor exists for this circuit_id. Distinct from a network / indexer failure, which is surfaced verbatim so the Resolver can treat "no anchor exists yet" vs "we could not reach the indexer" differently.
IMPORTANT: this is INTERNAL to the vkeystore package. The Resolver translates "no anchor present" + "no Tier-1 success" + "no cache" into ErrVkeyUnavailable at the package boundary.
var ErrCircuitIDInvalid = errors.New("ERR_CIRCUIT_ID_INVALID")
ErrCircuitIDInvalid means the supplied circuit_id failed the safe-token allow-list (`^[A-Za-z0-9._-]+$`, no `.` / `..` segments, no path separators). Inputs that fail this check MUST NOT be used to build cache paths. Surface code: ERR_CIRCUIT_ID_INVALID.
var ErrCircuitPinMismatch = errors.New("ERR_CIRCUIT_PIN_MISMATCH")
ErrCircuitPinMismatch means a vkey was obtained but SHA-256(vkey) does not match the committed vkey_sha256. The verifier MUST NOT use the vkey. Surface code: ERR_CIRCUIT_PIN_MISMATCH. Non-retryable; requires diagnosis.
var ErrDomainAlreadyPrefixed = errors.New("ERR_DOMAIN_ALREADY_PREFIXED")
ErrDomainAlreadyPrefixed means the supplied PublisherDomain already begins with `paygate-zk.` — caller likely passed the full service host instead of the publisher base domain. The Tier-1 backend would otherwise build a doubled host (`paygate-zk.paygate-zk.example.com`). Surface code: ERR_DOMAIN_ALREADY_PREFIXED.
var ErrPublisherDomainInvalid = errors.New("ERR_PUBLISHER_DOMAIN_INVALID")
ErrPublisherDomainInvalid means the supplied PublisherDomain failed strict DNS-hostname validation. Rejected inputs include: schemes (`https://`, etc.), userinfo (`@`), path/query/fragment delimiters (`/`, `?`, `#`), backslashes, whitespace, control characters, ports, labels with leading/trailing hyphens, labels >63 chars, fewer than 2 labels, and all-digit TLDs (IPv4-looking). Surface code: ERR_PUBLISHER_DOMAIN_INVALID. SECURITY-CRITICAL: the validated value is interpolated into the Tier-1 fetch URL host.
var ErrVkeyCorrupt = errors.New("ERR_VKEY_CORRUPT")
ErrVkeyCorrupt means a locally cached file failed structural integrity checks (e.g. meta.json malformed, vkey.bin missing, zero-length blob). Treated as a cache miss; the resolver falls through to Tier 1. Surface code: ERR_VKEY_CORRUPT.
var ErrVkeySha256Invalid = errors.New("ERR_VKEY_SHA256_INVALID")
ErrVkeySha256Invalid means the supplied vkey_sha256 pin is not exactly 64 lowercase hex characters. Surface code: ERR_VKEY_SHA256_INVALID.
ErrVkeyUnavailable means no byte source could supply a vkey: Tier 1 failed (or absent), no fresh cache hit. Tier 2 (the anchor) is HASH-ONLY and cannot satisfy this case — it does not provide vkey bytes. The verifier MUST NOT proceed. Surface code: ERR_VKEY_UNAVAILABLE.
Retryable in a future invocation (restore network, supply cached vkey, or add a real byte-source backend). Not retryable within the current call.
Functions ¶
func Sha256Hex ¶
Sha256Hex returns the lowercase-hex SHA-256 of body. This is the canonical pin form used in the discovery manifest and on-chain anchor.
func ValidateCircuitID ¶
ValidateCircuitID enforces a safe-token allow-list. circuit_id strings are interpolated directly into filesystem cache paths and URL path segments, so any path-separator, control character, or `.` / `..` segment is rejected with ErrCircuitIDInvalid.
Allowed: ASCII letters, digits, `.`, `_`, `-` (one or more). Rejected: empty, whitespace, `/`, `\`, `\0`, any other byte, and the special segments `.` and `..`.
func ValidatePublisherDomain ¶
ValidatePublisherDomain enforces a strict DNS-hostname grammar on the publisher base domain BEFORE it is interpolated into the Tier-1 fetch URL host. SECURITY-CRITICAL: an unvalidated domain string could carry URL-authority or path-injection bytes (`/`, `@`, `?`, `#`, whitespace, control bytes, scheme prefix, port, userinfo) that would alter the URL the Tier-1 backend retrieves.
Rules:
- Trim ASCII whitespace, then reject empty.
- Reject inputs already starting with `paygate-zk.` (caller passed the full service host; the backend would build a doubled host).
- Allowed bytes: ASCII letters, digits, `.`, `-`. Anything else (including `/`, `\`, `@`, `?`, `#`, `:`, whitespace, control bytes, and any non-ASCII byte) is rejected.
- Must contain at least 2 labels separated by `.`.
- Each label: 1-63 bytes, no leading or trailing `-`.
- Reject all-digit TLD (IPv4-looking, e.g. `1.2.3.4`).
Returns ErrDomainAlreadyPrefixed for the prefix case and ErrPublisherDomainInvalid for every other rejection.
func ValidateVkeySha256 ¶
ValidateVkeySha256 requires exactly 64 lowercase hex characters. Mixed-case, short, long, or non-hex inputs are rejected with ErrVkeySha256Invalid.
Types ¶
type AnchorLookupFunc ¶
AnchorLookupFunc is a function-shaped Tier2AnchorBackend — the v1 mock and the future P1.8 client both adapt through this seam.
func (AnchorLookupFunc) AnchorHash ¶
AnchorHash satisfies Tier2AnchorBackend.
type LocalCache ¶
type LocalCache struct {
// contains filtered or unexported fields
}
LocalCache persists resolved vkeys under ${KONAREEF_STATE_DIR:-~/.konareef}/vkeys/{circuit_id}/{vkey_sha256}/.
func NewLocalCache ¶
func NewLocalCache() (*LocalCache, error)
NewLocalCache initialises the cache directory tree and writes CACHEDIR.TAG on first use.
func (*LocalCache) Has ¶
func (c *LocalCache) Has(circuitID, vkeySha256 string) bool
Has reports whether the cache contains an entry at circuitID/vkeySha256 (directory exists with a vkey.bin file). Used by the P1.8 pin-check state machine to distinguish State B (cache hit, pins match) from State A (cache miss) without invoking the full Lookup (which would also do pin verification).
Invalid identifiers return false.
func (*LocalCache) HasAnyPinFor ¶
func (c *LocalCache) HasAnyPinFor(circuitID string) bool
HasAnyPinFor reports whether the cache contains at least one entry for circuitID (under any vkey_sha256 directory). Used by the P1.8 pin-check state machine to detect State C divergence (a different pin for the same circuit_id is cached).
Identifier validation mirrors Lookup/Store; an invalid circuitID returns false (the caller cannot have stored anything under it).
func (*LocalCache) Lookup ¶
func (c *LocalCache) Lookup(circuitID, vkeySha256 string) (*Vkey, error)
Lookup returns the cached Vkey for (circuitID, vkeySha256) or nil if absent. Returns ErrCircuitIDInvalid / ErrVkeySha256Invalid when the inputs would be unsafe to interpolate into a filesystem path (path traversal / separator / non-hex / wrong-length). Returns ErrCircuitPinMismatch when the on-disk vkey.bin no longer hashes to its pin (tampering / partial write). Returns ErrVkeyCorrupt when meta.json is malformed or vkey.bin is missing.
func (*LocalCache) Store ¶
func (c *LocalCache) Store(circuitID, vkeySha256 string, body []byte, src Source) error
Store writes (circuitID, vkeySha256, body) and a meta.json record. The caller is responsible for having pin-verified body before calling Store; Store double-checks defensively. Both identifier inputs are validated (ErrCircuitIDInvalid / ErrVkeySha256Invalid) before any filesystem interpolation to defeat path traversal / cache poisoning.
type ResolveRequest ¶
type ResolveRequest struct {
CircuitID string
VkeySha256 string // lowercase-hex 32-byte pin from discovery manifest
PublisherDomain string // publisher base domain, no scheme, no `paygate-zk.` prefix
}
ResolveRequest names the circuit to resolve plus the pin the resolved vkey MUST match. PublisherDomain is the publisher **base** domain (e.g. "example.com"); the Tier-1 backend prepends "paygate-zk." internally to produce the well-known URL `https://paygate-zk.example.com/.well-known/circuits/{circuit_id}/vkey`. Passing a value already prefixed with `paygate-zk.` is rejected with ErrDomainAlreadyPrefixed at Validate() time, since the backend would otherwise build the doubled host `paygate-zk.paygate-zk.example.com`.
func (ResolveRequest) Validate ¶
func (r ResolveRequest) Validate() error
Validate enforces the minimum field set required for resolution AND hardens every field that is later interpolated into a filesystem path or URL host. CircuitID must be a safe token (`^[A-Za-z0-9._-]+$`, no `.` or `..` segments, no path separators). VkeySha256 must be exactly 64 lowercase-hex characters. PublisherDomain must be non-empty and MUST NOT already start with `paygate-zk.`.
type Resolver ¶
type Resolver struct {
Cache *LocalCache
Tier1 *Tier1HTTPSBackend
Tier2 Tier2AnchorBackend
CacheTTL time.Duration // default 24h when zero
}
Resolver orchestrates the two-tier vkey resolution per PRD 4 § 4.6 (C.6).
Lookup order:
- Cache (if Cache != nil). Hit-within-TTL with matching pin → return. Hit-past-TTL with Tier1 != nil → fall through to refresh. Hit-past-TTL with Tier1 == nil ("Cached profile") → serve with Stale=true.
- Tier 1 HTTPS (if Tier1 != nil). On success: pin-verify, store in cache, run optional Tier-2 cross-check (defence-in-depth), return.
- Tier 2 anchor cross-check (if Tier2 != nil AND we got bytes from Tier 1). If the anchor exists AND disagrees with the pin → ErrCircuitPinMismatch. If the anchor is missing (ErrAnchorNotFound) → best-effort v1: ignore.
IMPORTANT — Tier 2 is HASH-ONLY cross-validation, NEVER a byte fallback. The anchor exposes the committed vkey_sha256, not the vkey bytes. If Tier 1 fails AND there is no fresh cache hit, the resolver returns ErrVkeyUnavailable regardless of whether a Tier-2 backend is configured. A real byte fallback would require a separate byte-source backend (e.g. a publisher mirror or bundle-inline vkey) — not the anchor.
Dual failure (no cache, no Tier 1 success, no byte source) → ErrVkeyUnavailable.
type Tier1HTTPSBackend ¶
type Tier1HTTPSBackend struct {
Client *http.Client
BaseURLOverride string // test seam; empty in production
}
Tier1HTTPSBackend fetches a vkey from the well-known URL. The Fetch and BuildURL methods take the publisher **base** domain (e.g. `example.com`) — NOT the full `paygate-zk.<domain>` service host. The `paygate-zk.` prefix is added by this backend. Passing an already-prefixed input is rejected with ErrDomainAlreadyPrefixed to prevent the doubled host `paygate-zk.paygate-zk.example.com`.
In production BuildURL emits
https://paygate-zk.{domain}/.well-known/circuits/{circuit_id}/vkey
In tests, set BaseURLOverride to an httptest.Server URL to bypass the "paygate-zk." prefix and TLS — every other path component stays identical.
func NewTier1HTTPSBackend ¶
func NewTier1HTTPSBackend(client *http.Client) *Tier1HTTPSBackend
NewTier1HTTPSBackend returns a backend with a 30s default HTTP timeout when client is nil.
func (*Tier1HTTPSBackend) BuildURL ¶
func (b *Tier1HTTPSBackend) BuildURL(circuitID, publisherDomain string) (string, error)
BuildURL constructs the well-known URL. publisherDomain MUST be the publisher base domain (e.g. `example.com`); the `paygate-zk.` host prefix is added here. Inputs already starting with `paygate-zk.` are rejected with ErrDomainAlreadyPrefixed; inputs that fail strict DNS-hostname validation are rejected with ErrPublisherDomainInvalid. circuitID is validated against the safe-token allow-list before it is interpolated into the URL path; url.PathEscape further hardens the path segment.
SECURITY-CRITICAL: the URL is constructed via net/url after both circuitID and publisherDomain are validated, so a malformed input cannot inject authority/path components even if a future validation rule is loosened.
When BaseURLOverride is non-empty, the override host replaces "https://paygate-zk.{publisherDomain}" — used in tests — but circuitID is still validated.
func (*Tier1HTTPSBackend) Fetch ¶
func (b *Tier1HTTPSBackend) Fetch(ctx context.Context, circuitID, publisherDomain string) ([]byte, error)
Fetch returns the raw vkey body for circuitID. All transport / status / oversize / context-cancellation errors are folded into ErrVkeyUnavailable so the resolver can fall through to Tier 2 / cache cleanly. Input validation errors (ErrCircuitIDInvalid, ErrDomainAlreadyPrefixed) are surfaced as-is and NOT folded into ErrVkeyUnavailable — they indicate a programming bug, not transient unavailability.
type Tier2AnchorBackend ¶
type Tier2AnchorBackend interface {
// AnchorHash returns the lowercase-hex SHA-256(vkey) committed on-chain
// for circuitID, or ErrAnchorNotFound if no anchor exists, or another
// error if the lookup itself failed.
AnchorHash(ctx context.Context, circuitID string) (string, error)
}
Tier2AnchorBackend resolves the on-chain anchor hash for a circuit_id. The anchor commits SHA-256(vkey) — NOT the vkey body itself.
v1: the production wiring (BSV indexer / Bittoku service) is OWED BY P1.8. This interface exists so P1.6 ships a fully testable resolver without taking a hard dep on any single indexer.
type Vkey ¶
type Vkey struct {
CircuitID string // exact circuit_id string from bundle header
VkeySha256 string // lowercase-hex SHA-256 of Bytes
Bytes []byte // raw vkey bytes
Source Source // how this vkey was obtained
ResolvedAt time.Time // wallclock at resolution time
Stale bool // true if served from cache past TTL with no Tier-1 backend
}
Vkey is a resolved verification key plus provenance metadata.