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
- Variables
- func CanReveal(facade string) bool
- func Canonical(v any) ([]byte, error)
- func CanonicalizeJSON(b []byte) ([]byte, error)
- func ClampTTL(r Risk, configured time.Duration) time.Duration
- func ClearScreenAAD(id, dev string) []byte
- func ConnectPlanHash(subj Subject) (string, error)
- func DeviceID(cred []byte) string
- func EncodeX25519(pub *ecdh.PublicKey) string
- func HashOf(v any) (string, error)
- func InputDigests(v any) (map[string]string, error)
- func KeyID(pub ed25519.PublicKey) string
- func MarshalDevices(devs []Device) ([]byte, error)
- func NewID(now time.Time) string
- func NewNonce() string
- func Open(priv *ecdh.PrivateKey, s *Sealed, aad []byte) ([]byte, error)
- func RequestMessage(method, pathQuery, instance string, at time.Time, nonce string, body []byte) []byte
- func RevealAAD(r *RevealKey) []byte
- func RiskNames() string
- func SecretAAD(t *Ticket) []byte
- func SignBytes(key ed25519.PrivateKey, c Context, msg []byte) string
- func TicketHash(signed []byte) string
- func ValidDeviceID(s string) bool
- func ValidID(s string) bool
- func VerifyAssertion(dev *Device, challenge []byte, a *DeviceAssertion) (uint32, error)
- func VerifyBytes(pub ed25519.PublicKey, c Context, msg []byte, sig string) error
- func VerifyDevice(dev *Device, d *Decision, signed []byte) error
- func VerifyKeyCert(signer ed25519.PublicKey, c Context, bind string, data []byte) (*ecdh.PublicKey, error)
- type Approver
- type Change
- type Context
- type Decision
- func (d *Decision) Auto() bool
- func (d *Decision) Marshal() ([]byte, error)
- func (d *Decision) Match(t *Ticket) error
- func (d *Decision) OpenSecret(t *Ticket, priv *ecdh.PrivateKey) ([]byte, error)
- func (d *Decision) Sign(key ed25519.PrivateKey) error
- func (d *Decision) Statement(dev, ticketHash string) DeviceStatement
- func (d *Decision) Usable(t *Ticket, now time.Time, ttl time.Duration) error
- func (d *Decision) Validate() error
- type Device
- type DeviceAssertion
- type DeviceBox
- type DeviceSet
- type DeviceStatement
- type FacadeKey
- type FacadeSet
- type Flags
- type HubRequest
- type KeyCert
- type Kind
- type Outcome
- type PII
- type PinnedFacade
- type Renewal
- type RequestKind
- type Requester
- type RevealAnswer
- type RevealKey
- type Risk
- type Rule
- type Screen
- type Sealed
- type Source
- type Subject
- type Ticket
- func NewConnectTicket(src Source, risk Risk, subj Subject, policyFP string, now time.Time, ...) (*Ticket, error)
- func NewKindTicket(src Source, k Kind, risk Risk, subj *Subject, planHash, policyFP string, ...) *Ticket
- func NewTicket(src Source, risk Risk, planHash, policyFP string, now time.Time, ...) *Ticket
- func VerifyTicket(pub ed25519.PublicKey, data []byte) (*Ticket, error)
Constants ¶
const ( FacadeTTY = "tty" FacadeDeck = "deck" )
Local facades: they run on the host, at the human's own terminal.
const ( CredentialAsk = "ask" CredentialKeychain = "keychain" CredentialVault = "vault" CredentialNone = "none" )
Credential sources a connect ticket names (Subject.Credential): where the lock gets the backend's secret. They mirror the policy's credentials modes, plus CredentialNone for a backend without one.
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.
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.
const ( RuleAuto = "auto" RuleHuman = "human" )
Who decided.
const FacadeMobile = "mobile"
FacadeMobile is the facade name of decisions taken on a device.
const MaxChanges = 64
MaxChanges bounds Renewal.Changes.
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).
const MaxDevices = 8
MaxDevices bounds the devices a lock pins.
const MaxFacades = 16
MaxFacades bounds a FacadeSet.
const RevealTTL = 2 * time.Minute
RevealTTL is how long a RevealKey and the request carrying it can be acted on.
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.
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.
const UncheckedInput = "unchecked"
UncheckedInput is the digest a Recompute gives an input it cannot check now; DiffInputs reports it as ChangeUnchecked.
const Version = 1
Version is the schema version of the values this package writes and reads; tickets and decisions are also read and written at VersionKinds. A value of any other version is refused (fail closed).
const VersionKinds = 2
VersionKinds is the schema version of tickets that name what they ask for (Kind, Subject) and of decisions that name what decided them (Rule) and may carry a sealed secret (Secret). A version-1 value has none of these members and keeps verifying as it did: Version stays the version of every other signed value, and of the tickets NewTicket makes.
A lock pinned to a lockkit that knows VersionKinds reads both versions; a hub or a lock that does not refuses a version-2 value (fail closed).
Variables ¶
var ErrAuto = errors.New("ticket: auto decision on a never-auto ticket")
ErrAuto: an auto rule decided a ticket that only a human may decide.
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.
var ErrDenied = errors.New("ticket: not approved")
ErrDenied: the decision is not an approval.
var ErrDevice = errors.New("ticket: device signature refused")
ErrDevice: a device assertion does not verify, or a decision that needs one has none.
var ErrExpired = errors.New("ticket: expired")
ErrExpired: the ticket or the approval is past its TTL.
var ErrFacade = errors.New("ticket: facade key refused")
ErrFacade: a reveal key does not verify under a facade the lock pins.
var ErrInvalid = errors.New("ticket: invalid")
ErrInvalid is wrapped by every validation error.
var ErrMismatch = errors.New("ticket: decision does not match the ticket")
ErrMismatch: a verified decision is not for this ticket.
var ErrNoSecret = errors.New("ticket: no secret for the lock")
ErrNoSecret: a connect decision carries no secret where the lock asked for one, or one where it did not.
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.
var ErrOpen = errors.New("ticket: sealed box does not open")
ErrOpen: a sealed box does not open with this key and associated data.
var ErrTooLarge = errors.New("ticket: clear screen too large to seal")
ErrTooLarge: a clear screen over MaxClearScreen.
var Kinds = []Kind{KindConnect, KindRead, KindWrite, KindDDL, KindAction, KindEndpoint, KindPolicy}
Kinds lists every kind.
var Risks = []Risk{RiskRead, RiskReadPII, RiskGrant, RiskWrite, RiskProdWrite, RiskPolicyLoosen, RiskUnlock}
Risks lists every risk class, from the least to the most sensitive.
Functions ¶
func CanReveal ¶ added in v0.5.0
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 ¶
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 ¶
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 ¶
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
ClearScreenAAD is the associated data of ticket id's clear screen sealed to device dev.
func ConnectPlanHash ¶ added in v0.13.0
ConnectPlanHash is the plan hash of a connect ticket: the hash of its subject. A decision on it binds exactly that profile, endpoint, credential source and secret key.
func DeviceID ¶ added in v0.4.0
DeviceID is the id of the device whose WebAuthn credential id is cred.
func EncodeX25519 ¶ added in v0.13.0
EncodeX25519 is pub as a Subject.SecretTo carries it.
func HashOf ¶
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
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 ¶
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
MarshalDevices is the canonical DeviceSet of devs, as ParseDevices reads it.
func NewID ¶
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 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
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 SecretAAD ¶ added in v0.13.0
SecretAAD binds a sealed secret to the connect ticket it answers: its id, instance, lock key and plan hash. A box sealed for one ticket does not open as another's.
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
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
ValidDeviceID reports whether s has the form of a device id.
func ValidID ¶
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 ¶
VerifyBytes checks sig, as SignBytes returns it, over msg under c.
func VerifyDevice ¶ added in v0.4.0
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.
Types ¶
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
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" )
const ContextSecret Context = "lockkit/connect-secret/v1"
ContextSecret is the associated data of a connect decision's secret.
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"`
// Rule is what decided a VersionKinds decision (required there,
// absent from a Version one), for the lock's audit.
Rule *Rule `json:"rule,omitempty"`
// Secret, on an approval of a connect ticket that asked for one
// (Subject.SecretTo), is the backend's secret sealed to the lock
// (SealSecret, OpenSecret): the hub relays it without reading it.
Secret *Sealed `json:"secret,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 ¶
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) Match ¶
Match checks that d answers t: same ticket id, instance, lock key and plan hash; a VersionKinds ticket is answered by a VersionKinds decision (which names its rule), and only a connect ticket that asked for a secret gets one. A lock calls it with its own ticket (never one rebuilt from the decision) before acting on d.
func (*Decision) OpenSecret ¶ added in v0.13.0
OpenSecret opens the secret of d, a decision that Match and Usable accepted for the lock's own connect ticket t, with priv, the one-use key whose public half is t.Subject.SecretTo. A ticket that asked for a secret and an approval without one (or the reverse) is ErrNoSecret; a box that does not open is ErrOpen. The caller wipes the secret after use (secrets.Wipe).
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 ¶
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, anything but an approval is ErrDenied, and an auto rule's approval of a Flags.NeverAuto ticket is ErrAuto.
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
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
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
SealingKey is d's X25519 key.
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 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 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. Always set on a
// RiskGrant ticket (Validate refuses one without it).
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 Kind ¶ added in v0.13.0
type Kind string
Kind is the closed set of what a version-2 ticket asks for, so that a decider (`lox run`'s rules, a facade) rules on a structured request, not on the screen's text alone. Each kind goes with the risk classes KindRisks allows.
const ( // KindConnect: open the lock's backend connection for one profile // (Subject.Profile, Endpoint, Credential). A lock asks once per // connection: the connection lives until it is idle (lazyconn). KindConnect Kind = "connect" // KindRead: read through an open connection (a query, a grant's scope). KindRead Kind = "read" // KindWrite: change data. KindWrite Kind = "write" // KindDDL: change the schema. KindDDL Kind = "ddl" // KindAction: a verb that is neither a read nor a write of rows: an MQ // publish or purge, a file move, an HTTP method (Subject.Verb, Target). KindAction Kind = "action" // KindEndpoint: reach a new host or route (lockhttp; Subject.Endpoint, // Verb, Target). KindEndpoint Kind = "endpoint" // KindPolicy: loosen the policy in force. KindPolicy Kind = "policy" )
func ParseKind ¶ added in v0.13.0
ParseKind returns the kind named s; any other name is an error that does not quote s.
func (*Kind) UnmarshalJSON ¶ added in v0.13.0
UnmarshalJSON refuses a kind outside Kinds.
type Outcome ¶
type Outcome string
Outcome is the closed set of decisions.
func (*Outcome) UnmarshalJSON ¶
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
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.
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 ¶
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.
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" RiskGrant Risk = "grant" RiskWrite Risk = "write" RiskProdWrite Risk = "prod-write" RiskPolicyLoosen Risk = "policy-loosen" RiskUnlock Risk = "unlock" )
The risk classes. RiskGrant asks for a grant: one approval for a bounded read scope (database, tables, duration, read budget) that the lock then runs reads inside without a ticket each. The scope is the plan: PlanHash is the hash of its canonical JSON (HashOf), so a decision binds exactly that scope, and the lock enforces it. A grant ticket is always NeverAuto.
func KindRisks ¶ added in v0.13.0
KindRisks lists the risk classes a ticket of kind k may carry (nil for an unknown kind).
func (Risk) TTL ¶
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; grants, production writes and unlock (a secret typed for the lock) 4 h. A grant's own lifetime is part of its scope, not of the ticket.
func (*Risk) UnmarshalJSON ¶
UnmarshalJSON refuses a risk class outside Risks.
type Rule ¶ added in v0.13.0
Rule is what decided a version-2 decision, for the lock's audit: one of the decider's auto rules (By RuleAuto, ID the rule's id, as `lox run` names it), or a human on Decision.Facade (By RuleHuman, no ID).
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
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 ¶
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
SealGCM is Seal with SealAlgGCM, for a recipient that opens boxes with WebCrypto.
func SealSecret ¶ added in v0.13.0
SealSecret seals secret for connect ticket t, to the key the lock put in t.Subject.SecretTo: what a decider (`lox run`) puts in Decision.Secret. The caller verified t first (VerifyTicket, under the lock key it pins): the signature is what binds SecretTo to the lock, so a hub cannot slip in a key of its own.
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 Subject ¶ added in v0.13.0
type Subject struct {
// Profile is the lock profile the request runs under.
Profile string `json:"profile,omitempty"`
// Endpoint is the backend (connect: "db.internal:5432/shop") or the
// new host (endpoint: "api.example.com"): host[:port][/path] only,
// without userinfo, query, options or whitespace (validEndpoint).
Endpoint string `json:"endpoint,omitempty"`
// Credential is where the lock gets the secret (connect only): one of
// the Credential* sources.
Credential string `json:"credential,omitempty"`
// SecretTo, on a connect ticket, asks the decider for the secret: it
// is the lock's one-use X25519 public key (32 bytes, base64url) for
// this ticket, signed with the ticket. The decision carries the
// secret sealed to it (Decision.Secret): the hub relays a box it
// cannot open. Set when the profile is "ask", or its Vault token has
// expired.
SecretTo string `json:"secret_to,omitempty"`
// Verb is an action's verb ("publish", "purge", "GET") or an
// endpoint's method.
Verb string `json:"verb,omitempty"`
// Target is what the verb acts on (a queue, a path, a route, a table),
// without query string, fragment or whitespace (validTarget).
Target string `json:"target,omitempty"`
}
Subject is the structured request of a version-2 ticket, what a decider's rules match on. Like the screen, it leaves the lock and the hub reads it: the lock puts in it only names from its own configuration (a profile, an endpoint without userinfo, a credential source) and names it masks as it masks the screen (a queue, a route), never a secret, a value or a row.
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"`
// Kind is what a VersionKinds ticket asks for, and Subject its
// structured request; a Version ticket has neither.
Kind Kind `json:"kind,omitempty"`
Subject *Subject `json:"subject,omitempty"`
Sig string `json:"sig,omitempty"`
}
Ticket is a request for a human decision, signed by the lock.
func NewConnectTicket ¶ added in v0.13.0
func NewConnectTicket(src Source, risk Risk, subj Subject, policyFP string, now time.Time, ttl time.Duration) (*Ticket, error)
NewConnectTicket is the connect ticket for subj, whose plan is subj itself (ConnectPlanHash).
func NewKindTicket ¶ added in v0.13.0
func NewKindTicket(src Source, k Kind, risk Risk, subj *Subject, planHash, policyFP string, now time.Time, ttl time.Duration) *Ticket
NewKindTicket is NewTicket for a version-2 ticket of kind k, asking for subj (nil only for read, write, ddl and policy). Sign validates the pair: a kind with a risk it does not allow, or a subject that is not the kind's, is refused.
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). A RiskGrant ticket starts with Flags.NeverAuto set.
func VerifyTicket ¶
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) EffectiveKind ¶ added in v0.13.0
EffectiveKind is t's kind; for a version-1 ticket, the kind its risk implies (read for the read classes and grants, write for writes, policy for policy loosening, connect for unlock).
Source Files
¶
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. |