pairing

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package pairing owns the device tokens that gate a serve's origin passthrough once it is exposed beyond loopback (GDK-433).

Model: each paired device — a remote gadak binding this serve as its workspace origin — gets an opaque random token, presented as `Authorization: Bearer <token>` (the Jira DC PAT shape; the Cloud email:token Basic shape is deliberately not reused). The server stores only the SHA-256 hash plus metadata, in `pairing.json` inside the profile directory: next to config.json and its 0600 atomic-write convention, and never inside the mirror (gadak.db is a disposable cache of the origin, not a place for originals) or any export. The plaintext exists once, in `gadak pairing mint` output, inside a one-line offer.

Gate semantics (single owner: server.handleOriginREST): while at least one active — unrevoked, unexpired — token exists, every request under origin.RESTPrefix must present a valid Bearer token. With no active token the passthrough behaves exactly as before (implicit loopback trust, decision 0003). There is no loopback bypass: a tailnet proxy reaches the serve as loopback, so the token is the only identity the server can distinguish.

Index

Constants

View Source
const HomeLabel = "_home"

HomeLabel is the reserved label of the home routing token. It is not a device: revoking it locks local writes while a serve is running.

View Source
const OfferV1 = 1

OfferV1 is the only offer format this build understands. A version bump means capability change, not app version: mixed-version clients syncing against one serve is the normal state (GDK-433 prior-art survey), so an unknown version is an explicit refusal, never a silent best-effort parse.

View Source
const RemoteRel = "remote-origin.json"

RemoteRel is the profile-relative file holding this workspace's stored pairing credential: the workspace's origin is a remote gadak serve, and this is how to reach and authenticate it.

It sits next to config.json (the credential convention — 0600, atomic write, profile-scoped) rather than inside it, for the same reason the server's token store is a separate file: config.json is what settings surfaces read and rewrite; a credential file has one writer, the pairing path. The token is plaintext here because the client must present it.

View Source
const ScopeLocalRouting = "local-routing"

ScopeLocalRouting is the home machine's own routing token. pairing list uses this so the `_home` row is not mistaken for a paired device.

View Source
const ScopeOrigin = "origin"

ScopeOrigin is full origin passthrough — the device-token scope. It exists in the stored metadata so a narrower scope (read-only, wiki-only, local-routing) can be told apart from tokens minted before scopes mattered.

View Source
const StoreRel = "pairing.json"

StoreRel is the profile-relative path of the token store. A separate file, not a config.json key: config.json is settings the UI edits and exports surface; this is a credential-adjacent secret list.

Variables

This section is empty.

Functions

func EncodeOffer

func EncodeOffer(o Offer) (string, error)

EncodeOffer renders o as the single-line base64url form.

func Explain

func Explain(dir, bearer string, now time.Time) (Verdict, Reason)

Explain classifies a rejected bearer. Authorize remains the gate decision (and last_used_at); this is the reason the 401 body carries.

hash in store, RevokedAt set → revoked
hash in store, ExpiresAt elapsed → expired
empty bearer or hash not in the store → unknown

func FormatExpiry

func FormatExpiry(t time.Time) string

FormatExpiry renders an offer's ExpiresAt for mint output; "" when unset.

func RemotePath

func RemotePath(dir string) string

RemotePath is the absolute credential path inside a profile directory.

func SaveRemote

func SaveRemote(dir string, r Remote) error

SaveRemote writes the pairing credential atomically at 0600, creating the profile directory if needed. Called only after a successful verify-before-save round trip — a credential that was never proven good must not reach disk.

func StorePath

func StorePath(dir string) string

StorePath is the absolute token-store path inside a profile directory.

Types

type Meta

type Meta struct {
	Label      string     `json:"label"`
	Scope      string     `json:"scope"`
	Hash       string     `json:"hash"`
	CreatedAt  time.Time  `json:"created_at"`
	ExpiresAt  time.Time  `json:"expires_at"`
	RevokedAt  *time.Time `json:"revoked_at,omitempty"`
	LastUsedAt *time.Time `json:"last_used_at,omitempty"`
}

Meta is one stored token. Hash is the hex SHA-256 of the plaintext; the plaintext is never persisted anywhere.

func List

func List(dir string) ([]Meta, error)

List returns every stored token, earliest created first, including revoked and expired ones — revocation is audit history, not noise.

func Mint

func Mint(dir, label string, ttl time.Duration, now time.Time) (string, Meta, error)

Mint creates a token for label, persists its hash, and returns the plaintext. The plaintext appears exactly here; callers print it once (as an offer) and never store it. A duplicate *active* label is refused so `pairing revoke <label>` stays unambiguous; a revoked label may be reused.

func Revoke

func Revoke(dir, selector string, now time.Time) (Meta, error)

Revoke revokes the token selected by exact label or hash prefix and returns it. Refuses ambiguities instead of guessing, and refuses an already-revoked token rather than no-op'ing silently. Revoked entries stay in the store as audit history but never stand as candidates: a re-minted label revokes by label again, pointed at the live mint.

func Rotate

func Rotate(dir, label string, ttl time.Duration, now time.Time) (string, Meta, error)

Rotate replaces every live token with label by a newly minted one in a single store write, so pairing list never shows two live rows with the same name. Used for the home routing token: mint + revoke of the previous `_home` must not be two verbs a crash can split.

func (Meta) Active

func (m Meta) Active(now time.Time) bool

Active reports whether this token admits requests at now. The home origin's clock is authoritative (GDK-369): expiry is judged where the store lives, never by a client-supplied timestamp.

type Offer

type Offer struct {
	V         int    `json:"v"`
	Endpoint  string `json:"endpoint"`
	Token     string `json:"token"`
	ExpiresAt string `json:"expires_at"`
	Label     string `json:"label"`
}

Offer is the one-line pairing secret `gadak pairing mint` prints and a remote gadak consumes via `gadak init --pairing-code`. Base64url of a JSON document: one pasteable line, no shell-hostile characters.

The offer carries the token itself, so it is treated like a credential: never echoed into a log line, an error message, or argv more than the one flag the user already typed. ExpiresAt is advisory for the human (RFC3339, home origin's clock); the gate judges expiry from the store.

func DecodeOffer

func DecodeOffer(s string) (Offer, error)

DecodeOffer parses an offer line. Errors describe the problem without quoting the payload — the token inside must not leak through an error path. Unknown version, missing endpoint, and missing token are distinct errors so `init --pairing-code` can tell the user what to redo.

type Reason

type Reason string

Reason is why a rejected bearer was refused. Only tokens that were minted (hash in the store) get a detailed reason; anything else is unknown so a probe cannot tell unused strings from never-issued ones.

const (
	ReasonExpired Reason = "expired"
	ReasonRevoked Reason = "revoked"
	ReasonUnknown Reason = "unknown"
)

type Remote

type Remote struct {
	Endpoint string `json:"endpoint"`
	Token    string `json:"token"`
	Label    string `json:"label,omitempty"`
	PairedAt string `json:"pairedAt,omitempty"`
}

Remote is the stored client side of a pairing: where the home serve is, the device token, and the label the home shows in `pairing list`.

func LoadRemote

func LoadRemote(dir string) (*Remote, error)

LoadRemote reads the stored pairing credential. Missing file is (nil, nil): most workspaces are not paired, and that is not an error.

type Verdict

type Verdict int

Verdict is Authorize's answer.

const (
	// VerdictOff: no active token exists, so the gate does not apply and
	// the passthrough keeps its pre-pairing behavior.
	VerdictOff Verdict = iota
	// VerdictAccept: a valid Bearer token was presented.
	VerdictAccept
	// VerdictReject: active tokens exist but the request carried no valid
	// one. Missing and wrong are the same answer — do not reveal which.
	VerdictReject
)

func Authorize

func Authorize(dir, bearer string, now time.Time) (Verdict, error)

Authorize decides whether bearer may pass the gate at now. It fails closed: a store that cannot be read rejects, because tokens may exist; only a proven-absent store is VerdictOff. On accept it records last_used_at (throttled to one disk write per token per interval).

Jump to

Keyboard shortcuts

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