bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
package main
// device_plan.go — the approval device signing a ship plan (ops_plan.go).
//
// The pod files a DRAFT: the pipeline, the commit, the expiry, and the lines.
// The device makes the plan it will sign from it, filling in what only it may
// say:
//
// - each machine's key, from its OWN pins (pin-machine, typed by the person
// from what the machine printed when this device was pinned there) —
// a machine the device has not pinned makes the whole plan unsignable;
// - a fresh nonce, and a version one above the last it signed for that
// pipeline, so a machine can refuse an older plan;
// - whether the plan holds root work (any line with verbs), and, when it
// does, that the person's code was checked HERE before signing.
//
// What the person reads is rendered from the plan itself, line by line: the
// machine, the op, its params, the verbs with their arguments, the inputs it
// takes from earlier steps, and whether a line may be undone or adopts the
// live image. The words and the signed bytes come from the same plan.
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"sort"
"strings"
"time"
"unicode"
)
const planDeviceVersionsFile = "device_plan_versions.json"
// devicePlanDraft is what the pod sends in a ship_plan ask's details.
type devicePlanDraft struct {
Pipeline string `json:"pipeline"`
Commit string `json:"commit"`
Expires string `json:"expires"`
Lines []shipPlanLine `json:"lines"`
}
// devicePlanFromDetails builds the plan this device would sign (without its
// version, nonce or code flag, which are set at signing) and checks it.
func devicePlanFromDetails(details map[string]any) (*shipPlan, error) {
raw, err := json.Marshal(details["plan"])
if err != nil {
return nil, errors.New("the ask carries no plan")
}
var d devicePlanDraft
if err := json.Unmarshal(raw, &d); err != nil {
return nil, errors.New("the plan in the ask does not parse")
}
f, err := deviceKeyLoad()
if err != nil || f.Pub == "" {
return nil, errors.New("this machine is not an approval device (privatae-bastion device-init)")
}
pins := machinePinsLoad()
p := &shipPlan{V: shipPlanVersion, Kind: shipPlanKind, Pipeline: d.Pipeline, Version: 1, Commit: d.Commit,
Nonce: strings.Repeat("0", planNonceMin), Expires: d.Expires, Device: f.Pub, Machines: map[string]string{},
Lines: d.Lines}
for _, l := range d.Lines {
key, ok := pins[l.Machine]
if !ok {
return nil, fmt.Errorf("this device has not pinned %s's key: pin it here first (privatae-bastion pin-machine %s <key>, with the key %s printed when you pinned this device there)", l.Machine, l.Machine, l.Machine)
}
p.Machines[l.Machine] = key
if len(l.Verbs) > 0 {
p.Root = true
}
}
if err := planShape(p); err != nil {
return nil, err
}
return p, nil
}
// devicePlanWords renders the plan for the person: what they approve, line by
// line, from the plan's own fields.
func devicePlanWords(p *shipPlan) string {
var b strings.Builder
exp, _ := time.Parse(time.RFC3339, p.Expires)
fmt.Fprintf(&b, "Ship %s at commit %s: %d step(s), within %s.\n", wordsSafe(p.Pipeline), wordsSafe(truncate(p.Commit, 12)), len(p.Lines),
time.Until(exp).Round(time.Minute))
if p.Root {
b.WriteString("It runs ROOT work on your machines (the steps marked root below): approving it needs your code.\n")
}
for _, l := range p.Lines {
kind := "program only"
if len(l.Verbs) > 0 {
kind = "root"
}
fmt.Fprintf(&b, "%d. %s on %s: op %s (%s, %s) with %s\n", l.N, wordsSafe(l.Step), wordsSafe(l.Machine),
wordsSafe(l.Op), wordsSafe(truncate(l.OpSHA256, 16)), kind, wordsSafe(l.Params))
if l.Attempts > 1 {
fmt.Fprintf(&b, " may run up to %d times if it fails\n", l.Attempts)
}
names := make([]string, 0, len(l.Inputs))
for n := range l.Inputs {
names = append(names, n)
}
sort.Strings(names)
for _, n := range names {
m := planInputRe.FindStringSubmatch(l.Inputs[n])
if m == nil {
continue
}
if m[2] != "" {
fmt.Fprintf(&b, " %s = the digest of %s that step %s builds\n", wordsSafe(n), wordsSafe(m[2]), wordsSafe(m[1]))
} else {
fmt.Fprintf(&b, " %s = the digest step %s builds\n", wordsSafe(n), wordsSafe(m[1]))
}
}
for _, v := range l.Verbs {
keys := make([]string, 0, len(v.Args))
for k := range v.Args {
keys = append(keys, k)
}
sort.Strings(keys)
var args []string
for _, k := range keys {
args = append(args, wordsSafe(k)+"="+wordsSafe(v.Args[k]))
}
fmt.Fprintf(&b, " %s %s\n", wordsSafe(v.Verb), strings.Join(args, " "))
}
if l.Undo {
b.WriteString(" may be undone, to what that machine had before, if a later step fails\n")
}
if l.AdoptLive {
b.WriteString(" FIRST ROLL: takes the image running now as its starting point, without checking its history\n")
}
}
return strings.TrimRight(b.String(), "\n")
}
// wordsSafe is a field as the person may read it: no control character, no
// line break, no Unicode format or direction character, which could redraw,
// hide or reorder what they read. The plan's form refuses them already
// (planShape); this is what the words do whatever reached them.
func wordsSafe(s string) string {
var b strings.Builder
for _, r := range s {
if r < 0x20 || (r >= 0x7f && r <= 0x9f) || unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Zl, r) || unicode.Is(unicode.Zp, r) {
b.WriteRune('?')
continue
}
b.WriteRune(r)
}
return b.String()
}
// planPurpose names a plan ask for de-duplication: the same draft from the
// same pod is the same ask.
func planPurpose(p *shipPlan) string {
raw, _ := json.Marshal(p)
sum := sha256.Sum256(raw)
return shipPlanKind + "|" + p.Pipeline + "|" + hex.EncodeToString(sum[:])
}
// devicePlanSign signs the plan of an approved ask: a fresh nonce, the next
// version for the pipeline, and — for a root plan — the code, checked here
// against the factor enrolled on the pairing the ask came in on.
func devicePlanSign(r deviceRequest, code string) (string, error) {
p, err := devicePlanFromDetails(r.Details)
if err != nil {
return "", err
}
if p.Root {
if strings.TrimSpace(code) == "" {
return "", errors.New("this plan runs root work: approve it with your code")
}
dir, err := deviceFactorDir()
if err != nil {
return "", err
}
if _, err := stepupVerifyAndBurn(dir, code, time.Now()); err != nil {
return "", fmt.Errorf("your code: %v", err)
}
p.CodeGiven = true
}
f, err := deviceKeyLoad()
if err != nil || f.Priv == "" {
return "", errors.New("this machine holds no device key")
}
priv, err := base64.StdEncoding.DecodeString(f.Priv)
if err != nil || len(priv) != ed25519.PrivateKeySize {
return "", errors.New("bad device key file")
}
n := make([]byte, 16)
if _, err := rand.Read(n); err != nil {
return "", err
}
p.Nonce = hex.EncodeToString(n)
v, err := deviceNextPlanVersion(p.Pipeline)
if err != nil {
return "", err
}
p.Version = v
if err := planShape(p); err != nil {
return "", err
}
body, _ := json.Marshal(p)
env := shipPlanEnvelope{PayloadType: shipPlanPayloadType, Payload: base64.StdEncoding.EncodeToString(body),
Signature: base64.StdEncoding.EncodeToString(ed25519.Sign(ed25519.PrivateKey(priv), dssePAE(shipPlanPayloadType, body)))}
out, _ := json.Marshal(env)
return string(out), nil
}
// deviceFactorDir is where the person's own factor for this device is
// enrolled: the anchor pairing's (the device lane answers only its anchor,
// deviceLaneOnly), or the only pairing of an unanchored device. Never a
// folder the ask names: the pod sends the ask.
func deviceFactorDir() (string, error) {
cfgs, err := loadPairings()
if err != nil {
return "", errors.New("this device's pairings cannot be read, so there is no factor to check your code against")
}
var pick *Config
if a := currentAnchor(); a != nil {
for _, c := range cfgs {
if c != nil && c.HostName == a.AnchorHost {
pick = c
}
}
} else if len(cfgs) == 1 {
pick = cfgs[0]
}
if pick == nil {
return "", errors.New("this device has no pairing its factor belongs to (anchor it, or keep one pairing)")
}
dir := pick.dcnDir()
if dir == "" {
return "", errors.New("this device's pairing has no folder for its factor")
}
return dir, nil
}
// deviceNextPlanVersion is one above the last version this device signed for
// the pipeline, recorded before it is used.
func deviceNextPlanVersion(pipeline string) (int64, error) {
m := planLedgerLoad(planDeviceVersionsFile)
var v int64
_, _ = fmt.Sscan(m[pipeline], &v)
v++
m[pipeline] = fmt.Sprint(v)
if err := planLedgerSave(planDeviceVersionsFile, m); err != nil {
return 0, fmt.Errorf("could not record the plan's version: %v", err)
}
return v, nil
}
// devicePlanLive: a signed plan envelope whose plan has not expired.
func devicePlanLive(approval string, now time.Time) bool {
var env shipPlanEnvelope
if json.Unmarshal([]byte(strings.TrimSpace(approval)), &env) != nil || env.PayloadType != shipPlanPayloadType {
return false
}
body, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
return false
}
var p shipPlan
if json.Unmarshal(body, &p) != nil {
return false
}
exp, err := time.Parse(time.RFC3339, p.Expires)
return err == nil && exp.After(now)
}
// deviceNewPlanRequest files a ship_plan ask. A root plan's code is checked,
// when the person approves, against the factor enrolled on this device's own
// pairing (deviceFactorDir), never one the ask names.
func deviceNewPlanRequest(details map[string]any) (string, error) {
p, err := devicePlanFromDetails(details)
if err != nil {
return "", err
}
purpose := planPurpose(p)
machines := make([]string, 0, len(p.Machines))
for h := range p.Machines {
machines = append(machines, h)
}
sort.Strings(machines)
server := strings.Join(machines, ", ")
deviceMu.Lock()
defer deviceMu.Unlock()
m := deviceRequestsLoad()
for _, r := range m {
if r.Purpose == purpose && r.State == "denied" {
return "", errors.New("you refused this on your device; it cannot be asked again for 24h")
}
}
for id, r := range m {
if r.Purpose == purpose && (r.State == "pending" || (r.State == "approved" && !r.Delivered && devicePlanLive(r.Approval, time.Now()))) {
return id, nil
}
}
id, err := newID()
if err != nil {
return "", err
}
m[id] = deviceRequest{Purpose: purpose, Words: devicePlanWords(p), Server: server, Details: details,
At: time.Now().UTC().Format(time.RFC3339), State: "pending", Plan: true, NeedsCode: p.Root}
if err := deviceRequestsSave(m); err != nil {
return "", err
}
return id, nil
}
// cmdPinMachine / cmdUnpinMachine: the person, at their device.
func cmdPinMachine(host, key string, unpin bool) error {
if _, err := deviceKeyLoad(); err != nil {
return errors.New("this machine is not an approval device (privatae-bastion device-init)")
}
if unpin {
if err := unpinMachineKey(host); err != nil {
return err
}
fmt.Printf("%s unpinned; this device signs no plan for it until it is pinned again\n", host)
return nil
}
if err := pinMachineKey(host, key); err != nil {
return err
}
fmt.Printf("%s pinned; this device may now sign ship plans that include it\n", host)
return nil
}
// machineKeyLine is what a machine prints when this device is pinned there:
// the command the person runs on the device to pin the machine back.
func machineKeyLine() string {
pub, _, err := loadOrCreateOwnerKey()
if err != nil {
return ""
}
hn, _ := os.Hostname()
return "privatae-bastion pin-machine " + hn + " " + base64.StdEncoding.EncodeToString(pub)
}