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 DeviceID(cred []byte) 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 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) Marshal() ([]byte, error)
- func (d *Decision) Match(t *Ticket) 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 Outcome
- type PII
- type PinnedFacade
- type Renewal
- type RequestKind
- type Requester
- type RevealAnswer
- type RevealKey
- type Risk
- type Screen
- type Sealed
- type Source
- type Ticket
Constants ¶
const ( FacadeTTY = "tty" FacadeDeck = "deck" )
Local facades: they run on the host, at the human's own terminal.
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 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 tickets and decisions this package writes and reads. A value of any other version is refused (fail closed).
Variables ¶
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 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 Risks = []Risk{RiskRead, RiskReadPII, 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 DeviceID ¶ added in v0.4.0
DeviceID is the id of the device whose WebAuthn credential id is cred.
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 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" )
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 ¶
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 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 ¶
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.
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.
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.
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.
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; production writes and unlock (a secret typed for the lock) 4 h.
func (*Risk) UnmarshalJSON ¶
UnmarshalJSON refuses a risk class outside 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
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).
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 ¶
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.
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. |