396 lines
16 KiB
Go
396 lines
16 KiB
Go
//go:build !windows
|
|
|
|
package main
|
|
|
|
// Fuzz targets for grants and signed records on an anchored box: which
|
|
// capability family lets a pairing run an op, which family a step-up request
|
|
// is gated on, and what a signed record can be read as.
|
|
//
|
|
// The box here is the worst case for kind confusion: ONE key signs every kind
|
|
// of record (the anchor's pairing records and the entity's grants, plans and
|
|
// op terms), so the only thing keeping a record of one kind from being
|
|
// accepted as another is each verifier's own check of what it is reading.
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// memberFor is the member pairing that holds a grant of exactly one family.
|
|
func memberFor(fam string) string { return "m-" + fam + ".host.privatae.ai" }
|
|
|
|
const memberNoGrant = "m-nogrant.host.privatae.ai"
|
|
|
|
// A pairing's grant names families, and an op runs only if the grant names its
|
|
// family. Whatever the op is called: the anchor pairing may ask for it, a member
|
|
// with no grant may not, and a member granted exactly one family may ask only
|
|
// for an op of that family (or of "-", the lanes that authorize nothing on this
|
|
// box). No op is in a family a grant cannot name except the three floors.
|
|
func FuzzEachOpIsAllowedOnlyByItsOwnFamily(f *testing.F) {
|
|
for _, s := range []string{
|
|
"uptime", "observe:ps", "computer:apps", "session:open", "session:root", "dcn_read", "ctx_put", "backup_fetch",
|
|
"harness:run", "mode:x", "model:pull", "mode", "model", "egress", "inference", "device_request", "service:up",
|
|
"services", "service_grant_sync", "op_run", "op_", "stepup_request", "pairing_grant_sync", "audit_fetch",
|
|
"sessionx", "observe", "Observe:ps", "", "dcn", "op", " uptime", "uptime\x00",
|
|
} {
|
|
f.Add(s)
|
|
}
|
|
priv := setupBox(f)
|
|
future := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339)
|
|
for _, fam := range allCaps {
|
|
host := memberFor(fam)
|
|
writePairingState(f, host, pairingActive)
|
|
canonical, _ := json.Marshal(grantTerms(host, []string{fam}, future))
|
|
g, _ := json.Marshal(pairingGrantFile{TermsCanonical: base64.StdEncoding.EncodeToString(canonical),
|
|
Signature: base64.StdEncoding.EncodeToString(ed25519.Sign(priv, canonical))})
|
|
p, err := pairingGrantPath(host)
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, g, 0o600); err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
if caps, ok := pairingCaps(host); !ok || len(caps) != 1 || caps[0] != fam {
|
|
f.Fatalf("the fixture grant for %s does not verify: %v %v", host, caps, ok)
|
|
}
|
|
}
|
|
writePairingState(f, memberNoGrant, pairingActive)
|
|
floors := map[string]bool{"": true, capsFamilyStepup: true, capsFamilyAnchor: true, capsFamilyNone: true}
|
|
grantable := map[string]bool{}
|
|
for _, c := range allCaps {
|
|
grantable[c] = true
|
|
}
|
|
f.Fuzz(func(t *testing.T, op string) {
|
|
fam := opFamily(op)
|
|
if !grantable[fam] && !floors[fam] {
|
|
t.Fatalf("op %q is in family %q, which no grant can name and is not a floor", op, fam)
|
|
}
|
|
if why := capsAllow(tAnchor, op); why != "" {
|
|
t.Fatalf("the anchor pairing was refused %q: %s", op, why)
|
|
}
|
|
if capsAllow(memberNoGrant, op) == "" {
|
|
t.Fatalf("a member with no grant was allowed %q (family %q)", op, fam)
|
|
}
|
|
if capsAllow("", op) == "" {
|
|
t.Fatalf("an empty pairing identity was allowed %q", op)
|
|
}
|
|
for _, g := range allCaps {
|
|
allowed := capsAllow(memberFor(g), op) == ""
|
|
if want := fam == g || fam == capsFamilyNone; allowed != want {
|
|
t.Fatalf("a member granted only %q: allowed=%v for %q (family %q)", g, allowed, op, fam)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// stepupArity is how many "|" separate the fields of each kind's purpose, so a
|
|
// field that smuggles in a "|" shows up as a purpose with too many fields.
|
|
var stepupArity = map[string][]int{
|
|
"dcn_store_grant": {7}, "dcn_move_plan": {5}, "session_root": {2}, reachKind: {4, 5},
|
|
"critical_action": {5}, "model_pull": {4}, opKindRun: {6}, opKindGrant: {6}, "mgmt_op": {3},
|
|
}
|
|
|
|
// A step-up request asks the person to approve something, and raising it is
|
|
// gated on the family of what it approves. For any details a pod sends: if the
|
|
// box would build a purpose from them, the purpose starts with the request's
|
|
// own kind and has that kind's number of fields (so an approval for one kind
|
|
// can never be read as one for another, or shift a value into another field),
|
|
// and the request is gated on a family a grant can name — never on "-", which
|
|
// any granted pairing passes — except critical_action, which authorizes nothing
|
|
// on this box, and a directive, which only the anchor may ask for.
|
|
func FuzzAStepupIsGatedOnWhatItApproves(f *testing.F) {
|
|
hn, _ := os.Hostname()
|
|
for _, d := range []map[string]any{
|
|
{"kind": "dcn_store_grant", "server": hn, "root": "/srv/x", "mask": []string{"read"}, "excludes": []string{".ssh"}, "ttl_days": 30},
|
|
{"kind": "dcn_move_plan", "store_id": 1, "plan_id": 2, "root": "/srv/x", "archive": "old", "moves": []map[string]string{{"from": "a", "to": "b"}}},
|
|
{"kind": "session_root", "server": hn, "session_id": "s1"},
|
|
{"kind": reachKind, "server": hn, "slot": "gitea", "subject": "h.example", "ttl_days": 7},
|
|
{"kind": reachKind, "server": hn, "slot": "gitea", "subject": "h.example", "mirrors": []map[string]string{{"url": "https://github.com/a/b", "repo": "a/b"}}},
|
|
{"kind": "critical_action", "action": "frontend_deploy", "entity_id": 9001, "action_id": "a1", "target": "t", "summary": "s|x"},
|
|
{"kind": "model_pull", "model": "qwen3:4b", "role": "embed", "max_bytes": 1000},
|
|
{"kind": opKindRun, "server": hn, "op": "write-hello", "sha256": strings.Repeat("a", 64), "params": "{}", "expires": "2030-01-01T00:00:00Z", "changes_sha256": strings.Repeat("b", 64)},
|
|
{"kind": opKindGrant, "server": hn, "op": "write-hello", "sha256": strings.Repeat("a", 64), "bounds": "{}", "expires": "2030-01-01T00:00:00Z", "uses": 3},
|
|
{"kind": "mgmt_op", "op": "service:up", "server": hn, "args": []string{"a|b"}},
|
|
{"kind": "mgmt_op", "op": "self-update", "server": hn},
|
|
{"kind": "mgmt_op", "op": "stepup_request", "server": hn},
|
|
{"kind": "model_pull", "model": "a|b", "role": "embed", "max_bytes": 1},
|
|
{"kind": "session_root", "server": "h|x", "session_id": "s"},
|
|
} {
|
|
raw, _ := json.Marshal(d)
|
|
f.Add(string(raw))
|
|
}
|
|
grantable := map[string]bool{}
|
|
for _, c := range allCaps {
|
|
grantable[c] = true
|
|
}
|
|
f.Fuzz(func(t *testing.T, details string) {
|
|
var d map[string]any
|
|
if json.Unmarshal([]byte(details), &d) != nil || d == nil {
|
|
return
|
|
}
|
|
fam := stepupDetailsFamily(d)
|
|
p, err := purposeFromDetails(d)
|
|
if err != nil {
|
|
return
|
|
}
|
|
kind, _ := d["kind"].(string)
|
|
if !strings.HasPrefix(p, kind+"|") {
|
|
t.Fatalf("details of kind %q built the purpose %q", kind, p)
|
|
}
|
|
arity, n := stepupArity[kind], strings.Count(p, "|")
|
|
fits := false
|
|
for _, a := range arity {
|
|
fits = fits || a == n
|
|
}
|
|
if !fits {
|
|
t.Fatalf("a %s purpose has %d separators, want %v: %q", kind, n, arity, p)
|
|
}
|
|
switch {
|
|
case kind == "critical_action":
|
|
if fam != capsFamilyNone {
|
|
t.Fatalf("critical_action is gated on %q", fam)
|
|
}
|
|
case kind == "mgmt_op" && isDirectiveOp(d["op"].(string)):
|
|
if fam != capsFamilyAnchor {
|
|
t.Fatalf("a directive's approval is gated on %q, not the anchor's", fam)
|
|
}
|
|
case kind == "mgmt_op":
|
|
if fam != opFamily(d["op"].(string)) {
|
|
t.Fatalf("an approval for op %q is gated on %q, not the op's own family %q", d["op"], fam, opFamily(d["op"].(string)))
|
|
}
|
|
default:
|
|
if !grantable[fam] {
|
|
t.Fatalf("a request of kind %q, which the box would build a purpose for, is gated on %q", kind, fam)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// signedKinds is every verifier on this box that reads a record signed by a
|
|
// pinned key, and a well-formed record of each kind. Each verifier is called
|
|
// the way its lane calls it.
|
|
type signedKind struct {
|
|
name string
|
|
record []byte
|
|
verify func(canonical, sig []byte) bool
|
|
}
|
|
|
|
func signedKindsBox(f *testing.F) (ed25519.PrivateKey, []signedKind) {
|
|
priv := setupBox(f)
|
|
pub := base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey))
|
|
dcn := f.TempDir()
|
|
if err := dcnSaveFile(dcn, &dcnGrantFile{Stores: map[string]dcnGrantRec{}, VerifyPubkey: pub}); err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
hn, _ := os.Hostname()
|
|
soon := time.Now().Add(time.Hour).UTC().Format(time.RFC3339)
|
|
enc := base64.StdEncoding.EncodeToString
|
|
rec := func(c, s []byte) dcnGrantRec { return dcnGrantRec{TermsCanonical: enc(c), Signature: enc(s)} }
|
|
js := func(m map[string]any) []byte { b, _ := json.Marshal(m); return b }
|
|
anchorRec := func(kind string) map[string]any {
|
|
return map[string]any{"kind": kind, "server": hn, "anchor": tAnchor, "subject": tMember,
|
|
"caps": []string{"observe"}, "issued_at": soon, "expires": soon}
|
|
}
|
|
opRec := func(kind string) map[string]any {
|
|
m := opTerm(kind, strings.Repeat("a", 64), "{}", "{}", 1, time.Hour, "user")
|
|
if kind == opKindGrant {
|
|
m["uses"] = 3
|
|
}
|
|
return m
|
|
}
|
|
kinds := []signedKind{
|
|
{"pairing_grant", js(anchorRec("pairing_grant")), func(c, s []byte) bool {
|
|
_, err := verifyAnchorRecord(enc(c), enc(s), "pairing_grant")
|
|
return err == nil
|
|
}},
|
|
{"pairing_revoke", js(anchorRec("pairing_revoke")), func(c, s []byte) bool {
|
|
_, err := verifyAnchorRecord(enc(c), enc(s), "pairing_revoke")
|
|
return err == nil
|
|
}},
|
|
{"anchor_consent", js(anchorRec("anchor_consent")), func(c, s []byte) bool {
|
|
_, err := verifyAnchorRecord(enc(c), enc(s), "anchor_consent")
|
|
return err == nil
|
|
}},
|
|
{"management grant", js(map[string]any{"entity": 9001, "server": "*", "ops": []string{"uptime"}, "expires": soon}),
|
|
func(c, s []byte) bool {
|
|
_, err := sessionVerifyGrant(dcn, tAnchor, hn, "uptime", enc(c), enc(s), "")
|
|
return err == nil
|
|
}},
|
|
{"dcn_store_grant", js(map[string]any{"kind": "dcn_store_grant", "class": "machine", "server": hn,
|
|
"root": "/srv/x", "mask": []string{"read"}, "expires": soon}), func(c, s []byte) bool {
|
|
_, err := dcnVerifyRec(pub, rec(c, s))
|
|
return err == nil
|
|
}},
|
|
{"dcn_move_plan", js(map[string]any{"kind": "dcn_move_plan", "store_id": 1, "plan_id": 2, "server": hn,
|
|
"root": "/srv/x", "archive": "old", "moves": []map[string]string{{"from": "a", "to": "b"}}, "expires": soon}),
|
|
func(c, s []byte) bool {
|
|
_, err := dcnVerifyPlan(pub, enc(c), enc(s))
|
|
return err == nil
|
|
}},
|
|
{reachKind, js(map[string]any{"kind": reachKind, "server": hn, "slot": "gitea", "subject": tMember,
|
|
"expires": soon}), func(c, s []byte) bool {
|
|
_, err := reachVerifyRec(pub, rec(c, s))
|
|
return err == nil
|
|
}},
|
|
{opKindRun, js(opRec(opKindRun)), func(c, s []byte) bool {
|
|
_, err := verifyOpTerms(dcn, enc(c), enc(s), opKindRun)
|
|
return err == nil
|
|
}},
|
|
{opKindGrant, js(opRec(opKindGrant)), func(c, s []byte) bool {
|
|
_, err := verifyOpTerms(dcn, enc(c), enc(s), opKindGrant)
|
|
return err == nil
|
|
}},
|
|
}
|
|
return priv, kinds
|
|
}
|
|
|
|
// Whatever bytes the one key has signed, at most one verifier on this box
|
|
// accepts them: a record is read as one kind or refused, never as two. Each
|
|
// well-formed seed is accepted as its own kind and nothing else, so the
|
|
// property is not passing because every verifier refuses everything.
|
|
func FuzzASignedRecordIsReadAsAtMostOneKind(f *testing.F) {
|
|
priv, kinds := signedKindsBox(f)
|
|
for _, k := range kinds {
|
|
f.Add(k.record)
|
|
}
|
|
f.Add([]byte(`{"kind":"dcn_store_grant","KIND":"service_reach_grant"}`))
|
|
f.Add([]byte(`{"kind":null,"server":"*","ops":["uptime"],"expires":"2099-01-01T00:00:00Z"}`))
|
|
f.Fuzz(func(t *testing.T, canonical []byte) {
|
|
sig := ed25519.Sign(priv, canonical)
|
|
var readAs []string
|
|
for _, k := range kinds {
|
|
if k.verify(canonical, sig) {
|
|
readAs = append(readAs, k.name)
|
|
}
|
|
}
|
|
if len(readAs) > 1 {
|
|
t.Fatalf("one signed record is read as %v:\n%s", readAs, canonical)
|
|
}
|
|
for _, k := range kinds {
|
|
if bytes.Equal(canonical, k.record) && (len(readAs) != 1 || readAs[0] != k.name) {
|
|
t.Fatalf("the well-formed %s record is read as %v", k.name, readAs)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Every verifier checks the signature over the exact bytes it then reads. For
|
|
// each kind, a record and a signature that verify: change any byte of either
|
|
// and the verifier refuses.
|
|
func FuzzAChangedSignedByteNeverVerifies(f *testing.F) {
|
|
priv, kinds := signedKindsBox(f)
|
|
sigs := make([][]byte, len(kinds))
|
|
for i, k := range kinds {
|
|
sigs[i] = ed25519.Sign(priv, k.record)
|
|
f.Add(uint8(i), k.record, sigs[i])
|
|
f.Add(uint8(i), append(append([]byte(nil), k.record...), ' '), sigs[i])
|
|
mut := append([]byte(nil), sigs[i]...)
|
|
mut[0] ^= 1
|
|
f.Add(uint8(i), k.record, mut)
|
|
}
|
|
f.Fuzz(func(t *testing.T, which uint8, canonical, sig []byte) {
|
|
i := int(which) % len(kinds)
|
|
k := kinds[i]
|
|
ok := k.verify(canonical, sig)
|
|
same := bytes.Equal(canonical, k.record) && bytes.Equal(sig, sigs[i])
|
|
if ok && !same {
|
|
t.Fatalf("the %s verifier accepted bytes that were not signed:\n%q\n%x", k.name, canonical, sig)
|
|
}
|
|
if same && !ok {
|
|
t.Fatalf("the %s verifier refused the record and signature it was given", k.name)
|
|
}
|
|
})
|
|
}
|
|
|
|
// A device approval is signed over its fields joined by newlines. Whatever a
|
|
// pod does to an approval the device signed, the box accepts it only as the
|
|
// approval the device signed: never for another purpose, never with another
|
|
// nonce (a fresh nonce would make a spent approval new again).
|
|
func FuzzADeviceApprovalIsReadOneWayOnly(f *testing.F) {
|
|
const n = "0123456789abcdef0123456789abcdef"
|
|
f.Add("mgmt_op|uptime|h|d", n, "mgmt_op|uptime|h|d", n)
|
|
f.Add("mgmt_op|uptime|h|d\nx", n, "mgmt_op|uptime|h|d", "x\n"+n)
|
|
f.Add("a", n+"\nb", "a\n"+n, "b")
|
|
f.Add("a", n, "a", n+"0")
|
|
setupBox(f)
|
|
pub, priv, _ := ed25519.GenerateKey(nil)
|
|
key := base64.StdEncoding.EncodeToString(pub)
|
|
if err := pinDeviceKey(key); err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
expires := time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339)
|
|
f.Fuzz(func(t *testing.T, signedPurpose, signedNonce, purpose, nonce string) {
|
|
a := deviceApproval{V: deviceApprovalVersion, Purpose: signedPurpose, Nonce: signedNonce,
|
|
Expires: expires, Uses: 1, Key: key}
|
|
sig := base64.StdEncoding.EncodeToString(ed25519.Sign(priv, a.canonical()))
|
|
b := a
|
|
b.Purpose, b.Nonce, b.Sig = purpose, nonce, sig
|
|
raw, _ := json.Marshal(&b)
|
|
_, err := verifyDeviceApproval(string(raw), purpose, time.Now())
|
|
plain := !strings.ContainsAny(signedPurpose+signedNonce, "\r\n") && len(signedNonce) >= 16 && signedPurpose != ""
|
|
if err != nil {
|
|
if plain && purpose == signedPurpose && nonce == signedNonce {
|
|
t.Fatalf("the approval the device signed for (%q, %q) was refused: %v", purpose, nonce, err)
|
|
}
|
|
return
|
|
}
|
|
if purpose != signedPurpose || nonce != signedNonce {
|
|
t.Fatalf("an approval signed for (%q, %q) was accepted as (%q, %q)", signedPurpose, signedNonce, purpose, nonce)
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzAGrantsRunBoundsHoldWhateverTheHostSends: whatever bounds text a host
|
|
// sends, a run bound the machine accepts is one it can hold — a window has a
|
|
// length, a zone and a day, a rate names a limit inside the ceilings — and
|
|
// judging a run against it at any moment answers, never panics, and never
|
|
// lets a run inside an interval or past a day's count.
|
|
func FuzzAGrantsRunBoundsHoldWhateverTheHostSends(f *testing.F) {
|
|
m := &opManifest{Params: map[string]opParam{"count": {Type: "integer", Min: ptr64(1), Max: ptr64(5)}}}
|
|
for _, s := range []string{
|
|
`{"_window":{"days":["mon"],"from":"22:00","to":"06:00","tz":"Europe/Berlin"}}`,
|
|
`{"_rate":{"per_day":2,"min_interval_s":600},"count":{"max":2}}`,
|
|
`{"_window":null,"_rate":null}`, `{"_x":1}`, `{"_window":{"from":"02:00","to":"02:00"}}`,
|
|
} {
|
|
f.Add(s, int64(1761739200), int64(300))
|
|
}
|
|
f.Fuzz(func(t *testing.T, raw string, at, gap int64) {
|
|
b, err := parseGrantBounds(m, raw)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if w := b.Window; w != nil {
|
|
if w.from == w.to || w.loc == nil || w.day == [7]bool{} {
|
|
t.Fatalf("accepted a window that is no window: %s", raw)
|
|
}
|
|
}
|
|
if r := b.Rate; r != nil {
|
|
if (r.PerDay == nil && r.MinInterval == nil) || (r.PerDay != nil && (*r.PerDay < 1 || *r.PerDay > opGrantUsesMax)) ||
|
|
(r.MinInterval != nil && (*r.MinInterval < 1 || *r.MinInterval > opRateMinInterval)) {
|
|
t.Fatalf("accepted a rate outside the ceilings: %s", raw)
|
|
}
|
|
}
|
|
now := time.Unix(at%(1<<40), 0)
|
|
last := now.Add(-time.Duration(gap%(1<<30)) * time.Second)
|
|
if last.After(now) {
|
|
last = now
|
|
}
|
|
why, _, _ := opNotNowWhy(b, []time.Time{last}, now)
|
|
if b.Window == nil && b.Rate != nil && b.Rate.MinInterval != nil &&
|
|
now.Sub(last) < time.Duration(*b.Rate.MinInterval)*time.Second && why == "" {
|
|
t.Fatalf("a run inside the interval was let through: %s", raw)
|
|
}
|
|
if b.Window == nil && b.Rate != nil && b.Rate.PerDay != nil && *b.Rate.PerDay == 1 && now.Sub(last) < 24*time.Hour && why == "" {
|
|
t.Fatalf("a second run in a day was let through: %s", raw)
|
|
}
|
|
})
|
|
}
|
|
|
|
func ptr64(v int64) *int64 { return &v }
|