Help us shape the future of Go! Take the 2026 Go Developer Survey

ticket

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package ticket is the contract between a lock and lockhub: the ticket a lock pushes when a request needs a human, the decision the hub signs back, their canonical JSON (RFC 8785), Ed25519 signatures and the X25519 sealed boxes that carry what the hub must not read.

Signatures are computed over the canonical form of the value without its "sig" member, prefixed by a Context. Verification decodes strictly (no unknown member, no trailing data), validates, re-canonicalizes what it decoded and checks the signature over that: what verifies is exactly what the caller then reads.

Index

Constants

View Source
const (
	FacadeTTY  = "tty"
	FacadeDeck = "deck"
)

Local facades: they run on the host, at the human's own terminal.

View Source
const (
	// RenewExpired: the ticket expired before a decision.
	RenewExpired = "expired"
	// RenewApprovalExpired: the approval aged past its TTL before the run.
	RenewApprovalExpired = "approval-expired"
)

Renewal reasons.

View Source
const (
	ChangeChanged = "changed"
	ChangeNew     = "new"
	ChangeGone    = "gone"
	// ChangeUnchecked: the lock could not check this input now (a
	// Recompute without the session it needs): it may have moved. The run
	// checks it; no facade presents the renewal as unchanged.
	ChangeUnchecked = "unchecked"
)

Kinds of Change.

View Source
const FacadeMobile = "mobile"

FacadeMobile is the facade name of decisions taken on a device.

View Source
const MaxChanges = 64

MaxChanges bounds Renewal.Changes.

View Source
const MaxClearScreen = 256 << 10

MaxClearScreen bounds the canonical clear screen sealed to a device; a larger one is not sealed (the device shows the masked screen only).

View Source
const MaxDevices = 8

MaxDevices bounds the devices a lock pins.

View Source
const MaxFacades = 16

MaxFacades bounds a FacadeSet.

View Source
const RevealTTL = 2 * time.Minute

RevealTTL is how long a RevealKey and the request carrying it can be acted on.

View Source
const SealAlg = "x25519-hkdf-sha256-xchacha20poly1305"

SealAlg is the only sealed-box construction: an ephemeral X25519 key agreement, HKDF-SHA256 over the shared secret (salt: the ephemeral then the recipient public key), XChaCha20-Poly1305 with a random nonce.

View Source
const SealAlgGCM = "x25519-hkdf-sha256-aes256gcm"

SealAlgGCM is the same construction with AES-256-GCM and a 12-byte random nonce, for a recipient that has WebCrypto and no XChaCha20 (a device's browser). Each box has its own ephemeral key, so its AES key encrypts one message only.

View Source
const UncheckedInput = "unchecked"

UncheckedInput is the digest a Recompute gives an input it cannot check now; DiffInputs reports it as ChangeUnchecked.

View Source
const Version = 1

Version is the schema version of tickets and decisions this package writes and reads. A value of any other version is refused (fail closed).

Variables

View Source
var ErrBadSignature = errors.New("ticket: bad signature")

ErrBadSignature: the signature does not verify under the key given, or the key is not the one the signed value names.

View Source
var ErrDenied = errors.New("ticket: not approved")

ErrDenied: the decision is not an approval.

View Source
var ErrDevice = errors.New("ticket: device signature refused")

ErrDevice: a device assertion does not verify, or a decision that needs one has none.

View Source
var ErrExpired = errors.New("ticket: expired")

ErrExpired: the ticket or the approval is past its TTL.

View Source
var ErrFacade = errors.New("ticket: facade key refused")

ErrFacade: a reveal key does not verify under a facade the lock pins.

View Source
var ErrInvalid = errors.New("ticket: invalid")

ErrInvalid is wrapped by every validation error.

View Source
var ErrMismatch = errors.New("ticket: decision does not match the ticket")

ErrMismatch: a verified decision is not for this ticket.

View Source
var ErrNotCanonicalizable = errors.New("ticket: not canonicalizable JSON")

ErrNotCanonicalizable is wrapped by every error of CanonicalizeJSON: input that is not I-JSON (RFC 7493), the subset RFC 8785 defines.

View Source
var ErrOpen = errors.New("ticket: sealed box does not open")

ErrOpen: a sealed box does not open with this key and associated data.

View Source
var ErrTooLarge = errors.New("ticket: clear screen too large to seal")

ErrTooLarge: a clear screen over MaxClearScreen.

Risks lists every risk class, from the least to the most sensitive.

Functions

func CanReveal added in v0.5.0

func CanReveal(facade string) bool

CanReveal reports whether facade may show a ticket's clear values on demand (Ctrl-O): only local facades. A device has its own path (PII.SealedFor); a chat facade never shows clear values.

func Canonical

func Canonical(v any) ([]byte, error)

Canonical is the RFC 8785 (JCS) form of v: json.Marshal, then CanonicalizeJSON. Signatures and hashes are computed over it, so it refuses what json.Marshal would encode with a loss: a string with invalid UTF-8 (json.Marshal writes U+FFFD for it) and, as CanonicalizeJSON, an integer a double cannot hold exactly. Encode such values as strings (base64, decimal) first.

func CanonicalizeJSON

func CanonicalizeJSON(b []byte) ([]byte, error)

CanonicalizeJSON is the RFC 8785 (JCS) form of one JSON value: object members sorted by their UTF-16 code units, no whitespace, strings with the minimal escapes, numbers as ECMAScript prints them. It refuses what I-JSON refuses: invalid UTF-8, lone surrogates, duplicate member names, numbers out of the IEEE 754 double range, integers it cannot hold exactly (above 2^53, mostly: two of them would print alike), and trailing data.

func ClampTTL

func ClampTTL(r Risk, configured time.Duration) time.Duration

ClampTTL is the TTL of class r for an instance configured with configured: an instance may only shorten it. configured <= 0 keeps r.TTL().

func ClearScreenAAD added in v0.4.0

func ClearScreenAAD(id, dev string) []byte

ClearScreenAAD is the associated data of ticket id's clear screen sealed to device dev.

func DeviceID added in v0.4.0

func DeviceID(cred []byte) string

DeviceID is the id of the device whose WebAuthn credential id is cred.

func HashOf

func HashOf(v any) (string, error)

HashOf is "sha256:" and the hex SHA-256 of the canonical form of v: the plan hash of a plan's inputs, a policy fingerprint.

func InputDigests added in v0.5.0

func InputDigests(v any) (map[string]string, error)

InputDigests names the inputs of a plan hash: for each member of the JSON object v encodes to, the HashOf its value. What a product hashes into its plan hash (a struct, a map) gives its inputs by field name. The digests stay in the lock (plans.Binding.Inputs): DiffInputs sends only the names of those that moved.

func KeyID

func KeyID(pub ed25519.PublicKey) string

KeyID names an Ed25519 public key: "ed25519:" and the first 16 bytes of its SHA-256, in hex. It names a key, it does not vouch for one: the verifier always uses the key it pinned.

func MarshalDevices added in v0.4.0

func MarshalDevices(devs []Device) ([]byte, error)

MarshalDevices is the canonical DeviceSet of devs, as ParseDevices reads it.

func NewID

func NewID(now time.Time) string

NewID is a fresh ticket id: a ULID (48-bit Unix milliseconds, then 80 random bits), 26 upper-case characters that sort by creation time.

func NewNonce

func NewNonce() string

NewNonce is a fresh random nonce: 16 bytes, base64url.

func Open

func Open(priv *ecdh.PrivateKey, s *Sealed, aad []byte) ([]byte, error)

Open decrypts s (SealAlg or SealAlgGCM) with priv and aad. Any failure is ErrOpen.

func RequestMessage

func RequestMessage(method, pathQuery, instance string, at time.Time, nonce string, body []byte) []byte

RequestMessage is what a lock signs, under ContextRequest, for one HTTP request to the hub (the Lock-Sig header): the method, the path with its query, the signing instance (Lock-Instance), the Unix time in seconds (Lock-Time), the request nonce (Lock-Nonce) and the hex SHA-256 of the body, one per line. The hub accepts it within a minute of its own clock and each nonce once.

func RevealAAD added in v0.5.0

func RevealAAD(r *RevealKey) []byte

RevealAAD is the associated data of a reveal's box: the reveal, its ticket, instance and lock key.

func RiskNames added in v0.8.1

func RiskNames() string

RiskNames lists Risks, comma-separated, for messages.

func SignBytes

func SignBytes(key ed25519.PrivateKey, c Context, msg []byte) string

SignBytes signs msg under context c.

func TicketHash added in v0.4.0

func TicketHash(signed []byte) string

TicketHash is "sha256:" and the hex SHA-256 of a signed ticket's bytes as the lock sent them (its canonical form, Ticket.Marshal).

func ValidDeviceID added in v0.4.0

func ValidDeviceID(s string) bool

ValidDeviceID reports whether s has the form of a device id.

func ValidID

func ValidID(s string) bool

ValidID reports whether s is a ULID in canonical form: 26 characters of the upper-case alphabet, the first at most '7'.

func VerifyAssertion added in v0.4.0

func VerifyAssertion(dev *Device, challenge []byte, a *DeviceAssertion) (uint32, error)

VerifyAssertion checks a WebAuthn assertion of dev over challenge: the client data is a webauthn.get for dev's origin carrying challenge, the authenticator data is for dev's relying party with the user present and verified, and the ES256 signature verifies under dev's key. It returns the authenticator's signature counter.

func VerifyBytes

func VerifyBytes(pub ed25519.PublicKey, c Context, msg []byte, sig string) error

VerifyBytes checks sig, as SignBytes returns it, over msg under c.

func VerifyDevice added in v0.4.0

func VerifyDevice(dev *Device, d *Decision, signed []byte) error

VerifyDevice checks that d was taken on dev about the ticket signed: d names dev as its device and approver, on the mobile facade, and dev's assertion verifies over d's statement for that ticket. signed is the lock's own signed ticket (never one the hub sent back). The caller verified d's hub signature first (VerifyDecision), matched d to the ticket (Match), and picks dev among its pinned devices by d.Device.Device.

func VerifyKeyCert

func VerifyKeyCert(signer ed25519.PublicKey, c Context, bind string, data []byte) (*ecdh.PublicKey, error)

VerifyKeyCert decodes a key certificate signed by signer under context c and bound to bind, and returns its X25519 key.

Types

type Approver

type Approver struct {
	ID      string `json:"id"`
	Display string `json:"display"`
}

Approver is who decided, as the hub identified them.

type Change added in v0.5.0

type Change struct {
	Input string `json:"input"`
	// Change is ChangeChanged, ChangeNew or ChangeGone.
	Change string `json:"change"`
}

Change is one plan-hash input that moved: its name and how. Names only: a digest of a short value (a name, an id) would let a hub confirm a guess, so neither values nor digests leave the lock.

func DiffInputs added in v0.5.0

func DiffInputs(before, after map[string]string) []Change

DiffInputs lists the inputs whose digest differs between before and after, sorted by name; the first MaxChanges only (a plan has a handful of inputs).

type Context

type Context string

Context separates what one Ed25519 key signs: a lock's key signs its tickets, its X25519 key and its requests to the hub, and none of these signatures is valid as another. A new signed kind gets a new context.

const (
	// ContextDeviceDecision is hashed with a DeviceStatement into the
	// WebAuthn challenge a device signs.
	ContextDeviceDecision Context = "lockkit/device-decision/v1"
	// ContextClearScreen is the associated data of a clear screen sealed
	// to a device (with the ticket and device ids).
	ContextClearScreen Context = "lockkit/clear-screen/v1"
)
const (
	// ContextHubRequest signs a HubRequest with the hub's key.
	ContextHubRequest Context = "lockkit/hub-request/v1"
	// ContextRevealKey signs a RevealKey with a facade's key.
	ContextRevealKey Context = "lockkit/reveal-key/v1"
	// ContextRevealBox is the associated data of a reveal's box.
	ContextRevealBox Context = "lockkit/reveal-box/v1"
	// ContextRevealAnswer signs a RevealAnswer with the lock's key.
	ContextRevealAnswer Context = "lockkit/reveal-answer/v1"
)
const (
	ContextTicket   Context = "lockkit/ticket/v1"
	ContextDecision Context = "lockkit/decision/v1"
	// ContextKeyCert signs an X25519 public key with an Ed25519 key.
	ContextKeyCert Context = "lockkit/x25519-key/v1"
	// ContextRequest signs a lock's HTTP request to the hub (Lock-Sig).
	ContextRequest Context = "lockkit/request/v1"
	// ContextReveal is a KeyCert context for a one-use key bound to a
	// ticket. Reveals do not use it: a RevealKey is signed under
	// ContextRevealKey.
	ContextReveal Context = "lockkit/reveal/v1"
)

type Decision

type Decision struct {
	V        int     `json:"v"`
	Ticket   string  `json:"ticket"`
	Instance string  `json:"instance"`
	KeyID    string  `json:"key_id"` // the lock's key
	PlanHash string  `json:"plan_hash"`
	Decision Outcome `json:"decision"`
	// Reason is the human's reason (a denial's, mostly); untrusted.
	Reason   string   `json:"reason"`
	Approver Approver `json:"approver"`
	// Facade is the facade the human answered on ("tty", "deck", ...).
	Facade string    `json:"facade"`
	At     time.Time `json:"at"`
	// Nonce is single use: a lock consumes it with the decision.
	Nonce    string `json:"nonce"`
	HubKeyID string `json:"hub_key_id"` // KeyID of the hub's key
	// Device is the assertion of the device the human decided on (facade
	// FacadeMobile), over Statement; absent from any other facade. A lock
	// checks it with VerifyDevice against the devices it pins.
	Device *DeviceAssertion `json:"device,omitempty"`
	Sig    string           `json:"sig,omitempty"`
}

Decision is the hub's answer to one ticket, signed by the hub. It repeats the ticket's instance, lock key and plan hash: a lock refuses a decision that does not match its own ticket (Match), so a decision cannot be replayed to another instance or plan.

func VerifyDecision

func VerifyDecision(pub ed25519.PublicKey, data []byte) (*Decision, error)

VerifyDecision decodes a decision and checks it: schema, then the signature under pub, the hub key the caller pinned. A decision naming another hub key id is refused. The caller then checks Match against its ticket and consumes the nonce.

func (*Decision) Marshal

func (d *Decision) Marshal() ([]byte, error)

Marshal is the canonical JSON of a signed decision, as it is sent.

func (*Decision) Match

func (d *Decision) Match(t *Ticket) error

Match checks that d answers t: same ticket id, instance, lock key and plan hash. A lock calls it with its own ticket (never one rebuilt from the decision) before acting on d.

func (*Decision) Sign

func (d *Decision) Sign(key ed25519.PrivateKey) error

Sign validates d, sets d.HubKeyID to the key's id when empty (a different one is an error), and signs it.

func (*Decision) Statement added in v0.4.0

func (d *Decision) Statement(dev, ticketHash string) DeviceStatement

Statement is the statement a device signs for d, as device dev, about the signed ticket whose TicketHash is ticketHash.

func (*Decision) Usable

func (d *Decision) Usable(t *Ticket, now time.Time, ttl time.Duration) error

Usable checks that an approval of t can still be acted on at now: it came before the ticket expired, and it is younger than ttl (the instance's ClampTTL for t.Risk; 0 never ages). A decision dated in the future by more than a minute is refused too, and anything but an approval is ErrDenied.

func (*Decision) Validate

func (d *Decision) Validate() error

Validate checks d's schema. It does not check the signature.

type Device added in v0.4.0

type Device struct {
	V int `json:"v"`
	// ID is DeviceID(credential): "webauthn:" and 16 bytes of hex.
	ID string `json:"id"`
	// Credential is the WebAuthn credential id (base64url).
	Credential string `json:"credential"`
	// Name is the human's label ("phone"), for display.
	Name string `json:"name"`
	// RPID is the WebAuthn relying party id (the facade's host name) and
	// Origin the facade's origin ("https://hub.example.net").
	RPID   string `json:"rp_id"`
	Origin string `json:"origin"`
	// ES256 is the credential's P-256 public key, uncompressed (65 bytes,
	// base64url).
	ES256 string `json:"es256"`
	// X25519 is the device's sealing key (32 bytes, base64url).
	X25519 string `json:"x25519"`
}

Device is a pinned approval device. Its ID derives from its WebAuthn credential id; Fingerprint is what the human compares between the host's terminal and the device when pairing.

func ParseDevices added in v0.4.0

func ParseDevices(data []byte) ([]Device, error)

ParseDevices reads a DeviceSet in canonical JSON (MarshalDevices): no unknown or duplicate member, no trailing data, every device valid, ids unique, at most MaxDevices.

func (*Device) Fingerprint added in v0.4.0

func (d *Device) Fingerprint() (string, error)

Fingerprint is the SHA-256 of d's canonical form, as groups of four hex digits: the human compares it on the terminal and on the device before the device is pinned.

func (*Device) SealingKey added in v0.4.0

func (d *Device) SealingKey() (*ecdh.PublicKey, error)

SealingKey is d's X25519 key.

func (*Device) Validate added in v0.4.0

func (d *Device) Validate() error

Validate checks d's fields: version, id, keys, relying party.

type DeviceAssertion added in v0.4.0

type DeviceAssertion struct {
	Device     string `json:"device"` // Device.ID
	AuthData   string `json:"auth_data"`
	ClientData string `json:"client_data"`
	Sig        string `json:"sig"`
}

DeviceAssertion is a WebAuthn assertion of a device over a decision's DeviceStatement, as the browser returned it (all base64url).

type DeviceBox added in v0.4.0

type DeviceBox struct {
	Device string `json:"device"`
	Box    Sealed `json:"box"`
}

DeviceBox is a clear screen sealed to one device.

func SealClearScreen added in v0.4.0

func SealClearScreen(dev *Device, id string, s Screen) (*DeviceBox, error)

SealClearScreen seals the clear screen s of ticket id to dev, with SealAlgGCM (a browser opens it with WebCrypto).

type DeviceSet added in v0.4.0

type DeviceSet struct {
	V       int      `json:"v"`
	Devices []Device `json:"devices"`
}

DeviceSet is the file of a lock's pinned devices (--devices-file).

type DeviceStatement added in v0.4.0

type DeviceStatement struct {
	V        int       `json:"v"`
	Ticket   string    `json:"ticket"`
	Instance string    `json:"instance"`
	KeyID    string    `json:"key_id"`
	PlanHash string    `json:"plan_hash"`
	Decision Outcome   `json:"decision"`
	Reason   string    `json:"reason"`
	Approver string    `json:"approver"`
	Display  string    `json:"display"`
	Facade   string    `json:"facade"`
	At       time.Time `json:"at"`
	Nonce    string    `json:"nonce"`
	HubKeyID string    `json:"hub_key_id"`
	Device   string    `json:"device"`
	// TicketHash is TicketHash of the signed ticket, as the lock sent it.
	TicketHash string `json:"ticket_hash"`
}

DeviceStatement is what a device signs for a decision: every field of the decision a lock acts on, the hub's signature and the assertion aside, and the hash of the lock-signed ticket the device showed. A lock rebuilds it from the decision it received and its own signed ticket: a hub that shows the device another screen, another clear screen or other flags than the lock's gets a signature over another ticket hash.

func (DeviceStatement) Challenge added in v0.4.0

func (s DeviceStatement) Challenge() ([]byte, error)

Challenge is the WebAuthn challenge of s: the SHA-256 of ContextDeviceDecision, a NUL and the canonical form of s.

type FacadeKey added in v0.5.0

type FacadeKey struct {
	Facade string `json:"facade"`
	// Key is the raw 32-byte public key, standard base64.
	Key string `json:"key"`
}

FacadeKey is a facade's Ed25519 key, as a lock pins it: `lox up` writes the facades the human reviewed at the host's terminal.

type FacadeSet added in v0.5.0

type FacadeSet struct {
	V       int         `json:"v"`
	Facades []FacadeKey `json:"facades"`
}

FacadeSet is the file of the facade keys a lock pins.

type Flags

type Flags struct {
	// Production: the target is production; the facade asks for the
	// typed instance name.
	Production bool `json:"production"`
	// NeverAuto: never approved by an auto mode.
	NeverAuto bool `json:"never_auto"`
	// Retype, when set, is the answer that asks the lock to retype values.
	Retype string `json:"retype"`
}

Flags change how a facade asks.

type HubRequest added in v0.5.0

type HubRequest struct {
	V        int         `json:"v"`
	ID       string      `json:"id"` // NewID, made by the hub
	Kind     RequestKind `json:"kind"`
	Ticket   string      `json:"ticket"`
	Instance string      `json:"instance"`
	KeyID    string      `json:"key_id"` // the lock's key
	Approver Approver    `json:"approver"`
	Facade   string      `json:"facade"`
	At       time.Time   `json:"at"`
	// Reveal is the facade's key, for RequestReveal only.
	Reveal   *RevealKey `json:"reveal,omitempty"`
	HubKeyID string     `json:"hub_key_id"`
	Sig      string     `json:"sig,omitempty"`
}

HubRequest is a human's request to a lock about one of its tickets, signed by the hub. Approver and Facade are the hub's word: for a reveal the facade's own signature (Reveal) is what the lock checks. The lock checks it like a decision (pinned hub key, its own instance and key, fresh) and, for a reveal, the facade's own signature on Reveal.

func VerifyHubRequest added in v0.5.0

func VerifyHubRequest(pub ed25519.PublicKey, data []byte) (*HubRequest, error)

VerifyHubRequest decodes a request and checks its schema and the hub's signature under pub, the hub key the lock pinned. The lock then checks that it is for its own instance and key, fresh (Fresh) and, for a reveal, Reveal.Verify under its pinned facades.

func (*HubRequest) Fresh added in v0.5.0

func (q *HubRequest) Fresh(now time.Time) bool

Fresh reports whether q (and its reveal key) can be acted on at now: made less than RevealTTL ago, not more than a minute ahead.

func (*HubRequest) Marshal added in v0.5.0

func (q *HubRequest) Marshal() ([]byte, error)

Marshal is the canonical JSON of a signed request.

func (*HubRequest) Sign added in v0.5.0

func (q *HubRequest) Sign(key ed25519.PrivateKey) error

Sign validates q, sets q.HubKeyID to the key's id when empty (a different one is an error), and signs it.

func (*HubRequest) Validate added in v0.5.0

func (q *HubRequest) Validate() error

Validate checks q's schema. It does not check the signatures.

type KeyCert

type KeyCert struct {
	V      int    `json:"v"`
	KeyID  string `json:"key_id"` // the signing Ed25519 key
	X25519 string `json:"x25519"`
	// Bind ties the key to one use ("" for a long-lived key).
	Bind string `json:"bind"`
	Sig  string `json:"sig,omitempty"`
}

KeyCert is an X25519 public key signed by an Ed25519 key: a lock's sealing key (ContextKeyCert), or a one-use key bound to a ticket (ContextReveal, Bind = the ticket id; a reveal uses RevealKey). Whoever seals to it checks the signature under the Ed25519 key they pinned: a hub cannot substitute its own key.

func CertifyX25519

func CertifyX25519(key ed25519.PrivateKey, c Context, pub *ecdh.PublicKey, bind string) (*KeyCert, error)

CertifyX25519 signs pub with key under context c (ContextKeyCert or ContextReveal), bound to bind.

type Outcome

type Outcome string

Outcome is the closed set of decisions.

const (
	Approved Outcome = "approved"
	Denied   Outcome = "denied"
)

func (*Outcome) UnmarshalJSON

func (o *Outcome) UnmarshalJSON(b []byte) error

UnmarshalJSON refuses an outcome other than Approved and Denied.

type PII

type PII struct {
	// Touched names the kinds of personal data involved ("email").
	Touched []string `json:"touched"`
	// ClearSealed is the unmasked screen sealed to one facade's device
	// key, for a facade able to show it; nil otherwise.
	ClearSealed *Sealed `json:"clear_sealed"`
	// SealedFor is the clear screen sealed to each device the lock pins
	// (SealClearScreen), when the lock is set to seal it; absent
	// otherwise. ClearSealed (one recipient, any facade key) is not used
	// for devices.
	SealedFor []DeviceBox `json:"sealed_for,omitempty"`
}

PII says what personal data the request touches.

type PinnedFacade added in v0.5.0

type PinnedFacade struct {
	Facade string
	Key    ed25519.PublicKey
}

PinnedFacade is a parsed FacadeKey.

func ParseFacades added in v0.5.0

func ParseFacades(data []byte) ([]PinnedFacade, error)

ParseFacades reads a FacadeSet: version, facade names (CanReveal only: a key of any other facade is refused, not ignored), keys, no key twice.

type Renewal added in v0.5.0

type Renewal struct {
	// Of is the expired ticket.
	Of string `json:"of"`
	// Reason is why Of ended: RenewExpired or RenewApprovalExpired.
	Reason string `json:"reason"`
	// ApprovedAt is when Of was approved, for RenewApprovalExpired.
	ApprovedAt *time.Time `json:"approved_at"`
	// Checked: the lock compared the plan-hash inputs. False, Changes is
	// empty because the lock could not compare them (the run still checks
	// the plan hash, and refuses a stale plan).
	Checked bool `json:"checked"`
	// Changes are the inputs whose digest moved, by name.
	Changes []Change `json:"changes"`
}

Renewal says that a ticket asks again for an expired one (decision H1): the same request, re-checked by the lock, with what changed in its plan-hash inputs since the expired ticket was made (and approved, when it was). A facade shows the changes and the human approves in one step; production still asks for the typed name.

func (*Renewal) Unchanged added in v0.5.0

func (r *Renewal) Unchanged() bool

Unchanged reports whether a facade may present r as nothing changed: the lock checked, and no input moved.

type RequestKind added in v0.5.0

type RequestKind string

RequestKind is the closed set of hub requests.

const (
	// RequestReveal: seal the clear screen of a ticket to a facade's
	// RevealKey.
	RequestReveal RequestKind = "reveal"
	// RequestRenew: ask again for an expired ticket (Ticket.Renews).
	RequestRenew RequestKind = "renew"
)

type Requester

type Requester struct {
	Agent   string `json:"agent"`
	Session string `json:"session"`
}

Requester is the agent that asked, as the agent says: a label for display, never authenticated.

type RevealAnswer added in v0.5.0

type RevealAnswer struct {
	V        int     `json:"v"`
	Request  string  `json:"request"` // HubRequest.ID
	Reveal   string  `json:"reveal"`  // RevealKey.ID
	Ticket   string  `json:"ticket"`
	Instance string  `json:"instance"`
	KeyID    string  `json:"key_id"` // the lock's key
	Box      *Sealed `json:"box"`
	Sig      string  `json:"sig,omitempty"`
}

RevealAnswer is a lock's answer to a reveal: the clear screen sealed to the facade's ephemeral key, signed by the lock (ContextRevealAnswer), so that a hub can neither read it nor replace it.

func SealReveal added in v0.5.0

func SealReveal(key ed25519.PrivateKey, q *HubRequest, epk *ecdh.PublicKey, clear Screen) (*RevealAnswer, error)

SealReveal seals clear to the facade key of q (verified by the caller, epk) and signs the answer with the lock's key.

func (*RevealAnswer) Marshal added in v0.5.0

func (a *RevealAnswer) Marshal() ([]byte, error)

Marshal is the canonical JSON of a signed answer.

type RevealKey added in v0.5.0

type RevealKey struct {
	V int `json:"v"`
	// ID names this reveal (NewID); the box is bound to it.
	ID       string `json:"id"`
	Ticket   string `json:"ticket"`
	Instance string `json:"instance"`
	KeyID    string `json:"key_id"` // the lock's key
	Facade   string `json:"facade"`
	// EPK is the ephemeral X25519 public key.
	EPK         string    `json:"epk"`
	At          time.Time `json:"at"`
	FacadeKeyID string    `json:"facade_key_id"`
	Sig         string    `json:"sig,omitempty"`
}

RevealKey is a facade's ephemeral X25519 key for one reveal of one ticket, signed by the facade's Ed25519 key (ContextRevealKey). The lock seals the clear screen to EPK only if the signature verifies under a facade key it pins for Facade: a hub cannot put its own key in.

func NewRevealKey added in v0.5.0

func NewRevealKey(key ed25519.PrivateKey, facade string, t *Ticket, now time.Time) (*RevealKey, *ecdh.PrivateKey, error)

NewRevealKey makes the reveal key of facade (signed by key) for ticket t, and returns it with the ephemeral private key, which the facade keeps in memory only and drops once the screen is shown.

func (*RevealKey) Verify added in v0.5.0

func (r *RevealKey) Verify(pinned []PinnedFacade) (*ecdh.PublicKey, error)

Verify checks r against the facade keys a lock pins: the key it names is pinned for r.Facade, and signs r. It returns r's ephemeral key.

type Risk

type Risk string

Risk is the closed set of risk classes. Each product maps its operations onto one of them; the hub routes and expires tickets by risk alone, so a product cannot invent a class a facade does not know.

const (
	RiskRead         Risk = "read"
	RiskReadPII      Risk = "read-pii"
	RiskWrite        Risk = "write"
	RiskProdWrite    Risk = "prod-write"
	RiskPolicyLoosen Risk = "policy-loosen"
	RiskUnlock       Risk = "unlock"
)

func ParseRisk

func ParseRisk(s string) (Risk, error)

ParseRisk returns the risk class named s; any other name is an error.

func (Risk) TTL

func (r Risk) TTL() time.Duration

TTL is how long a ticket of class r waits for a decision, and how long an approval stays usable once given; 0 means no expiry. Reads never expire; writes and policy loosening last 24 h; production writes and unlock (a secret typed for the lock) 4 h.

func (*Risk) UnmarshalJSON

func (r *Risk) UnmarshalJSON(b []byte) error

UnmarshalJSON refuses a risk class outside Risks.

func (Risk) Valid

func (r Risk) Valid() bool

Valid reports whether r is one of Risks.

type Screen

type Screen struct {
	// Header names the request ("db / shop · tier read").
	Header string `json:"header"`
	// Note is the agent's one-line reason (untrusted).
	Note string `json:"note"`
	// Body are the lines of the request; Warnings are shown in red.
	Body     []string `json:"body"`
	Warnings []string `json:"warnings"`
	// Red frames the screen in red outside production too.
	Red bool `json:"red"`
}

Screen is the approval screen, masked by the lock before it leaves the lock (the render package builds it). It mirrors the console's screen.

func OpenClearScreen added in v0.4.0

func OpenClearScreen(priv *ecdh.PrivateKey, id, dev string, b *DeviceBox) (Screen, error)

OpenClearScreen opens the clear screen of ticket id sealed to device dev, whose sealing key is priv. A box for another device is ErrOpen.

func OpenReveal added in v0.5.0

func OpenReveal(lockPub ed25519.PublicKey, priv *ecdh.PrivateKey, r *RevealKey, data []byte) (Screen, error)

OpenReveal checks a lock's answer to the facade's reveal r (signed by lockPub, the lock key the facade pinned; for r's reveal, ticket, instance and key) and opens it with the ephemeral key priv. The clear screen is the lock's own words and the agent's: a facade shows it through its own escaping, like the masked one.

type Sealed

type Sealed struct {
	Alg   string `json:"alg"`
	EPK   string `json:"epk"`   // ephemeral X25519 public key
	Nonce string `json:"nonce"` // 24 bytes
	CT    string `json:"ct"`    // ciphertext and tag
}

Sealed is a sealed box: what only the holder of one X25519 private key can read, relayed by a hub that cannot (a secret for unlock, a clear screen for a facade's device, a reveal).

func Seal

func Seal(to *ecdh.PublicKey, plaintext, aad []byte) (*Sealed, error)

Seal encrypts plaintext to the X25519 key to. aad is bound to the box and must be given again to Open (a ticket id, a context): a box sealed for one ticket does not open as another's.

func SealGCM added in v0.4.0

func SealGCM(to *ecdh.PublicKey, plaintext, aad []byte) (*Sealed, error)

SealGCM is Seal with SealAlgGCM, for a recipient that opens boxes with WebCrypto.

type Source

type Source struct {
	// Lock is the product: "locksql", "lockfile", ...
	Lock string `json:"lock"`
	// Instance names the lock instance ("crm-prod"); decisions are bound
	// to it.
	Instance string `json:"instance"`
	// KeyID is KeyID of the Ed25519 key that signs the ticket.
	KeyID string `json:"key_id"`
}

Source is the lock that issued a ticket.

type Ticket

type Ticket struct {
	V         int       `json:"v"`
	ID        string    `json:"id"` // NewID
	Source    Source    `json:"source"`
	Workspace string    `json:"workspace"`
	Requester Requester `json:"requester"`
	Risk      Risk      `json:"risk"`
	Flags     Flags     `json:"flags"`
	Screen    Screen    `json:"screen"`
	PII       PII       `json:"pii"`
	// PlanHash binds the ticket to the plan it asks for (HashOf).
	PlanHash string `json:"plan_hash"`
	// PolicyFP is the fingerprint of the policy the plan was made under.
	PolicyFP string    `json:"policy_fp"`
	Created  time.Time `json:"created"`
	// Expires is when a pending ticket expires; nil never.
	Expires *time.Time `json:"expires"`
	// Renews, when set, makes this ticket the renewal of an expired one
	// (Renewal); absent otherwise.
	Renews *Renewal `json:"renews,omitempty"`
	Sig    string   `json:"sig,omitempty"`
}

Ticket is a request for a human decision, signed by the lock.

func NewTicket

func NewTicket(src Source, risk Risk, planHash, policyFP string, now time.Time, ttl time.Duration) *Ticket

NewTicket is a ticket of the current Version with a fresh ID, created now, expiring after ttl (0: never; see ClampTTL).

func VerifyTicket

func VerifyTicket(pub ed25519.PublicKey, data []byte) (*Ticket, error)

VerifyTicket decodes a ticket and checks it: schema, then the signature under pub, the lock key the caller pinned for t.Source.Instance. A ticket naming another key id is refused.

func (*Ticket) Expired

func (t *Ticket) Expired(now time.Time) bool

Expired reports whether pending ticket t has expired at now.

func (*Ticket) Marshal

func (t *Ticket) Marshal() ([]byte, error)

Marshal is the canonical JSON of a signed ticket, as it is sent.

func (*Ticket) Sign

func (t *Ticket) Sign(key ed25519.PrivateKey) error

Sign validates t, sets t.Source.KeyID to the key's id when empty (a different one is an error), and signs it.

func (*Ticket) Validate

func (t *Ticket) Validate() error

Validate checks t's schema: version, id, source, risk, hashes, times, sizes. It does not check the signature.

Directories

Path Synopsis
Package devicetest is a virtual WebAuthn authenticator for tests: it signs device assertions as a phone's platform authenticator would, so locks and the hub can test device-signed decisions without a browser.
Package devicetest is a virtual WebAuthn authenticator for tests: it signs device assertions as a phone's platform authenticator would, so locks and the hub can test device-signed decisions without a browser.

Jump to

Keyboard shortcuts

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