vkeystore

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
var ErrVkeyUnavailable = errors.New("ERR_VKEY_UNAVAILABLE")

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

func Sha256Hex(body []byte) string

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

func ValidateCircuitID(id string) error

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

func ValidatePublisherDomain(domain string) error

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

func ValidateVkeySha256(pin string) error

ValidateVkeySha256 requires exactly 64 lowercase hex characters. Mixed-case, short, long, or non-hex inputs are rejected with ErrVkeySha256Invalid.

func VerifyPin

func VerifyPin(want string, body []byte) error

VerifyPin compares the SHA-256 of body against the caller-supplied want pin. Case-insensitive (both sides are lowered before compare). Returns ErrCircuitPinMismatch when the hashes differ. Returns a plain error when want is not a 64-char hex string.

Types

type AnchorLookupFunc

type AnchorLookupFunc func(ctx context.Context, circuitID string) (string, error)

AnchorLookupFunc is a function-shaped Tier2AnchorBackend — the v1 mock and the future P1.8 client both adapt through this seam.

func (AnchorLookupFunc) AnchorHash

func (f AnchorLookupFunc) AnchorHash(ctx context.Context, circuitID string) (string, error)

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:

  1. 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.
  2. Tier 1 HTTPS (if Tier1 != nil). On success: pin-verify, store in cache, run optional Tier-2 cross-check (defence-in-depth), return.
  3. 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.

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, req ResolveRequest) (*Vkey, error)

Resolve returns a verified Vkey for req or a sentinel error.

type Source

type Source string

Source records how a Vkey was obtained. Persisted in meta.json.

const (
	SourceCache         Source = "cache"
	SourceWellKnown     Source = "well-known-url"
	SourceAnchor        Source = "on-chain-anchor"
	SourceBundleInline  Source = "bundle-self-contained"
	SourcePublisherFile Source = "publisher-file"
)

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.

Jump to

Keyboard shortcuts

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