Add atomic operator execution and sealed idempotent result storage
4 files changed,
+453
−0
internal/operator/engine.go+200 −0internal/operator/engine_test.go+215 −0internal/operator/migrations/credential/migrations/0001_operator.sql+19 −0internal/operator/migrations/directory/migrations/0001_operator.sql+19 −0
CI failed — run details
diff --git a/internal/operator/engine.go b/internal/operator/engine.go| new file mode 100644 |
| index 0000000..a88296f |
| --- /dev/null |
| +++ b/internal/operator/engine.go |
| @@ -0,0 +1,200 @@ |
| +package operator |
| + |
| +import ( |
| + "context" |
| + "crypto/hmac" |
| + "crypto/sha256" |
| + "database/sql" |
| + "database/sql/driver" |
| + "embed" |
| + "errors" |
| + "io/fs" |
| + "math" |
| + "time" |
| + |
| + "github.com/carlosframework/rastrillo/migrate" |
| +) |
| + |
| +//go:embed migrations |
| +var migrations embed.FS |
| + |
| +var CredentialSchema = roleSchema("credential") |
| +var DirectorySchema = roleSchema("directory") |
| + |
| +func roleSchema(role string) *migrate.Set { |
| + f, e := fs.Sub(migrations, "migrations/"+role) |
| + if e != nil { |
| + panic(e) |
| + } |
| + return migrate.MustFromFS(f, "oficina_"+role+"_operator") |
| +} |
| + |
| +type Sealer interface { |
| + Seal(string, []byte) ([]byte, error) |
| + Open(string, []byte) ([]byte, error) |
| +} |
| + |
| +// Mutation executes on the engine's already locked connection. It must not |
| +// commit, open another writer, dial the network or emit its result itself. |
| +type Mutation func(context.Context, *sql.Conn, Request) ([]byte, error) |
| + |
| +type Engine struct { |
| + db *sql.DB |
| + verifier *Verifier |
| + sealer Sealer |
| + macKey []byte |
| + prefix string |
| + now func() time.Time |
| +} |
| + |
| +func NewEngine(db *sql.DB, v *Verifier, sealer Sealer, macKey []byte, now func() time.Time) (*Engine, error) { |
| + if db == nil || v == nil || sealer == nil || len(macKey) != 32 || now == nil { |
| + return nil, ErrRefused |
| + } |
| + return &Engine{db, v, sealer, append([]byte(nil), macKey...), v.pins.Role + "_operator", now}, nil |
| +} |
| +func (e *Engine) purpose(a Authorization) string { |
| + return "operator.result/v1/" + a.Origin + "/" + a.OperationID + "/" + a.Action |
| +} |
| +func (e *Engine) mac(r Request) []byte { |
| + m := hmac.New(sha256.New, e.macKey) |
| + m.Write(r.input) |
| + return m.Sum(nil) |
| +} |
| + |
| +// Execute validates against this engine's pinned verifier, then commits the |
| +// mutation, result and accepted stream together. Expired authentic input is |
| +// useful only when its exact previously committed result already exists. |
| +func (e *Engine) Execute(ctx context.Context, raw []byte, mutate Mutation) ([]byte, error) { |
| + if e == nil || mutate == nil { |
| + return nil, ErrRefused |
| + } |
| + req, err := e.verifier.Verify(raw) |
| + if err != nil { |
| + return nil, ErrRefused |
| + } |
| + a := req.authorization |
| + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) |
| + defer cancel() |
| + conn, err := e.db.Conn(ctx) |
| + if err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + defer conn.Close() |
| + for _, statement := range []string{"PRAGMA foreign_keys=ON", "PRAGMA secure_delete=ON", "PRAGMA busy_timeout=1000"} { |
| + if _, err = conn.ExecContext(ctx, statement); err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + } |
| + var fk, secure int |
| + var journal string |
| + if conn.QueryRowContext(ctx, "PRAGMA foreign_keys").Scan(&fk) != nil || fk != 1 || conn.QueryRowContext(ctx, "PRAGMA secure_delete").Scan(&secure) != nil || secure != 1 || conn.QueryRowContext(ctx, "PRAGMA journal_mode").Scan(&journal) != nil || journal != "wal" { |
| + return nil, ErrUnavailable |
| + } |
| + violations, err := conn.QueryContext(ctx, "PRAGMA foreign_key_check") |
| + if err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + bad := violations.Next() |
| + readErr := violations.Err() |
| + closeErr := violations.Close() |
| + if bad || readErr != nil || closeErr != nil { |
| + return nil, ErrUnavailable |
| + } |
| + if _, err = conn.ExecContext(ctx, "BEGIN IMMEDIATE"); err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + finished := false |
| + defer func() { |
| + if !finished { |
| + cleanup, cancel := context.WithTimeout(context.Background(), time.Second) |
| + defer cancel() |
| + if _, err := conn.ExecContext(cleanup, "ROLLBACK"); err != nil { |
| + _ = conn.Raw(func(any) error { return driver.ErrBadConn }) |
| + } |
| + } |
| + }() |
| + var storedOrigin, storedInstallation, storedRegistry, storedDevice, storedHead string |
| + var storedSequence, storedCompleted int64 |
| + stateErr := conn.QueryRowContext(ctx, "SELECT origin,installation,registry,device,sequence,head,completed_at FROM "+e.prefix+"_state WHERE singleton=1").Scan(&storedOrigin, &storedInstallation, &storedRegistry, &storedDevice, &storedSequence, &storedHead, &storedCompleted) |
| + if stateErr != nil && !errors.Is(stateErr, sql.ErrNoRows) { |
| + return nil, ErrUnavailable |
| + } |
| + if stateErr == nil && (storedOrigin != a.Origin || storedInstallation != a.Installation || storedRegistry != a.Registry || storedDevice != a.Device || storedSequence <= 0 || !CanonicalToken(storedHead) || storedCompleted <= 0 || e.now().Unix() < storedCompleted) { |
| + return nil, ErrRefused |
| + } |
| + var oldAction, oldHead string |
| + var oldSequence int64 |
| + var oldMAC, sealed []byte |
| + err = conn.QueryRowContext(ctx, "SELECT action,sequence,authorization_head,request_mac,sealed_result FROM "+e.prefix+"_results WHERE operation_id=?", a.OperationID).Scan(&oldAction, &oldSequence, &oldHead, &oldMAC, &sealed) |
| + if err == nil { |
| + if stateErr != nil || storedSequence < a.Sequence || oldAction != a.Action || oldSequence != a.Sequence || oldHead != req.head || !hmac.Equal(oldMAC, e.mac(req)) { |
| + return nil, ErrRefused |
| + } |
| + result, openErr := e.sealer.Open(e.purpose(a), sealed) |
| + if openErr != nil || len(result) == 0 || len(result) > InputLimit { |
| + return nil, ErrUnavailable |
| + } |
| + if _, err = conn.ExecContext(ctx, "COMMIT"); err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + finished = true |
| + return result, nil |
| + } |
| + if !errors.Is(err, sql.ErrNoRows) { |
| + return nil, ErrUnavailable |
| + } |
| + now := e.now() |
| + if !req.Fresh(now) { |
| + return nil, ErrRefused |
| + } |
| + var origin, installation, registry, device, head string |
| + var sequence, last int64 |
| + err = conn.QueryRowContext(ctx, "SELECT origin,installation,registry,device,sequence,head,completed_at FROM "+e.prefix+"_state WHERE singleton=1").Scan(&origin, &installation, ®istry, &device, &sequence, &head, &last) |
| + if errors.Is(err, sql.ErrNoRows) { |
| + if !a.Initial || a.Sequence != 1 || a.Previous != "" { |
| + return nil, ErrRefused |
| + } |
| + } else if err != nil { |
| + return nil, ErrUnavailable |
| + } else { |
| + if a.Initial || sequence == math.MaxInt64 || a.Sequence != sequence+1 || a.Previous != head || a.Origin != origin || a.Installation != installation || a.Registry != registry || a.Device != device || now.Unix() < last { |
| + return nil, ErrRefused |
| + } |
| + } |
| + var count int |
| + if conn.QueryRowContext(ctx, "SELECT count(*) FROM "+e.prefix+"_results").Scan(&count) != nil { |
| + return nil, ErrUnavailable |
| + } |
| + if count >= 10000 { |
| + return nil, ErrRefused |
| + } |
| + result, err := mutate(ctx, conn, req) |
| + if err != nil { |
| + if errors.Is(err, ErrRefused) { |
| + return nil, ErrRefused |
| + } |
| + return nil, ErrUnavailable |
| + } |
| + completed := e.now() |
| + if !req.Fresh(completed) || completed.Before(now) || len(result) == 0 || len(result) > InputLimit { |
| + return nil, ErrRefused |
| + } |
| + sealed, err = e.sealer.Seal(e.purpose(a), result) |
| + if err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + _, err = conn.ExecContext(ctx, "INSERT INTO "+e.prefix+"_results(operation_id,action,sequence,authorization_head,request_mac,sealed_result,completed_at) VALUES(?,?,?,?,?,?,?)", a.OperationID, a.Action, a.Sequence, req.head, e.mac(req), sealed, completed.Unix()) |
| + if err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + _, err = conn.ExecContext(ctx, "INSERT INTO "+e.prefix+"_state(singleton,origin,installation,registry,device,sequence,head,completed_at) VALUES(1,?,?,?,?,?,?,?) ON CONFLICT(singleton) DO UPDATE SET sequence=excluded.sequence,head=excluded.head,completed_at=excluded.completed_at", a.Origin, a.Installation, a.Registry, a.Device, a.Sequence, req.head, completed.Unix()) |
| + if err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + if _, err = conn.ExecContext(ctx, "COMMIT"); err != nil { |
| + return nil, ErrUnavailable |
| + } |
| + finished = true |
| + return append([]byte(nil), result...), nil |
| +} |
diff --git a/internal/operator/engine_test.go b/internal/operator/engine_test.go| new file mode 100644 |
| index 0000000..a59a899 |
| --- /dev/null |
| +++ b/internal/operator/engine_test.go |
| @@ -0,0 +1,215 @@ |
| +package operator |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "database/sql" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "sync" |
| + "sync/atomic" |
| + "testing" |
| + "time" |
| + |
| + sharedseal "amadan.net/oficina/home/handoff/seal" |
| + rdb "github.com/carlosframework/rastrillo/db" |
| + "github.com/carlosframework/rastrillo/migrate" |
| + _ "modernc.org/sqlite" |
| +) |
| + |
| +type engineFixture struct { |
| + engine *Engine |
| + db *sql.DB |
| + raw []byte |
| + now *time.Time |
| + path string |
| +} |
| + |
| +func newEngineFixture(t *testing.T) engineFixture { |
| + t.Helper() |
| + k, v, a := fixture(t) |
| + path := filepath.Join(t.TempDir(), "operator.db") |
| + handle, e := rdb.Open(path, nil) |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + t.Cleanup(func() { handle.Close() }) |
| + db := handle.Writer() |
| + db.SetMaxOpenConns(4) |
| + if _, e = db.Exec("PRAGMA journal_mode=WAL"); e != nil { |
| + t.Fatal(e) |
| + } |
| + if _, e = migrate.Apply(context.Background(), handle, DirectorySchema); e != nil { |
| + t.Fatal(e) |
| + } |
| + if _, e = db.Exec("CREATE TABLE test_items(id INTEGER PRIMARY KEY)"); e != nil { |
| + t.Fatal(e) |
| + } |
| + sealer, e := sharedseal.New(bytes.Repeat([]byte{6}, 32), "oficina/operator") |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + now := time.Unix(a.Issued, 0) |
| + engine, e := NewEngine(db, v, sealer, bytes.Repeat([]byte{7}, 32), func() time.Time { return now }) |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + return engineFixture{engine, db, signed(t, k, a), &now, path} |
| +} |
| + |
| +var privateResult = []byte(`{"bearer":"synthetic-private-bearer-keep-out-of-db","receipt":"synthetic-private-proof"}`) |
| + |
| +func insertOne(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) { |
| + _, e := c.ExecContext(ctx, "INSERT INTO test_items VALUES(1)") |
| + if e != nil { |
| + return nil, e |
| + } |
| + return privateResult, nil |
| +} |
| +func countRows(t *testing.T, db *sql.DB, table string) int { |
| + t.Helper() |
| + var n int |
| + if e := db.QueryRow("SELECT count(*) FROM " + table).Scan(&n); e != nil { |
| + t.Fatal(e) |
| + } |
| + return n |
| +} |
| +func TestEngineCommitsOnceAndRecoversExpiredResultPrivately(t *testing.T) { |
| + f := newEngineFixture(t) |
| + out, e := f.engine.Execute(context.Background(), f.raw, insertOne) |
| + if e != nil || !bytes.Equal(out, privateResult) { |
| + t.Fatal(e) |
| + } |
| + *f.now = f.now.Add(301 * time.Second) |
| + out, e = f.engine.Execute(context.Background(), f.raw, func(context.Context, *sql.Conn, Request) ([]byte, error) { |
| + t.Fatal("replay executed mutation") |
| + return nil, nil |
| + }) |
| + if e != nil || !bytes.Equal(out, privateResult) { |
| + t.Fatal(e) |
| + } |
| + if countRows(t, f.db, "test_items") != 1 || countRows(t, f.db, "directory_operator_results") != 1 { |
| + t.Fatal("non-atomic replay") |
| + } |
| + for _, suffix := range []string{"", "-wal"} { |
| + b, e := os.ReadFile(f.path + suffix) |
| + if e != nil && !errors.Is(e, os.ErrNotExist) { |
| + t.Fatal(e) |
| + } |
| + for _, secret := range [][]byte{privateResult, []byte("synthetic-private-bearer"), []byte("synthetic-private-proof")} { |
| + if bytes.Contains(b, secret) { |
| + t.Fatal("private result at rest") |
| + } |
| + } |
| + } |
| + if _, e = f.db.Exec("DELETE FROM directory_operator_state"); e != nil { |
| + t.Fatal(e) |
| + } |
| + if _, e = f.engine.Execute(context.Background(), f.raw, insertOne); e != ErrRefused { |
| + t.Fatal("missing authority accepted", e) |
| + } |
| +} |
| +func TestEngineFailureRollsBackMutationReceiptAndSequence(t *testing.T) { |
| + for _, mode := range []string{"mutation", "expiry", "seal"} { |
| + t.Run(mode, func(t *testing.T) { |
| + f := newEngineFixture(t) |
| + if mode == "seal" { |
| + f.engine.sealer = failingSeal{} |
| + } |
| + _, e := f.engine.Execute(context.Background(), f.raw, func(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) { |
| + if _, e := insertOne(ctx, c, r); e != nil { |
| + return nil, e |
| + } |
| + switch mode { |
| + case "mutation": |
| + return nil, errors.New("private@example.test") |
| + case "expiry": |
| + *f.now = f.now.Add(301 * time.Second) |
| + } |
| + return privateResult, nil |
| + }) |
| + if e != ErrRefused && e != ErrUnavailable { |
| + t.Fatal("unexpected error", e) |
| + } |
| + for _, table := range []string{"test_items", "directory_operator_results", "directory_operator_state"} { |
| + if countRows(t, f.db, table) != 0 { |
| + t.Fatal("partial mutation", table) |
| + } |
| + } |
| + }) |
| + } |
| +} |
| + |
| +type failingSeal struct{} |
| + |
| +func (failingSeal) Seal(string, []byte) ([]byte, error) { |
| + return nil, errors.New("private seal failure") |
| +} |
| +func (failingSeal) Open(string, []byte) ([]byte, error) { |
| + return nil, errors.New("private seal failure") |
| +} |
| +func TestEngineConcurrentRetryHasOneMutation(t *testing.T) { |
| + f := newEngineFixture(t) |
| + var called atomic.Int32 |
| + var wg sync.WaitGroup |
| + errs := make(chan error, 8) |
| + for i := 0; i < 8; i++ { |
| + wg.Add(1) |
| + go func() { |
| + defer wg.Done() |
| + _, e := f.engine.Execute(context.Background(), f.raw, func(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) { |
| + called.Add(1) |
| + return insertOne(ctx, c, r) |
| + }) |
| + errs <- e |
| + }() |
| + } |
| + wg.Wait() |
| + close(errs) |
| + for e := range errs { |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + } |
| + if called.Load() != 1 { |
| + t.Fatal("duplicate side effects", called.Load()) |
| + } |
| +} |
| +func TestEngineForeignKeyEnforcementUsesMutationConnection(t *testing.T) { |
| + f := newEngineFixture(t) |
| + if _, e := f.db.Exec("CREATE TABLE parent(id INTEGER PRIMARY KEY); CREATE TABLE child(parent INTEGER REFERENCES parent(id))"); e != nil { |
| + t.Fatal(e) |
| + } |
| + _, e := f.engine.Execute(context.Background(), f.raw, func(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) { |
| + var fk int |
| + if e := c.QueryRowContext(ctx, "PRAGMA foreign_keys").Scan(&fk); e != nil || fk != 1 { |
| + t.Fatal("foreign keys not on actual connection") |
| + } |
| + _, e := c.ExecContext(ctx, "INSERT INTO child VALUES(999)") |
| + return privateResult, e |
| + }) |
| + if e != ErrUnavailable || countRows(t, f.db, "child") != 0 || countRows(t, f.db, "directory_operator_results") != 0 { |
| + t.Fatal("foreign key violation escaped", e) |
| + } |
| +} |
| +func TestEngineRefusesExpiredNewAndCorruptExistingResult(t *testing.T) { |
| + f := newEngineFixture(t) |
| + *f.now = f.now.Add(300 * time.Second) |
| + if _, e := f.engine.Execute(context.Background(), f.raw, insertOne); e != ErrRefused { |
| + t.Fatal("expired new") |
| + } |
| + if countRows(t, f.db, "test_items") != 0 { |
| + t.Fatal("expired side effect") |
| + } |
| + *f.now = f.now.Add(-300 * time.Second) |
| + if _, e := f.engine.Execute(context.Background(), f.raw, insertOne); e != nil { |
| + t.Fatal(e) |
| + } |
| + if _, e := f.db.Exec("UPDATE directory_operator_results SET sealed_result=zeroblob(29)"); e != nil { |
| + t.Fatal(e) |
| + } |
| + if _, e := f.engine.Execute(context.Background(), f.raw, insertOne); e != ErrUnavailable { |
| + t.Fatal("corrupt result", e) |
| + } |
| +} |
diff --git a/internal/operator/migrations/credential/migrations/0001_operator.sql b/internal/operator/migrations/credential/migrations/0001_operator.sql| new file mode 100644 |
| index 0000000..bb52cf6 |
| --- /dev/null |
| +++ b/internal/operator/migrations/credential/migrations/0001_operator.sql |
| @@ -0,0 +1,19 @@ |
| +CREATE TABLE credential_operator_state ( |
| + singleton INTEGER PRIMARY KEY CHECK(singleton=1), |
| + origin TEXT NOT NULL, |
| + installation TEXT NOT NULL, |
| + registry TEXT NOT NULL, |
| + device TEXT NOT NULL, |
| + sequence INTEGER NOT NULL CHECK(typeof(sequence)='integer' AND sequence>0), |
| + head TEXT NOT NULL CHECK(length(head)=43), |
| + completed_at INTEGER NOT NULL CHECK(typeof(completed_at)='integer' AND completed_at>0) |
| +); |
| +CREATE TABLE credential_operator_results ( |
| + operation_id TEXT PRIMARY KEY CHECK(length(operation_id)=43), |
| + action TEXT NOT NULL, |
| + sequence INTEGER NOT NULL UNIQUE CHECK(typeof(sequence)='integer' AND sequence>0), |
| + authorization_head TEXT NOT NULL CHECK(length(authorization_head)=43), |
| + request_mac BLOB NOT NULL CHECK(typeof(request_mac)='blob' AND length(request_mac)=32), |
| + sealed_result BLOB NOT NULL CHECK(typeof(sealed_result)='blob' AND length(sealed_result) BETWEEN 29 AND 49180), |
| + completed_at INTEGER NOT NULL CHECK(typeof(completed_at)='integer' AND completed_at>0) |
| +); |
diff --git a/internal/operator/migrations/directory/migrations/0001_operator.sql b/internal/operator/migrations/directory/migrations/0001_operator.sql| new file mode 100644 |
| index 0000000..c6df4b5 |
| --- /dev/null |
| +++ b/internal/operator/migrations/directory/migrations/0001_operator.sql |
| @@ -0,0 +1,19 @@ |
| +CREATE TABLE directory_operator_state ( |
| + singleton INTEGER PRIMARY KEY CHECK(singleton=1), |
| + origin TEXT NOT NULL, |
| + installation TEXT NOT NULL, |
| + registry TEXT NOT NULL, |
| + device TEXT NOT NULL, |
| + sequence INTEGER NOT NULL CHECK(typeof(sequence)='integer' AND sequence>0), |
| + head TEXT NOT NULL CHECK(length(head)=43), |
| + completed_at INTEGER NOT NULL CHECK(typeof(completed_at)='integer' AND completed_at>0) |
| +); |
| +CREATE TABLE directory_operator_results ( |
| + operation_id TEXT PRIMARY KEY CHECK(length(operation_id)=43), |
| + action TEXT NOT NULL, |
| + sequence INTEGER NOT NULL UNIQUE CHECK(typeof(sequence)='integer' AND sequence>0), |
| + authorization_head TEXT NOT NULL CHECK(length(authorization_head)=43), |
| + request_mac BLOB NOT NULL CHECK(typeof(request_mac)='blob' AND length(request_mac)=32), |
| + sealed_result BLOB NOT NULL CHECK(typeof(sealed_result)='blob' AND length(sealed_result) BETWEEN 29 AND 49180), |
| + completed_at INTEGER NOT NULL CHECK(typeof(completed_at)='integer' AND completed_at>0) |
| +); |