//go:build linux package provisioner import ( "bytes" "context" "encoding/base64" "strings" "amadan.net/oficina/home/internal/operator" family "amadan.net/rastrillo/rastrillo/crypto" "github.com/go-json-experiment/json" ) // InstallationPlan is local preparation. Persisting it does not enroll a Home // role, assert fresh owner approval, or permit a remote command. type InstallationPlan struct { Version int `json:"version"` Installation string `json:"installation"` BaselineOperation string `json:"baseline_operation"` Credential InstallationTarget `json:"credential"` Directory InstallationTarget `json:"directory"` } type InstallationTarget struct { Console string `json:"console"` Account string `json:"account"` App string `json:"app"` Host string `json:"host"` Origin string `json:"origin"` Release string `json:"release"` Artifact string `json:"artifact_digest"` Environment string `json:"environment_digest"` DataKey string `json:"data_key_fingerprint"` } type acceptedHead struct { Sequence int64 Head string } type installationState struct { Plan InstallationPlan Generation string Heads map[string]acceptedHead Pending map[string]string Operations map[string]*preparedOperation } type preparedOperation struct { Request operator.Request Raw []byte Status string Result []byte } type installationBook struct { heads map[string]string installations map[string]*installationState events map[string][]byte records map[string]journalRecord results map[string][]byte } type localTransition struct { Operation string `json:"operation"` Kind string `json:"kind"` ResultRecord string `json:"result_record"` ResultHead string `json:"result_head"` } func validIdentifier(s string) bool { if len(s) == 0 || len(s) > 128 { return false } for _, c := range s { if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsRune("_-/.", c)) { return false } } return !strings.Contains(s, "..") } func validHex(s string, n int) bool { if len(s) != n { return false } for _, c := range s { if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') { return false } } return true } func (p InstallationPlan) valid() bool { if p.Version != 1 || !operator.CanonicalToken(p.Installation) || !operator.CanonicalToken(p.BaselineOperation) { return false } for _, t := range []InstallationTarget{p.Credential, p.Directory} { if !operator.Origin(t.Console) || !operator.Origin(t.Origin) || !operator.Origin("https://"+t.Host) || !validIdentifier(t.Account) || !validIdentifier(t.App) || !validHex(t.Release, 40) || !validHex(t.Artifact, 64) || !validHex(t.Environment, 64) || !validHex(t.DataKey, 64) { return false } } return p.Credential.Origin != p.Directory.Origin && !(p.Credential.Console == p.Directory.Console && p.Credential.Host == p.Directory.Host) } func sameTarget(a, b InstallationTarget) bool { return a.Console == b.Console && a.Host == b.Host } func (s *installationState) target(role string) (InstallationTarget, bool) { switch role { case "credential": return s.Plan.Credential, true case "directory": return s.Plan.Directory, true } return InstallationTarget{}, false } func (j *journal) request(s *installationState, raw []byte, action string) (operator.Request, error) { role := "directory" if strings.HasPrefix(action, "credential.") { role = "credential" } target, ok := s.target(role) if !ok { return operator.Request{}, ErrRefused } v, e := operator.NewVerifier(operator.Pins{Role: role, Origin: target.Origin, Installation: s.Plan.Installation, Registry: j.floor.identity.Registry, Device: j.floor.identity.Device, PublicKey: base64.RawURLEncoding.EncodeToString(j.key.SignPub()), Actions: []string{action}}) if e != nil { return operator.Request{}, ErrRefused } r, e := v.Verify(raw) if e != nil { return operator.Request{}, ErrRefused } return r, nil } func (j *journal) bookLocked() (*installationBook, error) { if len(j.locks) != 2 { return nil, ErrRefused } if e := j.recover(); e != nil { return nil, e } b := &installationBook{heads: map[string]string{}, installations: map[string]*installationState{}, events: map[string][]byte{}, records: map[string]journalRecord{}, results: map[string][]byte{}} rows, e := j.db.Query("SELECT CASE WHEN length(signed_record)<=8192 THEN signed_record ELSE NULL END, CASE WHEN length(sealed_payload)<=98304 THEN sealed_payload ELSE NULL END FROM records ORDER BY sequence") if e != nil { return nil, ErrUnavailable } defer rows.Close() for rows.Next() { var raw, sealed []byte if rows.Scan(&raw, &sealed) != nil { return nil, ErrUnavailable } r, e := j.validate(raw, sealed) if e != nil { return nil, e } plain, e := family.Open(j.key, payloadContext+"/"+r.Operation, sealed) if e != nil || len(plain) == 0 || len(plain) > 64<<10 { return nil, ErrRefused } if e = j.replay(b, r, plain); e != nil { return nil, e } b.events[r.Operation] = plain b.records[r.Operation] = r b.heads[r.Operation] = hash(raw) } if rows.Err() != nil { return nil, ErrUnavailable } return b, nil } func (j *journal) replay(b *installationBook, r journalRecord, plain []byte) error { s := b.installations[r.Installation] if r.Action == "provisioner.enroll" { var p InstallationPlan if s != nil || r.Expected != "absent" || closedJSON(plain, &p) != nil || !p.valid() || p.Installation != r.Installation || p.BaselineOperation != r.Operation { return ErrRefused } for _, old := range b.installations { for _, a := range []InstallationTarget{old.Plan.Credential, old.Plan.Directory} { if sameTarget(a, p.Credential) || sameTarget(a, p.Directory) { return ErrRefused } } } b.installations[p.Installation] = &installationState{p, "", map[string]acceptedHead{}, map[string]string{}, map[string]*preparedOperation{}} return nil } if s == nil { return ErrRefused } if r.Expected == "prepared" && !strings.HasPrefix(r.Action, "provisioner.") { req, e := j.request(s, plain, r.Action) if e != nil { return e } a := req.Authorization() if a.Role == "directory" && ((s.Generation == "" && a.Action != "directory.initialize") || (s.Generation != "" && a.Action == "directory.initialize")) { return ErrRefused } head := s.Heads[a.Role] if a.OperationID != r.Operation || a.JournalSequence != r.Sequence || a.Sequence != head.Sequence+1 || a.Previous != head.Head || s.Pending[a.Role] != "" || s.Operations[a.OperationID] != nil || r.Created < a.Issued || r.Created >= a.Expires { return ErrRefused } s.Operations[a.OperationID] = &preparedOperation{req, append([]byte(nil), plain...), "prepared", nil} s.Pending[a.Role] = a.OperationID return nil } if r.Action == "provisioner.result" { if r.Expected != "in-flight" { return ErrRefused } var out operator.Result if decodeOperatorResult(plain, &out) != nil || out.Version != 1 || out.Outcome != "committed" { return ErrRefused } pending := s.Operations[out.OperationID] if pending == nil || (pending.Status != "in-flight" && pending.Status != "ambiguous") { return ErrRefused } b.results[r.Operation] = append([]byte(nil), plain...) return nil } var tr localTransition if closedJSON(plain, &tr) != nil || !operator.CanonicalToken(tr.Operation) { return ErrRefused } op := s.Operations[tr.Operation] if op == nil { return ErrRefused } a := op.Request.Authorization() if r.Action == "provisioner.in-flight" { if r.Expected != "prepared" || tr.Kind != "in-flight" || (tr.ResultRecord != "" || tr.ResultHead != "") || op.Status != "prepared" || r.Created >= a.Expires || r.Created < a.Issued { return ErrRefused } op.Status = "in-flight" return nil } if r.Action != "provisioner.outcome" || r.Expected != "in-flight" || (op.Status != "in-flight" && op.Status != "ambiguous") { return ErrRefused } switch tr.Kind { case "ambiguous": if tr.ResultRecord != "" || tr.ResultHead != "" { return ErrRefused } op.Status = "ambiguous" case "refused": if (tr.ResultRecord != "" || tr.ResultHead != "") || r.Created < a.Expires || op.Status == "ambiguous" { return ErrRefused } op.Status = "refused" delete(s.Pending, a.Role) case "accepted": result, ok := b.results[tr.ResultRecord] rr := b.records[tr.ResultRecord] if !ok || rr.Installation != r.Installation || rr.Action != "provisioner.result" || !operator.CanonicalToken(tr.ResultHead) || b.heads[tr.ResultRecord] != tr.ResultHead { return ErrRefused } var out operator.Result if decodeOperatorResult(result, &out) != nil || out.Version != 1 || out.OperationID != a.OperationID || out.Outcome != "committed" { return ErrRefused } if a.Action == "directory.initialize" { var input operator.InitializeInput if operator.DecodeInput(op.Request, &input) != nil || s.Generation != "" || !operator.CanonicalToken(input.Generation) || out.Generation != input.Generation { return ErrRefused } s.Generation = out.Generation } if a.Action != "directory.initialize" && out.Generation != "" && out.Generation != s.Generation { return ErrRefused } s.Heads[a.Role] = acceptedHead{a.Sequence, op.Request.Head()} delete(s.Pending, a.Role) op.Status = "accepted" op.Result = append([]byte(nil), result...) default: return ErrRefused } return nil } func (j *journal) enroll(ctx context.Context, p InstallationPlan) error { if !p.valid() { return ErrRefused } raw, e := canonicalJSON(p) if e != nil { return e } j.mu.Lock() defer j.mu.Unlock() b, e := j.bookLocked() if e != nil { return e } if old, ok := b.events[p.BaselineOperation]; ok { if b.records[p.BaselineOperation].Action != "provisioner.enroll" || !bytes.Equal(old, raw) { return ErrRefused } return nil } next, _, e := j.floor.current() if e != nil { return e } r := journalRecord{Installation: p.Installation, Operation: p.BaselineOperation, Action: "provisioner.enroll", Expected: "absent", Sequence: next.Sequence + 1, Created: j.now().Unix()} if e = j.replay(b, r, raw); e != nil { return e } _, e = j.appendAtLocked(ctx, p.Installation, p.BaselineOperation, r.Action, r.Expected, raw, r.Created) return e } func (j *journal) prepare(ctx context.Context, id, op, action string, input any) ([]byte, error) { j.mu.Lock() defer j.mu.Unlock() b, e := j.bookLocked() if e != nil { return nil, e } s := b.installations[id] if s == nil || !operator.CanonicalToken(op) { return nil, ErrRefused } if old := s.Operations[op]; old != nil { a := old.Request.Authorization() if a.Action != action { return nil, ErrRefused } raw, e := operator.SignRequest(j.key, a, input) if e != nil || !bytes.Equal(raw, old.Raw) { // ECDSA signing may randomize the envelope. Compare verified exact input. if e != nil { return nil, ErrRefused } req, e := j.request(s, raw, action) if e != nil || !bytes.Equal(req.Input(), old.Request.Input()) { return nil, ErrRefused } } return append([]byte(nil), old.Raw...), nil } role := "directory" if strings.HasPrefix(action, "credential.") { role = "credential" } target, ok := s.target(role) if !ok || s.Pending[role] != "" { return nil, ErrRefused } point, _, e := j.floor.current() if e != nil { return nil, e } head := s.Heads[role] issued := j.now().Unix() a := operator.Authorization{Version: 1, Purpose: "operator-input", Role: role, Origin: target.Origin, Installation: id, Registry: j.floor.identity.Registry, Device: j.floor.identity.Device, Sequence: head.Sequence + 1, Initial: head.Sequence == 0, Previous: head.Head, JournalSequence: point.Sequence + 1, OperationID: op, Action: action, Issued: issued, Expires: issued + 300} raw, e := operator.SignRequest(j.key, a, input) if e != nil { return nil, ErrRefused } r := journalRecord{Installation: id, Operation: op, Action: action, Expected: "prepared", Sequence: point.Sequence + 1, Created: issued} if e = j.replay(b, r, raw); e != nil { return nil, e } if _, e = j.appendAtLocked(ctx, id, op, action, "prepared", raw, r.Created); e != nil { return nil, e } return raw, nil } func (j *journal) markInFlight(ctx context.Context, id, event, op string) error { tr := localTransition{Operation: op, Kind: "in-flight"} return j.transition(ctx, id, event, "provisioner.in-flight", "prepared", tr) } // transition is internal journal machinery. Production outcome calls must be // supplied by the future authenticated console adapter; no CLI outcome path // or worker-authentication substitute is implemented here. func (j *journal) transition(ctx context.Context, id, event, action, expected string, tr localTransition) error { raw, e := canonicalJSON(tr) if e != nil { return e } j.mu.Lock() defer j.mu.Unlock() b, e := j.bookLocked() if e != nil { return e } if old, ok := b.events[event]; ok { r := b.records[event] if r.Installation != id || r.Action != action || r.Expected != expected || !bytes.Equal(old, raw) { return ErrRefused } return nil } r := journalRecord{Installation: id, Operation: event, Action: action, Expected: expected, Created: j.now().Unix()} if e = j.replay(b, r, raw); e != nil { return e } _, e = j.appendAtLocked(ctx, id, event, action, expected, raw, r.Created) return e } type EnrollmentInfo struct { RegistryInfo Installation string `json:"installation"` State string `json:"state"` } // EnrollPlan records private local preparation only. Remote enrollment and owner // approval remain separate steps with no dispatcher in this package. func EnrollPlan(statePath, floorPath string, password, raw []byte) (EnrollmentInfo, error) { var p InstallationPlan if len(raw) > 64<<10 || json.Unmarshal(raw, &p, json.RejectUnknownMembers(true)) != nil || !p.valid() { return EnrollmentInfo{}, ErrRefused } j, e := existingJournal(statePath, floorPath, password) if e != nil { return EnrollmentInfo{}, e } defer closeExisting(j) if e = j.enroll(context.Background(), p); e != nil { return EnrollmentInfo{}, e } point, _, e := j.floor.current() if e != nil { return EnrollmentInfo{}, e } return EnrollmentInfo{RegistryInfo{j.floor.identity.Registry, j.floor.identity.Device, j.floor.identity.Public, point.Sequence}, p.Installation, "local-plan"}, nil } func decodeOperatorResult(raw []byte, out *operator.Result) error { if len(raw) == 0 || len(raw) > operator.BodyLimit || json.Unmarshal(raw, out, json.RejectUnknownMembers(true)) != nil { return ErrRefused } encoded, e := json.Marshal(out, json.Deterministic(true)) if e != nil || !bytes.Equal(encoded, raw) { return ErrRefused } return nil }