package operator import ( "context" "crypto/hmac" "crypto/sha256" "database/sql" "database/sql/driver" "embed" "errors" "io/fs" "math" "time" "amadan.net/rastrillo/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 checkSchema func(context.Context, *sql.Conn) error } 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}, 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 }) } } }() if e.checkSchema != nil { if err := e.checkSchema(ctx, conn); err != nil { return nil, err } } var storedOrigin, storedInstallation, storedRegistry, storedDevice, storedHead, storedKey string var storedSequence, storedCompleted int64 stateErr := conn.QueryRowContext(ctx, "SELECT origin,installation,registry,device,sequence,head,completed_at,key_id FROM "+e.prefix+"_state WHERE singleton=1").Scan(&storedOrigin, &storedInstallation, &storedRegistry, &storedDevice, &storedSequence, &storedHead, &storedCompleted, &storedKey) 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 || storedKey != e.verifier.kid || storedSequence <= 0 || !CanonicalToken(storedHead) || storedCompleted <= 0 || e.now().Unix() < storedCompleted) { return nil, ErrRefused } var oldAction, oldHead, oldOperator string var oldSequence int64 var oldMAC, sealed []byte err = conn.QueryRowContext(ctx, "SELECT action,sequence,authorization_head,request_mac,sealed_result,operator_ref FROM "+e.prefix+"_results WHERE operation_id=?", a.OperationID).Scan(&oldAction, &oldSequence, &oldHead, &oldMAC, &sealed, &oldOperator) if err == nil { if stateErr != nil || storedSequence < a.Sequence || oldOperator != a.Device || 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,operator_ref) VALUES(?,?,?,?,?,?,?,?)", a.OperationID, a.Action, a.Sequence, req.head, e.mac(req), sealed, completed.Unix(), a.Device) if err != nil { return nil, ErrUnavailable } _, err = conn.ExecContext(ctx, "INSERT INTO "+e.prefix+"_state(singleton,origin,installation,registry,device,sequence,head,completed_at,key_id) 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(), e.verifier.kid) 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 }