Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eql-domains-refuse-null-version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cipherstash/eql': patch
---

The stored and query-operand domains (`eql_v3_*`, `eql_v4_*`, `eql_v3.query_*`, `eql_v4.query_*`) now refuse a payload whose `v` is JSON `null`. The CHECK compared `v` with the bundle's version, and a comparison with `null` is NULL, which a CHECK lets through, so such a payload fitted both a v3 and a v4 domain. The range domains already refused it. A NULL value still fits every domain.
5 changes: 5 additions & 0 deletions .changeset/stack-auth-crate-0-44.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cipherstash/auth': patch
---

The native binaries are built from the `stack-auth` crate at its new version, 0.44.0 (it was 0.43.0). The crate changes that version carries are the ones this release already lists; the version number is what lets the crate publish to crates.io beside them. The package's API is unchanged.
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ incremental = true
# but cannot subtract them, and `stack-kms` / `stack-encrypt` need stack-auth
# without `http`. Consumers that want the default transport re-enable it with
# `features = ["http"]`.
stack-auth = { path = "./packages/stack-auth", version = "0.43.0", default-features = false }
stack-profile = { path = "./packages/stack-profile", version = "0.43.0" }
stack-auth = { path = "./packages/stack-auth", version = "0.44.0", default-features = false }
stack-profile = { path = "./packages/stack-profile", version = "0.44.0" }

# Suite crates, from crates.io; Cargo.lock holds the exact versions.
# cts-common and zerokms-protocol take caret requirements because the
Expand Down
4 changes: 2 additions & 2 deletions languages/golang/auth/guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions languages/golang/encrypt/guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion languages/golang/encrypt/postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,13 @@ func TestQueriesRunAgainstPostgres(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// reltuples is -1, an unknown count, until the table is analysed; the
// filter gives no row while it is.
if _, err := conn.ExecContext(ctx, `ANALYZE profiles`); err != nil {
t.Fatal(err)
}
var rows uint64
if err := conn.QueryRowContext(ctx, `SELECT greatest(reltuples, 0)::bigint FROM pg_class WHERE oid = 'profiles'::regclass`).Scan(&rows); err != nil {
if err := conn.QueryRowContext(ctx, `SELECT reltuples::bigint FROM pg_class WHERE oid = 'profiles'::regclass AND reltuples >= 0`).Scan(&rows); err != nil {
t.Fatal(err)
}
limit, err := q.TargetLimit(ctx, 10, encrypt.Stats{Rows: rows})
Expand Down
15 changes: 11 additions & 4 deletions languages/golang/encrypt/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"log/slog"
"math"

"github.com/cipherstash/stack/languages/golang/internal/record"
"github.com/cipherstash/vitaminc/bindings/go/vcffi"
Expand Down Expand Up @@ -60,8 +61,12 @@ type Bucket struct {
// size the fetch at. None of it is secret: the server holds the filters and
// the catalog holds the row count.
type Stats struct {
// Rows is the rows the query scans, such as greatest(reltuples, 0)
// from pg_class.
// Rows is the rows the query scans, such as reltuples from pg_class
// read with a reltuples >= 0 filter. reltuples is -1 for a table never
// vacuumed or analysed: that is an unknown count, not an empty table,
// and a count of 0 would promise a fetch of exactly want. While the
// count is unknown, call no TargetLimit and page with a cursor, or run
// ANALYZE first.
Rows uint64
// Histogram is a measured histogram of the stored filters' set-bit
// counts. With none, the engine uses the index's declared prior, and
Expand Down Expand Up @@ -101,12 +106,14 @@ type Limit struct {
func (l Limit) Rows() (uint64, bool) { return l.rows, l.bounded }

// FetchOr is the rows to fetch, or page when no finite fetch can be
// promised, for a caller that pages either way.
// promised, for a caller that pages either way. Never more than
// math.MaxInt64, the largest LIMIT PostgreSQL and database/sql take: a
// larger page is cut to it.
func (l Limit) FetchOr(page uint64) uint64 {
if l.bounded {
return l.rows
}
return page
return min(page, math.MaxInt64)
}

// QueryTerm derives the query term for one field of the plan, an operator
Expand Down
14 changes: 14 additions & 0 deletions languages/golang/encrypt/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ func TestTargetLimitIsTheRustFixtures(t *testing.T) {
}
}

// An unbounded limit pages, and a page past the largest LIMIT PostgreSQL
// takes is cut to it rather than bound as a value database/sql refuses.
func TestAnUnboundedLimitCutsThePageToTheLargestSQLLimit(t *testing.T) {
var unbounded encrypt.Limit
if got := unbounded.FetchOr(50); got != 50 {
t.Fatalf("FetchOr(50) = %d, want 50", got)
}
for _, page := range []uint64{math.MaxInt64, math.MaxInt64 + 1, math.MaxUint64} {
if got := unbounded.FetchOr(page); got != math.MaxInt64 {
t.Fatalf("FetchOr(%d) = %d, want %d", page, got, uint64(math.MaxInt64))
}
}
}

// A query the field does not answer is refused as a mistyped value is: an
// encoding error that names the field and the rule. The typed path's own
// refusals are term errors.
Expand Down
7 changes: 6 additions & 1 deletion languages/golang/encrypt/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,12 @@ func decodeRequest(item any) (ioRequest, bool) {
return ioRequest{}, false
}
}
return r, r.method != "" && r.url != ""
if r.method == "" || r.url == "" {
wipe(r.headers)
wipe(r.body)
return ioRequest{}, false
}
return r, true
}

// performStep performs every request of a step, in order, and encodes the
Expand Down
26 changes: 26 additions & 0 deletions languages/golang/encrypt/step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/cipherstash/stack/languages/golang/internal/guest"
"github.com/cipherstash/vitaminc/bindings/go/vcffi"
"github.com/cipherstash/vitaminc/bindings/go/vcvalue"
)

// The step loop, driven hermetically: the guest's init pauses on its
Expand Down Expand Up @@ -303,6 +304,31 @@ func TestAMalformedStepIsRefused(t *testing.T) {
}
}

// A request with no method or no URL is refused, and the bearer token and
// body it carried are wiped: the step never holds it, so its wipe cannot.
func TestARefusedRequestIsWiped(t *testing.T) {
for _, missing := range []string{"method", "url"} {
headers := []byte("authorization: Bearer stub-token")
body := []byte("wrapped key")
var obj vcvalue.Object
if missing != "method" {
obj = append(obj, vcvalue.Field{Key: "method", Value: "POST"})
}
if missing != "url" {
obj = append(obj, vcvalue.Field{Key: "url", Value: "https://zerokms.example/keys"})
}
obj = append(obj, vcvalue.Field{Key: "headers", Value: headers}, vcvalue.Field{Key: "body", Value: body})
if _, ok := decodeRequest(obj); ok {
t.Fatalf("a request with no %s was accepted", missing)
}
for name, b := range map[string][]byte{"headers": headers, "body": body} {
if !bytes.Equal(b, make([]byte, len(b))) {
t.Errorf("a request with no %s left its %s unwiped: %q", missing, name, b)
}
}
}
}

func marshalStep(t *testing.T, v any) []byte {
t.Helper()
b, err := vcffi.Marshal(v)
Expand Down
12 changes: 6 additions & 6 deletions packages/eql/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ BEGIN
{%- endfor %}
{{ v4_end }}
{%- endif %}
AND VALUE->>'v' = '{{ eql_version }}'
-- `"v": null` makes the comparison NULL, and a CHECK that is NULL
-- passes, so COALESCE refuses it. A NULL value still passes.
AND COALESCE(VALUE->>'v' = '{{ eql_version }}', VALUE IS NULL)
);
END IF;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ AS $$ SELECT {{ a.body }} $$;
--!
--! True when `{{ internal_schema }}.lo(r) <= {{ schema }}.{{ b.extractor }}(a) <= {{ internal_schema }}.hi(r)`.
--! The range is one operand, so a statement cannot pair the low bound of one
--! query with the high bound of another (ADR-0009, decision 7):
--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7):
--! `WHERE {{ schema }}.between(col, $1::{{ schema }}.{{ b.range }})`.
--!
--! @param a {{ b.column }} Encrypted column value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
--! `{"v": {{ eql_version }}, "i": {...}, "lo": <term>, "hi": <term>}`. Each bound is the
--! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a
--! `{v, i, lo, hi}` envelope. Both bounds are included. There is one
--! range domain per ordering term the family stores (ADR-0009,
--! decisions 7 and 8).
--! range domain per ordering term the family stores (Stack Encrypt
--! ADR-0009, decisions 7 and 8).
--! @note Cast the range parameter to its exact range domain
--! (`$1::{{ schema }}.query_{{ family_name }}_range_ore`). An uncast `$1`, a `jsonb`
--! parameter, or a range of another term or family fails with 42725
Expand Down
4 changes: 3 additions & 1 deletion packages/eql/crates/eql-codegen/templates/types.sql.j2
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ BEGIN
{%- endfor %}
{{ v4_end }}
{%- endif %}
AND VALUE->>'v' = '{{ eql_version }}'
-- `"v": null` makes the comparison NULL, and a CHECK that is NULL
-- passes, so COALESCE refuses it. A NULL value still passes.
AND COALESCE(VALUE->>'v' = '{{ eql_version }}', VALUE IS NULL)
);
END IF;

Expand Down
20 changes: 15 additions & 5 deletions packages/eql/src/v3/scalars/bigint/bigint_types.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading