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
+366
View File
@@ -0,0 +1,366 @@
package main
// coder_grant.go — the STANDING CODER GRANT on an anchored box.
//
// Before coder mode, every harness run on an anchored box spent a fresh
// approval from the person (harness.go harnessGate). A Host that places 20
// coding runs on a machine cannot ask its owner 20 times, so the person
// approves ONCE: this host may start coder runs here, of these engines, at most
// this many at a time, until this date. `privatae-bastion coders drop` (or the
// host's revoke, coder_grant_drop) takes it back.
//
// Built exactly like the service reach grant (service_reach.go): the same
// <pairing dir>/grants.json (a `coder` map), grantsMu, the pinned entity key,
// the KIND checked, the SUBJECT enforced (a host installs only its own grant),
// and the box's own approval spent once at install.
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
)
const coderGrantKind = "coder_grant"
// coderTerms is parsed FROM the verified canonical bytes — never from anything else.
type coderTerms struct {
Kind string `json:"kind"`
Server string `json:"server"`
Subject string `json:"subject"`
Engines []string `json:"engines"`
Max int `json:"max"`
Expires string `json:"expires"`
TTLDays int `json:"ttl_days,omitempty"`
Approval string `json:"approval,omitempty"`
}
// coderVerifyRec verifies one coder grant: signature, kind, a stated future
// expiry, and well-formed terms. Re-run on every use.
func coderVerifyRec(pinnedKeyB64 string, rec dcnGrantRec) (*coderTerms, error) {
canonical, err := verifySignedUnexpired(pinnedKeyB64, rec)
if err != nil {
return nil, err
}
var t coderTerms
if err := json.Unmarshal(canonical, &t); err != nil {
return nil, fmt.Errorf("signed terms unparsable")
}
if t.Kind != coderGrantKind {
return nil, fmt.Errorf("signed record is a %q, not a %s", t.Kind, coderGrantKind)
}
if strings.TrimSpace(t.Subject) == "" || strings.ContainsAny(t.Subject, "|\n ") {
return nil, fmt.Errorf("grant names no host")
}
if t.Max < 1 || t.Max > 100 {
return nil, fmt.Errorf("grant max must be 1-100")
}
if len(t.Engines) == 0 {
return nil, fmt.Errorf("grant names no engine")
}
for _, e := range t.Engines {
if !isCoderEngine(e) {
return nil, fmt.Errorf("grant names an engine that is not a coding engine")
}
}
return &t, nil
}
// purposeForCoderGrant is what the person approves, the same string the host
// builds: coder_grant|<server>|<subject>|engines:<sorted,comma>|max:<n>|ttl:<days>.
func purposeForCoderGrant(t *coderTerms) (string, error) {
eng := append([]string(nil), t.Engines...)
sort.Strings(eng)
if err := purposeBad([]string{t.Server, t.Subject}, eng); err != nil {
return "", err
}
return coderGrantKind + "|" + t.Server + "|" + t.Subject + "|engines:" + strings.Join(eng, ",") +
"|max:" + strconv.Itoa(t.Max) + "|ttl:" + strconv.Itoa(t.TTLDays), nil
}
// coderGrantFor is the valid coder grant pairing `self` holds on this machine
// (the newest one when there are several), or the refusal.
func coderGrantFor(dir, self string) (*coderTerms, string) {
if dir == "" || self == "" {
return nil, "no pairing identity for this request"
}
grantsMu.Lock()
f, err := dcnLoadFile(dir)
grantsMu.Unlock()
if err != nil {
return nil, "grant store unreadable"
}
if f.VerifyPubkey == "" || len(f.Coder) == 0 {
return nil, "no standing coder grant for " + self + " on this machine (the person approves one once; `privatae-bastion coders drop` revokes it)"
}
hn, _ := os.Hostname()
var best *coderTerms
bestID := int64(-1)
for k, rec := range f.Coder {
t, err := coderVerifyRec(f.VerifyPubkey, rec)
if err != nil || t.Subject != self || t.Server != hn {
continue
}
id, _ := strconv.ParseInt(k, 10, 64)
if id > bestID {
best, bestID = t, id
}
}
if best == nil {
return nil, "no valid standing coder grant for " + self + " on this machine"
}
return best, ""
}
// handleCoderGrantSync installs one signed coder grant pushed by the host.
// Payload (b64 JSON): {grant_id, terms_canonical, signature, verify_pubkey}.
func handleCoderGrantSync(ws *websocket.Conn, dir string, args []string) {
refuse := func(id int64, why string) {
dcnAudit(dir, "coder_grant_sync", id, "", "refused: "+why)
sendMgmt(ws, 'R', []byte("coder_grant_sync refused: "+why+"\n"))
sendMgmtExit(ws, 126)
}
if len(args) < 1 {
sendMgmt(ws, 'R', []byte("coder_grant_sync: need <payload_b64>\n"))
sendMgmtExit(ws, 1)
return
}
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(args[0]))
if err != nil {
sendMgmt(ws, 'R', []byte("bad base64\n"))
sendMgmtExit(ws, 1)
return
}
var payload struct {
GrantID int64 `json:"grant_id"`
TermsCanonical string `json:"terms_canonical"`
Signature string `json:"signature"`
VerifyPubkey string `json:"verify_pubkey"`
}
if err := json.Unmarshal(raw, &payload); err != nil || payload.GrantID <= 0 {
sendMgmt(ws, 'R', []byte("bad payload json\n"))
sendMgmtExit(ws, 1)
return
}
grantsMu.Lock()
defer grantsMu.Unlock()
f, err := dcnLoadFile(dir)
if err != nil {
sendMgmt(ws, 'R', []byte("grant store unreadable\n"))
sendMgmtExit(ws, 1)
return
}
self := ""
if a := mgmtAccountOf(ws); a != nil {
self = a.self
}
if _, why := pinVerifyKey(f, self, payload.VerifyPubkey); why != "" {
refuse(payload.GrantID, why)
return
}
rec := dcnGrantRec{TermsCanonical: payload.TermsCanonical, Signature: payload.Signature}
terms, err := coderVerifyRec(f.VerifyPubkey, rec)
if err != nil {
refuse(payload.GrantID, "grant does not verify: "+err.Error())
return
}
if hn, _ := os.Hostname(); terms.Server == "" || terms.Server != hn {
refuse(payload.GrantID, fmt.Sprintf("grant is for server '%s', this box is '%s'", terms.Server, hn))
return
}
if self == "" || terms.Subject != self {
refuse(payload.GrantID, fmt.Sprintf("grant is for host '%s', not the host that sent it", terms.Subject))
return
}
if prev, held := f.Coder[fmt.Sprint(payload.GrantID)]; !held || prev.Signature != rec.Signature {
if approvalRequired(dir) {
purpose, perr := purposeForCoderGrant(terms)
if perr != nil {
refuse(payload.GrantID, "grant refused: "+perr.Error())
return
}
if terms.TTLDays <= 0 {
refuse(payload.GrantID, "grant states no duration (ttl_days)")
return
}
if exp, err := time.Parse(time.RFC3339, terms.Expires); err != nil ||
exp.After(time.Now().Add(time.Duration(terms.TTLDays+1)*24*time.Hour)) {
refuse(payload.GrantID, "grant expiry exceeds the approved duration")
return
}
if _, err := boxConsume(dir, terms.Approval, purpose); err != nil {
refuse(payload.GrantID, err.Error())
return
}
}
}
if f.Coder == nil {
f.Coder = map[string]dcnGrantRec{}
}
f.Coder[fmt.Sprint(payload.GrantID)] = rec
if err := dcnSaveFile(dir, f); err != nil {
sendMgmt(ws, 'R', []byte("grant persist failed: "+err.Error()+"\n"))
sendMgmtExit(ws, 1)
return
}
dcnAudit(dir, "coder_grant_sync", payload.GrantID, "", "installed for "+terms.Subject)
b, _ := json.Marshal(map[string]any{"state": "ok", "grant_id": payload.GrantID,
"subject": terms.Subject, "engines": terms.Engines, "max": terms.Max, "expires": terms.Expires})
sendMgmt(ws, 'O', b)
sendMgmtExit(ws, 0)
}
// coderGrantDropLocked removes one coder grant; the caller holds grantsMu.
func coderGrantDropLocked(dir string, id int64, why string) (bool, error) {
f, err := dcnLoadFile(dir)
if err != nil {
return false, err
}
key := strconv.FormatInt(id, 10)
if _, ok := f.Coder[key]; !ok {
return false, nil
}
delete(f.Coder, key)
if err := dcnSaveFile(dir, f); err != nil {
return true, err
}
dcnAudit(dir, "coder_grant_drop", id, "", why)
return true, nil
}
// handleCoderGrantDrop — the host revoked a coder grant: forget it here, now.
// A tighten needs no verification: it can only remove reach.
func handleCoderGrantDrop(ws *websocket.Conn, dir string, args []string) {
if len(args) < 1 {
sendMgmt(ws, 'R', []byte("coder_grant_drop: need <grant_id>\n"))
sendMgmtExit(ws, 1)
return
}
id, err := strconv.ParseInt(strings.TrimSpace(args[0]), 10, 64)
if err != nil {
sendMgmt(ws, 'R', []byte("coder_grant_drop: bad grant_id\n"))
sendMgmtExit(ws, 1)
return
}
grantsMu.Lock()
present, err := coderGrantDropLocked(dir, id, "host-revoked")
grantsMu.Unlock()
if err != nil {
sendMgmt(ws, 'R', []byte("grant drop failed: "+err.Error()+"\n"))
sendMgmtExit(ws, 1)
return
}
b, _ := json.Marshal(map[string]any{"state": "ok", "grant_id": id, "dropped": present})
sendMgmt(ws, 'O', b)
sendMgmtExit(ws, 0)
}
// coderGrantDropAll removes every coder grant this pairing dir holds (the
// owner's `coders drop`). Returns the ids dropped.
func coderGrantDropAll(dir, why string) ([]int64, error) {
grantsMu.Lock()
defer grantsMu.Unlock()
f, err := dcnLoadFile(dir)
if err != nil {
return nil, err
}
var ids []int64
for k := range f.Coder {
if id, err := strconv.ParseInt(k, 10, 64); err == nil {
ids = append(ids, id)
}
}
if len(f.Coder) == 0 {
return nil, nil
}
f.Coder = nil
if err := dcnSaveFile(dir, f); err != nil {
return nil, err
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
for _, id := range ids {
dcnAudit(dir, "coder_grant_drop", id, "", why)
}
return ids, nil
}
// heldCoderGrants is what the enforcing report tells the host this pairing
// holds: {grant_id, signature}, so it can re-sync what is missing or stale.
func heldCoderGrants(dir string) []map[string]any {
out := []map[string]any{}
if dir == "" {
return out
}
f, err := dcnLoadFile(dir)
if err != nil {
return out
}
ids := make([]int64, 0, len(f.Coder))
for k := range f.Coder {
if id, err := strconv.ParseInt(k, 10, 64); err == nil {
ids = append(ids, id)
}
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
for _, id := range ids {
out = append(out, map[string]any{"grant_id": id, "signature": f.Coder[strconv.FormatInt(id, 10)].Signature})
}
return out
}
// maybeReconcileCoderGrants: the heartbeat reply may carry
// {"coder_grants": {"grants": [{grant_id, signature}]}}. Held but unlisted on
// TWO consecutive replies → a revoke this box missed → dropped. ABSENT field →
// no instruction (never "drop all"). The pattern of maybeReconcileReach.
func maybeReconcileCoderGrants(heartbeatResponse []byte, dir string) []int64 {
if dir == "" {
return nil
}
var hb struct {
CoderGrants *struct {
Grants []struct {
GrantID int64 `json:"grant_id"`
} `json:"grants"`
} `json:"coder_grants"`
}
if json.Unmarshal(heartbeatResponse, &hb) != nil || hb.CoderGrants == nil {
return nil
}
expected := map[string]bool{}
for _, g := range hb.CoderGrants.Grants {
if g.GrantID > 0 {
expected[strconv.FormatInt(g.GrantID, 10)] = true
}
}
grantsMu.Lock()
defer grantsMu.Unlock()
f, err := dcnLoadFile(dir)
if err != nil {
return nil
}
var dropped []int64
for key := range f.Coder {
ukey := "coder:" + key
if expected[key] {
clearUnlisted(dir, ukey)
continue
}
if bumpUnlisted(dir, ukey) < dropAfterUnlisted {
continue
}
if id, err := strconv.ParseInt(key, 10, 64); err == nil {
if ok, err := coderGrantDropLocked(dir, id, "reconcile: not granted by host"); err == nil && ok {
dropped = append(dropped, id)
}
}
clearUnlisted(dir, ukey)
}
sort.Slice(dropped, func(i, j int) bool { return dropped[i] < dropped[j] })
return dropped
}