Documentation
¶
Overview ¶
Package hubclient is a lock's client for lockhub, the approval hub: it pushes signed tickets, long-polls the hub's decisions and turns an approval into a plan that runs once.
Every request but Hello is signed with the lock instance's key (the Lock-Instance, Lock-Time, Lock-Nonce and Lock-Sig headers, see ticket.RequestMessage); every decision is verified with the hub key the lock pinned, never one the hub sends. A screen leaves the lock only as a render.MaskedScreen.
The client fails closed: a hub that cannot be reached leaves the plan pending, and nothing falls back to another approver. A ticket the hub did not acknowledge is pushed again by the next Submit of its plan or by Sync.
Index ¶
- Constants
- Variables
- func AnswerCode(err error) string
- func HubKeyCommand(w io.Writer, home string, args []string) error
- func InstanceKey(home, instance string) (ed25519.PrivateKey, error)
- func KeyDir(home string) string
- func ParseRisks(s string) ([]ticket.Risk, error)
- func ReadDevices(path string) ([]ticket.Device, error)
- func ReadFacades(path string) ([]ticket.PinnedFacade, error)
- func ReadHubKey(path string) (ed25519.PublicKey, error)
- func ValidInstance(name string) bool
- type Client
- func (c *Client) Begin(st *plans.Store, ticketID string, caller plans.Caller, recheck Recheck) (*plans.Plan, *ticket.Decision, error)
- func (c *Client) Finish(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ...) error
- func (c *Client) HandleRequests(ctx context.Context, st *plans.Store) error
- func (c *Client) Hello(ctx context.Context) (Hello, error)
- func (c *Client) OnRequest(f func(q *ticket.HubRequest, renewal string, err error))
- func (c *Client) Poll(ctx context.Context, wait time.Duration) (int, error)
- func (c *Client) Renew(ctx context.Context, st *plans.Store, ticketID string, caller plans.Caller) (string, error)
- func (c *Client) SetRecompute(r Recompute)
- func (c *Client) Submit(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ...) (string, error)
- func (c *Client) SubmitClear(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ...) (string, error)
- func (c *Client) Sync(ctx context.Context, st *plans.Store) error
- func (c *Client) Watch(ctx context.Context, st *plans.Store, report func(error))
- func (c *Client) Withdraw(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ...) error
- type Config
- type Hello
- type HubError
- type Recheck
- type Recompute
Constants ¶
const ( // MaxPending bounds the pending tickets of an instance. MaxPending = 20 // RateN new tickets per RateWindow at most. RateN = 10 RateWindow = 5 * time.Minute // MaxWait bounds a long poll (the hub's own bound). MaxWait = 60 * time.Second // MaxInbox bounds the decisions kept for plans not yet run. MaxInbox = plans.MaxPlans // MaxRequests bounds the hub requests waiting to be handled. MaxRequests = 64 )
Defaults.
const ( CodeFacadeRefused = "facade_refused" // no pinned facade, or not this one CodeNotPending = "not_pending" // nothing to reveal: the ticket is not pending CodeNoClear = "no_clear_screen" CodeNotRenewable = "not_renewable" // not expired, or gone CodeRecheckFailed = "recheck_failed" CodePolicyChanged = "policy_changed" // plan again CodeLimited = "rate_limited" CodeFailed = "failed" )
Answer codes: the only error text a lock sends the hub for a request.
Variables ¶
var ( // ErrUnreachable: the hub did not answer, or answered 5xx. A ticket // signed for the plan stays pending. ErrUnreachable = errors.New("hubclient: hub unreachable") // ErrRateLimited: too many pending tickets or too many new tickets in // the rate window, here or at the hub; no ticket was opened. ErrRateLimited = errors.New("hubclient: too many tickets") // ErrPending: no decision on the ticket yet. ErrPending = errors.New("hubclient: no decision yet") // ErrDenied: the human denied the ticket; the plan is Denied. ErrDenied = errors.New("hubclient: denied") // ErrBadDecision: the hub sent a decision that does not verify under // the pinned hub key or is not for this instance; it was dropped. ErrBadDecision = errors.New("hubclient: decision refused") )
var ErrBadRequest = errors.New("hubclient: hub request refused")
ErrBadRequest: a hub request that does not verify, is not for this lock, is stale, or names a facade the lock does not pin.
var ErrNotRenewable = errors.New("hubclient: ticket cannot be renewed")
ErrNotRenewable: the ticket cannot be renewed (not expired, or gone).
Functions ¶
func AnswerCode ¶ added in v0.5.0
AnswerCode is the code a lock answers the hub for a request that failed with err.
func HubKeyCommand ¶
HubKeyCommand is a lock's `hub-key --instance NAME`: it prints the public key of the instance (InstanceKey of home, "" the user's, created on the first call) in base64 std on one line, for `lock up` to pin at the hub.
func InstanceKey ¶
func InstanceKey(home, instance string) (ed25519.PrivateKey, error)
InstanceKey returns the Ed25519 key of instance, kept in KeyDir(home)/<instance>.key (0600 in a 0700 directory, the seed in base64 std on one line); home "" is the user's home. The first call creates it; a key file group or others can read is refused.
func ParseRisks ¶ added in v0.4.0
ParseRisks reads a comma-separated list of risk classes ("" is none), as --require-device takes it.
func ReadDevices ¶ added in v0.4.0
ReadDevices reads the pinned approval devices: a ticket.DeviceSet (the file `lock up` mounts at /run/lockfamily/devices.json, written from the devices the human confirmed at the host's terminal).
func ReadFacades ¶ added in v0.5.0
func ReadFacades(path string) ([]ticket.PinnedFacade, error)
ReadFacades reads the facade keys a lock pins: a ticket.FacadeSet (the file `lock up` mounts at /run/lockfamily/facades.json, written from the facade keys the human reviewed at the host's terminal).
func ReadHubKey ¶
ReadHubKey reads the pinned hub key: an Ed25519 public key in base64 std on one line (the file `lock up` mounts at /run/lockfamily/hub.pub).
func ValidInstance ¶
ValidInstance reports whether name is a valid lock instance name.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client talks to one hub for one lock instance. It is safe for concurrent use.
func (*Client) Begin ¶
func (c *Client) Begin(st *plans.Store, ticketID string, caller plans.Caller, recheck Recheck) (*plans.Plan, *ticket.Decision, error)
Begin turns the decision on ticketID into a run of its plan, for caller (the agent that made the plan): the decision verifies under the pinned hub key and matches the ticket the lock signed (ticket.Decision.Match), is still usable, and the plan's binding has not moved (recheck, plans.Store.Check). The plan comes back Executing under the decision's nonce, consumed: the product runs it, then calls Finish.
Begin also returns the verified decision, for the lock's audit (who approved, on which facade, the hub's signature), with the plan or with the error of a decision that was matched but not acted on.
A decision taken on a device (facade mobile) must carry the signature of a pinned device over the lock's own signed ticket (ticket.VerifyDevice), and with Config.RequireDevice an approval of those risk classes must.
No decision yet is ErrPending; a denial ends the plan Denied and is ErrDenied with the human's reason; a decision too old or past the ticket ends it Expired. An expired ticket is ticket.ErrExpired: the product renews it (Renew) and answers pending with the new ticket.
func (*Client) Finish ¶
func (c *Client) Finish(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ok bool, reason string) error
Finish ends an executing plan (plans.Store.Finish) and tells the hub its ticket was consumed. The plan is finished even when the hub cannot be told; the error says so.
func (*Client) HandleRequests ¶ added in v0.5.0
HandleRequests acts on the hub requests the last polls brought (Watch calls it after each Poll): a reveal seals the plan's clear screen to the facade's ephemeral key, a renewal asks again for an expired ticket. Each request is answered at the hub, once.
func (*Client) OnRequest ¶ added in v0.5.0
func (c *Client) OnRequest(f func(q *ticket.HubRequest, renewal string, err error))
OnRequest sets f, told of every hub request the lock acted on or refused (q nil when it did not verify), with the new ticket of a renewal: for the lock's audit and console.
func (*Client) Poll ¶
Poll long-polls the hub once, waiting up to wait (at most MaxWait) for decisions after the cursor, keeps those that verify under the pinned hub key for this instance, and advances and saves the cursor. It returns how many decisions it kept; decisions it dropped make ErrBadDecision.
func (*Client) Renew ¶ added in v0.5.0
func (c *Client) Renew(ctx context.Context, st *plans.Store, ticketID string, caller plans.Caller) (string, error)
Renew asks again for the expired ticket ticketID of caller (decision H1): a new ticket for the same plan, re-checked now (SetRecompute), that says what changed in its plan-hash inputs. It returns the new ticket; a ticket renewed already returns its renewal. The agent runs the new ticket once it is decided.
func (*Client) SetRecompute ¶ added in v0.5.0
SetRecompute sets the product's Recompute.
func (*Client) Submit ¶
func (c *Client) Submit(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, t *ticket.Ticket, screen render.MaskedScreen) (string, error)
Submit asks the hub for a decision on plan planID of caller and returns the ticket id. t is the product's ticket for the plan (NewTicket; its plan hash and policy fingerprint the plan's); Submit sets its instance, key id, screen and touched kinds from screen, signs it, records it in st and pushes it.
Submit is idempotent: a plan already pending returns its ticket, and a pending plan of caller with the same plan hash lends its ticket instead of opening a new one. A screen MaskScreen did not build is refused (render.ErrUnmasked). With the hub unreachable, Submit returns the ticket id with ErrUnreachable: the plan is pending, and the ticket goes out on a later Submit or Sync.
func (*Client) SubmitClear ¶ added in v0.4.0
func (c *Client) SubmitClear(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, t *ticket.Ticket, screen render.MaskedScreen, clear *ticket.Screen) (string, error)
SubmitClear is Submit with the clear screen of the request: with Config.SealClear, it is sealed to each pinned device (ticket.SealClearScreen) in the ticket, which the hub relays without being able to read it; otherwise, with clear nil, or a clear screen over ticket.MaxClearScreen, it is not sent at all.
func (*Client) Sync ¶
Sync pushes every pending ticket the hub has not acknowledged since this client started: after a restart of the lock or of the hub. It also withdraws at the hub the tickets of plans withdrawn while the lock ran without it (plans.Store.WithdrawAll, Untold).
func (*Client) Watch ¶
Watch polls until ctx ends, backing off while the hub is unreachable and resending unacknowledged tickets (Sync) when it answers again. Errors go to report (nil: dropped).
func (*Client) Withdraw ¶
func (c *Client) Withdraw(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, reason string) error
Withdraw ends a plan not yet run as Withdrawn and withdraws its ticket at the hub. The plan is withdrawn even when the hub cannot be told; the error says so.
type Config ¶
type Config struct {
// URL is the hub's base URL ("http://lockhub:8443").
URL string
// Instance is the lock instance's name (Lock-Instance, Source.Instance).
Instance string
// Key is the instance's Ed25519 key: it signs tickets and requests.
Key ed25519.PrivateKey
// HubKey is the pinned hub key decisions must verify under.
HubKey ed25519.PublicKey
// StateFile keeps the decision cursor and the decisions not yet acted
// on, in the lock's volume (written 0600).
StateFile string
// ApprovalTTL is the instance's approval lifetime (ticket.ClampTTL; 0
// keeps each risk class's).
ApprovalTTL time.Duration
// MaxPending, RateN and RateWindow override the defaults when > 0.
MaxPending int
RateN int
RateWindow time.Duration
// HTTP is the HTTP client (nil: one without a global timeout; long
// polls are bounded by MaxWait). Redirects are refused unless it sets
// its own CheckRedirect.
HTTP *http.Client
// PlainHTTP (the flag --hub-plain-http) allows an http:// URL to a hub
// on a network of its own (lock up's): a service name without a dot or
// a private address only. Off, http is refused but to a loopback
// address; use https. Requests and decisions are signed either way.
PlainHTTP bool
// Devices are the approval devices the lock pins (--devices-file): a
// decision that carries a device signature, or that names the mobile
// facade, is kept only if it verifies under one of them, over the
// lock's own signed ticket (ticket.VerifyDevice).
Devices []ticket.Device
// SealClear (--seal-clear) seals each ticket's clear screen to every
// pinned device (SubmitClear). Off, clear screens never leave the
// lock. The device's code comes from the hub: see the ticket package
// on what a hub that serves malicious code can read.
SealClear bool
// RequireDevice lists the risk classes whose approvals must be signed
// by a pinned device, whatever facade the hub says they came from
// (--require-device): an approval without one ends the plan Denied.
RequireDevice []ticket.Risk
// Facades are the facade keys the lock pins (--facade-keys-file): a
// reveal is sealed only to a key one of them signed.
Facades []ticket.PinnedFacade
// Now is the clock (nil: time.Now).
Now func() time.Time
}
Config configures a Client. URL, Instance, Key, HubKey and StateFile are required: there is no discovery and no unpinned hub.
type Hello ¶
type Hello struct {
Name string `json:"name"`
Version string `json:"version"`
API int `json:"api"`
}
Hello is the hub's answer to GET /v1/hello.
type Recheck ¶
Recheck recomputes, just before running, the policy fingerprint and plan hash of a plan from its body and the lock's current state.
type Recompute ¶ added in v0.5.0
Recompute gives, for a stored plan, its policy fingerprint, plan hash and plan-hash inputs (ticket.InputDigests) as the lock sees them now: a renewal shows the human what moved since the expired ticket. A product sets it with SetRecompute; without one, a renewal keeps the plan's hash and says it was not checked (the run checks it anyway).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package hubtest is a fake lockhub for the tests of locks: it speaks the lock API v1 (signed requests, tickets, decisions with a cursor) on an httptest server, and lets a test decide tickets.
|
Package hubtest is a fake lockhub for the tests of locks: it speaks the lock API v1 (signed requests, tickets, decisions with a cursor) on an httptest server, and lets a test decide tickets. |