From 22d98ddfd13865ae6cd01f8a9d78db6f7f1fea75 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Mon, 20 Jul 2026 16:06:52 +1000 Subject: [PATCH 1/4] chore(deps): upgrade cipherstash-client to 0.42.0 and EQL to 3.0.1 Knowingly leaves the workspace not compiling. Fixing the fallout is deliberately deferred. - cipherstash-client / cipherstash-config / cts-common: 0.34.1-alpha.4 -> 0.42.0 - CS_EQL_VERSION: eql-2.3.0-pre.3 -> eql-3.0.1 (major, breaking) - Drop the CIP-3159 vendored stack-auth patch: 0.42.0 requires stack-auth ^0.42.0, which carries the CancelGuard fix upstream. vendor/stack-auth is left in place but is now unreferenced. Queued-Commit-Id: q-0c7qhchxzewgy0q472w3vcps2t --- Cargo.lock | 482 ++++++++++++++-------------------------- Cargo.toml | 18 +- mise.local.example.toml | 2 +- mise.toml | 2 +- 4 files changed, 171 insertions(+), 333 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2e0c684..e3cc5b16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.0" @@ -23,7 +14,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -39,6 +30,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", + "zeroize", +] + [[package]] name = "aes-gcm-siv" version = "0.11.1" @@ -192,12 +198,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "array-init" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" - [[package]] name = "arrayref" version = "0.3.9" @@ -321,9 +321,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -332,14 +332,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -431,30 +432,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -544,6 +521,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-modes" version = "0.9.1" @@ -737,15 +723,15 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] [[package]] name = "cipherstash-client" -version = "0.34.1-alpha.4" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3d67cc26d8422509d2c20644576124e7344a4bf14ded06c7affa8dc18aabca" +checksum = "6c8714a2997ab5a8cc2c871f01dcae9ff7c6b342ed51c5c0d7c8edd4d954c9d1" dependencies = [ "aes-gcm-siv", "anyhow", @@ -753,9 +739,9 @@ dependencies = [ "async-trait", "base16ct", "base64", + "base64ct", "base85", "blake3", - "cfg-if", "chrono", "cipherstash-config", "cipherstash-core", @@ -771,16 +757,12 @@ dependencies = [ "log", "miette", "opaque-debug", - "open 3.2.0", "orderable-bytes", "ore-rs", "percent-encoding", "rand 0.8.6", - "recipher 0.2.2", + "recipher 0.2.3", "reqwest", - "reqwest-middleware", - "reqwest-retry", - "reqwest-tracing", "rmp-serde", "rust-stemmers", "rust_decimal", @@ -800,7 +782,7 @@ dependencies = [ "url", "uuid", "vitaminc", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "winnow 0.6.26", "zeroize", "zerokms-protocol", @@ -808,9 +790,9 @@ dependencies = [ [[package]] name = "cipherstash-config" -version = "0.34.1-alpha.4" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283fa04db19f9bf2cb2f09e8c1505a15560310bc50fdc066734072c616aa8ca9" +checksum = "56cde3aaa5e2916a40530932f142c37ad6202835a6d221ce6c46fc14bfec8fc5" dependencies = [ "bitflags", "serde", @@ -820,10 +802,11 @@ dependencies = [ [[package]] name = "cipherstash-core" -version = "0.34.1-alpha.4" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5bb7181053c3fc35569e0800fa7510c85ee2bffee21abbd2a7aeb498f5f0972" +checksum = "1946988e0b7f9de259d85b10c9c1fd7e1327751103b808c2c6e930b9a87c25c9" dependencies = [ + "getrandom 0.2.15", "hmac", "lazy_static", "num-bigint", @@ -881,7 +864,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", - "vitaminc-protected", + "vitaminc-protected 0.1.0-pre4.2", "x509-parser", ] @@ -954,15 +937,14 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cllw-ore" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d007a5be83ae12adbd17543f9631d64090d761c029d2f8f7eb8f8ddb2a87caf" +checksum = "4f73a23cbc15404d9b314c03b16a888f798dbc681bceeb2e18674f602f9da02d" dependencies = [ "blake3", "chrono", "hex", "orderable-bytes", - "postgres-types", "rust_decimal", "subtle", "thiserror 1.0.69", @@ -977,7 +959,7 @@ checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" dependencies = [ "cipher", "dbl", - "digest", + "digest 0.10.7", ] [[package]] @@ -1157,6 +1139,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1168,9 +1159,9 @@ dependencies = [ [[package]] name = "cts-common" -version = "0.34.1-alpha.4" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b26644e630f2e690194c6b61f5b613b768061750f2060cf4db73ddb8058d284" +checksum = "fe2acdda527057d48061433ace5378d8452f10d6787f78e0e69b1cddacc57082" dependencies = [ "arrayvec", "axum", @@ -1182,6 +1173,7 @@ dependencies = [ "diesel", "either", "fake 3.1.0", + "getrandom 0.4.2", "http", "miette", "nom 8.0.0", @@ -1388,11 +1380,21 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1799,7 +1801,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" dependencies = [ - "rustix 0.38.44", + "rustix", "windows-targets 0.52.6", ] @@ -1837,18 +1839,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "ghash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] [[package]] name = "h2" @@ -1976,7 +1984,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2025,6 +2033,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2364,12 +2381,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is_ci" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2496,12 +2507,6 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" -[[package]] -name = "linux-raw-sys" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" - [[package]] name = "litemap" version = "0.7.5" @@ -2565,7 +2570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2627,18 +2632,10 @@ version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" dependencies = [ - "backtrace", - "backtrace-ext", "cfg-if", "miette-derive", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", "thiserror 1.0.69", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] @@ -2813,15 +2810,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.8.1" @@ -2847,16 +2835,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "open" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" -dependencies = [ - "pathdiff", - "windows-sys 0.42.0", -] - [[package]] name = "open" version = "5.3.3" @@ -2928,12 +2906,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "owo-colors" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" - [[package]] name = "parking" version = "2.2.1" @@ -3114,7 +3086,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" dependencies = [ - "array-init", "bytes", "chrono", "fallible-iterator", @@ -3388,9 +3359,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -3491,13 +3462,13 @@ dependencies = [ [[package]] name = "recipher" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b3561e1082283a4c064635b7886aa4d24db57a43ac31c55930c10797ab5cdeb" +checksum = "9398dce78ddfce08f93e9d9a3ac64d9b0a4fed478c0a82003c6e4c90dc245125" dependencies = [ "aes", - "async-trait", "cmac", + "getrandom 0.2.15", "hex", "hex-literal", "opaque-debug", @@ -3634,73 +3605,12 @@ dependencies = [ "web-sys", ] -[[package]] -name = "reqwest-middleware" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199dda04a536b532d0cc04d7979e39b1c763ea749bf91507017069c00b96056f" -dependencies = [ - "anyhow", - "async-trait", - "http", - "reqwest", - "serde", - "thiserror 2.0.18", - "tower-service", -] - -[[package]] -name = "reqwest-retry" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe2412db2af7d2268e7a5406be0431f37d9eb67ff390f35b395716f5f06c2eaa" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "getrandom 0.2.15", - "http", - "hyper", - "reqwest", - "reqwest-middleware", - "retry-policies", - "thiserror 2.0.18", - "tokio", - "tracing", - "wasmtimer", -] - -[[package]] -name = "reqwest-tracing" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5c1a1510677d43dce9e9c0c07fc5db8772c0e5a43e4f9cef75a11affa05a578" -dependencies = [ - "anyhow", - "async-trait", - "getrandom 0.2.15", - "http", - "matchit", - "reqwest", - "reqwest-middleware", - "tracing", -] - [[package]] name = "resolv-conf" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" -[[package]] -name = "retry-policies" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a4bd6027df676bcb752d3724db0ea3c0c5fc1dd0376fec51ac7dcaf9cc69be" -dependencies = [ - "rand 0.9.2", -] - [[package]] name = "ring" version = "0.17.14" @@ -3792,12 +3702,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -3831,20 +3735,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.9.3", + "linux-raw-sys", "windows-sys 0.59.0", ] @@ -4192,7 +4083,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -4339,13 +4230,16 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stack-auth" -version = "0.34.1-alpha.4" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1c9c640571eba8fa5a705ccebe5c5e50aa27d4d51f4ec1614ffd1841b8a13a" dependencies = [ "aquamarine", + "base64", "cts-common", "jsonwebtoken", "miette", - "open 5.3.3", + "open", "reqwest", "serde", "serde_json", @@ -4356,16 +4250,17 @@ dependencies = [ "url", "uuid", "vitaminc", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", + "web-time", "zeroize", "zerokms-protocol", ] [[package]] name = "stack-profile" -version = "0.34.1-alpha.4" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd61bc4129d2258ec1ba89d742558308560fc0f585f9c24d478685def8efd14" +checksum = "192a90bfa46efe194c2b8beae5523f6214714574c7e8a210c78021259f100e79" dependencies = [ "dirs", "gethostname", @@ -4423,27 +4318,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" -[[package]] -name = "supports-color" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" -dependencies = [ - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" - -[[package]] -name = "supports-unicode" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" - [[package]] name = "syn" version = "1.0.109" @@ -4528,26 +4402,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "terminal_size" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" -dependencies = [ - "rustix 1.0.3", - "windows-sys 0.59.0", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "unicode-linebreak", - "unicode-width 0.2.0", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -4943,9 +4797,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unarray" @@ -4965,12 +4819,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - [[package]] name = "unicode-normalization" version = "0.1.24" @@ -4998,12 +4846,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -5016,7 +4858,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -5095,9 +4937,11 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "atomic", "getrandom 0.3.2", + "js-sys", "md-5", "serde", "sha1_smol", + "wasm-bindgen", ] [[package]] @@ -5150,39 +4994,40 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8b739a2cb1e528e77a69267728532f52d2d5ce18ae2839e26c797859fe9015" +checksum = "d69481bc78bc3227d6c70d8aae6437c79badbf54fd9ec90c1b4ae2553068a989" dependencies = [ "vitaminc-aead", "vitaminc-encrypt", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "vitaminc-traits", ] [[package]] name = "vitaminc-aead" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c29cef4d4b0d018c4223d366017d2a9756012acf76e25011aaca877f3c74904" +checksum = "be80f3a3d83e69a786b97a831d660449a0437ccac3b3e369bf590afcb45569b0" dependencies = [ "bytes", "serde", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "zeroize", ] [[package]] name = "vitaminc-encrypt" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e3869aaf60ebb95ccbdfcf003985132325b4d1ac6f5d945ad2fbb9149afd3a" +checksum = "7477ef8ac925a75aacf5dbddfd4b17fd32f35ee9fb4a7c45ac3db80fd9ad4006" dependencies = [ + "aes-gcm", "aws-lc-rs", "vitaminc-aead", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "zeroize", ] @@ -5194,11 +5039,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af693c39d3cd1c818ef6267539433c6ceca87840b12d24124adbc9c8ecba1709" dependencies = [ "bitvec", - "digest", + "digest 0.10.7", + "serde", + "serde_bytes", + "subtle", + "vitaminc-protected-derive 0.1.0-pre4.2", + "zeroize", +] + +[[package]] +name = "vitaminc-protected" +version = "0.2.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8472e2b76b5dedaf429708393964c3cc6f7ee40e6a43ed420288e3e4900c6af" +dependencies = [ + "bitvec", + "digest 0.11.3", "serde", "serde_bytes", "subtle", - "vitaminc-protected-derive", + "vitaminc-protected-derive 0.2.0-pre.1", "zeroize", ] @@ -5213,24 +5073,36 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "vitaminc-protected-derive" +version = "0.2.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01e1715676d8bf606314c2a51df0793c01bd743bae4bc00643d68f766ee1e91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "vitaminc-random" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9de431cb93359d293ec7e70d05d87117a57f34bfc5bc94f040b81d4dd1afd6" +checksum = "b0785c13f839240523ba8db6535384a5e8d4fe2b2f28bbddcfcb5fd6de825996" dependencies = [ - "rand 0.10.0", + "getrandom 0.4.2", + "rand 0.10.2", "thiserror 2.0.18", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random-derives", "zeroize", ] [[package]] name = "vitaminc-random-derives" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d33ac4682235551d25c874525c20e03d4c863b39f556391f52f7a2083bfbdf" +checksum = "01e750eefb1f49940f589b2d397e2323d5df4b62bfb33b4e40e1d20a35c3f167" dependencies = [ "proc-macro2", "quote", @@ -5239,16 +5111,16 @@ dependencies = [ [[package]] name = "vitaminc-traits" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c25a9e51d24c3befddd71e907dd4ae9f21cfbaae065fb0ef5202e5d21cd198d0" +checksum = "3794e2c028cff00f40caea05ab6dce38181a94e13c0aaee640e7b867369780eb" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.18", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "zeroize", ] @@ -5429,20 +5301,6 @@ dependencies = [ "semver", ] -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot", - "pin-utils", - "slab", - "wasm-bindgen", -] - [[package]] name = "web-sys" version = "0.3.77" @@ -5666,21 +5524,6 @@ dependencies = [ "windows-link 0.1.1", ] -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -6275,15 +6118,16 @@ dependencies = [ [[package]] name = "zerokms-protocol" -version = "0.12.9" +version = "0.12.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2f045e2ee975a3d448419245c4621ea8844d2a004c63a96277181dc7cf8483" +checksum = "16f731f2de99e66396928faef44b02b39288dc9a93f77a4a3e1dcdc33c1adad0" dependencies = [ "base64", "cipherstash-config", "const-hex", "cts-common", "fake 2.10.0", + "getrandom 0.2.15", "opaque-debug", "rand 0.8.6", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5cbfa718..c6413e56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,9 +45,9 @@ debug = true [workspace.dependencies] sqltk = { version = "0.10.0" } -cipherstash-client = { version = "=0.34.1-alpha.4" } -cipherstash-config = { version = "=0.34.1-alpha.4" } -cts-common = { version = "=0.34.1-alpha.4" } +cipherstash-client = { version = "=0.42.0" } +cipherstash-config = { version = "=0.42.0" } +cts-common = { version = "=0.42.0" } thiserror = "2.0.9" tokio = { version = "1.44.2", features = ["full"] } @@ -59,12 +59,6 @@ tracing-subscriber = { version = "^0.3.20", features = [ "std", ] } -# HOTFIX (CIP-3159): backport the stack-auth token-refresh CancelGuard fix onto -# the 0.34.1-alpha.4 source that cipherstash-client 0.34.1-alpha.4 pins. Without -# this, a cancelled get_token() future could strand `refresh_in_progress = true`, -# wedging all later refreshes and causing ZeroKMS "Request not authorized" exactly -# ~15 min (token TTL) after startup. The patch keeps version 0.34.1-alpha.4 so it -# satisfies cipherstash-client's exact pin while replacing the registry source. -# Remove once Proxy moves to a cipherstash-client built against stack-auth >= 0.36.0. -[patch.crates-io] -stack-auth = { path = "vendor/stack-auth" } +# The CIP-3159 stack-auth hotfix patch was removed here: cipherstash-client 0.42.0 +# requires stack-auth ^0.42.0, which carries the CancelGuard token-refresh fix +# upstream (landed in stack-auth 0.36.0). vendor/stack-auth is now unreferenced. diff --git a/mise.local.example.toml b/mise.local.example.toml index 6e72948a..fbf0d567 100644 --- a/mise.local.example.toml +++ b/mise.local.example.toml @@ -15,7 +15,7 @@ CS_CLIENT_KEY = "client-key" CS_CLIENT_ID = "client-id" # The release of EQL that the proxy tests will use and releases will be built with -CS_EQL_VERSION = "eql-2.3.0-pre.3" +CS_EQL_VERSION = "eql-3.0.1" # TLS variables are required for providing TLS to Proxy's clients. # CS_TLS__TYPE can be either "Path" or "Pem" (case-sensitive). diff --git a/mise.toml b/mise.toml index 75110dd1..886faca4 100644 --- a/mise.toml +++ b/mise.toml @@ -34,7 +34,7 @@ CS_PROXY__HOST = "host.docker.internal" # Misc DOCKER_CLI_HINTS = "false" # Please don't show us What's Next. -CS_EQL_VERSION = "eql-2.3.0-pre.3" +CS_EQL_VERSION = "eql-3.0.1" [tools] From 6a050d585caf13c14500fcf67be9135099705507 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Mon, 20 Jul 2026 16:29:52 +1000 Subject: [PATCH 2/4] feat(encrypt): produce EQL v3 payloads, retire v2 Switch the encrypt path to cipherstash-client 0.42.0's encrypt_eql_v3 and make v3 the only wire envelope Proxy speaks. Still does not compile; threading the new types through the protocol layer is deferred. - lib.rs re-exports the V3 types under the old names and deliberately does NOT re-export the v2 ones, so any remaining v2 use fails to resolve rather than silently writing v2 payloads. - EQL_SCHEMA_VERSION 2 -> 3; startup gate now queries eql_v3.version() (eql_v2 schema no longer exists in EQL 3). - Decrypt is now local: there is no decrypt_eql_v3 in the client, but a scalar payload's 'c' is an EncryptedRecord and EncryptedRecord is Decryptable, so it goes straight to the cipher. - SteVec (jsonb) decrypt returns SteVecV3DecryptUnsupported: v3 stores the key header once at the document root with raw AEAD bytes per entry, and reassembling that here would hard-code the envelope layout. Queued-Commit-Id: q-4n8w7v1b7dadsf27c98sz33kk1 --- packages/cipherstash-proxy/src/error.rs | 9 +++ packages/cipherstash-proxy/src/lib.rs | 11 +++- packages/cipherstash-proxy/src/proxy/mod.rs | 4 +- .../src/proxy/zerokms/zerokms.rs | 55 +++++++++++++------ 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/packages/cipherstash-proxy/src/error.rs b/packages/cipherstash-proxy/src/error.rs index aee61575..8c8e2be8 100644 --- a/packages/cipherstash-proxy/src/error.rs +++ b/packages/cipherstash-proxy/src/error.rs @@ -254,6 +254,15 @@ pub enum EncryptError { #[error("InvalidIndexTerm")] InvalidIndexTerm, + /// EQL v3 SteVec (jsonb) documents carry the key header once at the document + /// root and raw AEAD bytes per entry, so an `EncryptedRecord` has to be + /// reassembled from `h` + `sv[0].c` before it can be decrypted. + /// cipherstash-client exposes no `decrypt_eql_v3`, and reassembling the + /// record here would hard-code the envelope layout. Blocked until the client + /// provides a v3 decrypt path. + #[error("Decrypting EQL v3 jsonb (SteVec) columns is not yet supported")] + SteVecV3DecryptUnsupported, + #[error( "KeysetId `{id}` could not be parsed using `SET CIPHERSTASH.KEYSET_ID`. KeysetId should be a valid UUID. For help visit {}#encrypt-keyset-id-could-not-be-parsed", ERROR_DOC_BASE_URL diff --git a/packages/cipherstash-proxy/src/lib.rs b/packages/cipherstash-proxy/src/lib.rs index 2d4ac8fa..3c7c9d69 100644 --- a/packages/cipherstash-proxy/src/lib.rs +++ b/packages/cipherstash-proxy/src/lib.rs @@ -16,13 +16,20 @@ pub use crate::config::{DatabaseConfig, ServerConfig, TandemConfig, TlsConfig}; pub use crate::log::init; pub use crate::proxy::Proxy; pub use cipherstash_client::encryption::Plaintext; -pub use cipherstash_client::eql::{EqlCiphertext, Identifier}; +// EQL v3 is the only wire envelope Proxy speaks. The v2 types +// (`EqlCiphertext`, `EqlOutput`, `EqlQueryPayload`) are deliberately not +// re-exported — v2 support is retired, so anything still reaching for them +// should fail to resolve rather than silently keep writing v2 payloads. +pub use cipherstash_client::eql::{ + EqlCiphertextV3 as EqlCiphertext, EqlOutputV3 as EqlOutput, + EqlQueryPayloadV3 as EqlQueryPayload, Identifier, +}; use std::mem; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const EQL_SCHEMA_VERSION: u16 = 2; +pub const EQL_SCHEMA_VERSION: u16 = 3; pub const SIZE_U8: usize = mem::size_of::(); pub const SIZE_I16: usize = mem::size_of::(); diff --git a/packages/cipherstash-proxy/src/proxy/mod.rs b/packages/cipherstash-proxy/src/proxy/mod.rs index 9a46b990..894e60f4 100644 --- a/packages/cipherstash-proxy/src/proxy/mod.rs +++ b/packages/cipherstash-proxy/src/proxy/mod.rs @@ -88,7 +88,7 @@ impl Proxy { pub async fn eql_version(config: &TandemConfig) -> Result, Error> { let client = connect::database(&config.database).await?; let rows = client - .query("SELECT eql_v2.version() AS version;", &[]) + .query("SELECT eql_v3.version() AS version;", &[]) .await; let version = match rows { @@ -156,7 +156,7 @@ pub trait EncryptionService: Send + Sync { keyset_id: Option, plaintexts: Vec>, columns: &[Option], - ) -> Result>, Error>; + ) -> Result>, Error>; /// Decrypt values retrieved from the database async fn decrypt( diff --git a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs index 15e120d3..95804959 100644 --- a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs +++ b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs @@ -10,12 +10,13 @@ use crate::{ proxy::EncryptionService, }; use cipherstash_client::{ - encryption::{Plaintext, QueryOp}, + encryption::{DecryptOptions, Plaintext, QueryOp}, eql::{ - decrypt_eql, encrypt_eql, EqlCiphertext, EqlDecryptOpts, EqlEncryptOpts, EqlOperation, + encrypt_eql_v3, EqlCiphertextV3, EqlEncryptOpts, EqlOperation, EqlOutputV3, PreparedPlaintext, }, schema::column::IndexType, + zerokms::WithContext, }; use eql_mapper::EqlTermVariant; use metrics::{counter, histogram}; @@ -157,7 +158,7 @@ impl EncryptionService for ZeroKms { keyset_id: Option, plaintexts: Vec>, columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { debug!(target: ENCRYPT, msg="Encrypt", ?keyset_id, default_keyset_id = ?self.default_keyset_id); // A keyset is required if no default keyset has been configured @@ -222,16 +223,16 @@ impl EncryptionService for ZeroKms { // Use default opts since cipher is already initialized with the correct keyset let opts = EqlEncryptOpts::default(); - debug!(target: ENCRYPT, msg="Calling encrypt_eql", count = prepared_plaintexts.len()); + debug!(target: ENCRYPT, msg="Calling encrypt_eql_v3", count = prepared_plaintexts.len()); let encrypt_start = Instant::now(); - let encrypted = encrypt_eql(cipher, prepared_plaintexts, &opts) + let encrypted = encrypt_eql_v3(cipher, prepared_plaintexts, &opts) .await .map_err(EncryptError::from)?; let encrypt_duration = encrypt_start.elapsed(); - debug!(target: ENCRYPT, msg="encrypt_eql completed", count = encrypted.len(), duration_ms = encrypt_duration.as_millis()); + debug!(target: ENCRYPT, msg="encrypt_eql_v3 completed", count = encrypted.len(), duration_ms = encrypt_duration.as_millis()); // Reconstruct the result vector with None values in the right places - let mut result: Vec> = vec![None; plaintexts.len()]; + let mut result: Vec> = vec![None; plaintexts.len()]; for (idx, ciphertext) in indices.into_iter().zip(encrypted.into_iter()) { result[idx] = Some(ciphertext); } @@ -247,7 +248,7 @@ impl EncryptionService for ZeroKms { async fn decrypt( &self, keyset_id: Option, - ciphertexts: Vec>, + ciphertexts: Vec>, ) -> Result>, Error> { debug!(target: ENCRYPT, msg="Decrypt", ?keyset_id, default_keyset_id = ?self.default_keyset_id); @@ -258,32 +259,52 @@ impl EncryptionService for ZeroKms { let cipher = self.init_cipher(keyset_id.clone()).await?; - // Collect indices and ciphertexts for non-None values + // Collect indices and the root records for non-None values. + // + // cipherstash-client has no `decrypt_eql_v3` counterpart to + // `encrypt_eql_v3` — the v2 `decrypt_eql` only accepts `EqlCiphertext`. + // For scalar payloads that costs us nothing: `EncryptedPayloadV3.c` is + // the same `EncryptedRecord` the v2 path would have unwrapped, and + // `EncryptedRecord` is itself `Decryptable`, so we hand it straight to + // the cipher. SteVec documents are the part that genuinely needs the + // client (see `SteVecV3DecryptUnsupported`). let mut indices: Vec = Vec::new(); - let mut ciphertexts_to_decrypt: Vec = Vec::new(); + let mut records_to_decrypt = Vec::new(); for (idx, ct_opt) in ciphertexts.iter().enumerate() { if let Some(ct) = ct_opt { + let record = match ct { + EqlCiphertextV3::Encrypted(payload) => payload.ciphertext.clone(), + EqlCiphertextV3::SteVec(_) => { + return Err(EncryptError::SteVecV3DecryptUnsupported.into()) + } + }; indices.push(idx); - ciphertexts_to_decrypt.push(ct.clone()); + records_to_decrypt.push(record); } } // If no ciphertexts to decrypt, return all None - if ciphertexts_to_decrypt.is_empty() { + if records_to_decrypt.is_empty() { return Ok(vec![None; ciphertexts.len()]); } - // Use default opts since cipher is already initialized with the correct keyset - let opts = EqlDecryptOpts::default(); + // Default opts: the cipher is already scoped to the right keyset, and + // Proxy does not set a lock context. + let opts = DecryptOptions::default(); - debug!(target: ENCRYPT, msg="Calling decrypt_eql", count = ciphertexts_to_decrypt.len()); + debug!(target: ENCRYPT, msg="Decrypting EQL v3 records", count = records_to_decrypt.len()); let decrypt_start = Instant::now(); - let decrypted = decrypt_eql(cipher, ciphertexts_to_decrypt, &opts) + let decrypted = cipher + .decrypt(records_to_decrypt, &opts) .await + .map_err(ZeroKMSError::from)? + .into_iter() + .map(|bytes| Plaintext::from_slice(&bytes)) + .collect::, _>>() .map_err(EncryptError::from)?; let decrypt_duration = decrypt_start.elapsed(); - debug!(target: ENCRYPT, msg="decrypt_eql completed", count = decrypted.len(), duration_ms = decrypt_duration.as_millis()); + debug!(target: ENCRYPT, msg="Decrypt completed", count = decrypted.len(), duration_ms = decrypt_duration.as_millis()); // Reconstruct the result vector with None values in the right places let mut result: Vec> = vec![None; ciphertexts.len()]; From e11ed9bf748cb513961815630b2ae9b5527762f9 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Mon, 20 Jul 2026 16:43:08 +1000 Subject: [PATCH 3/4] fix(decrypt): implement EQL v3 decrypt for scalar and SteVec payloads Follows the approach protect-ffi uses (encrypted_record_from_value / v3_root_record): skip decrypt_eql entirely and hand an EncryptedRecord straight to the cipher. Replaces the SteVecV3DecryptUnsupported error added in the previous commit, which was wrong -- KeyHeader::record_with_selector is public and does exactly the reassembly needed. Scalar and SteVec records are NOT interchangeable, hence the V3Record enum: RecordWithNonce unconditionally reports a nonce override and an AAD selector, so wrapping a scalar record in one would decrypt against a nonce the value was never encrypted with. Note protect-ffi's SteVec branch does not port directly: it pins cipherstash-client =0.41.1, where sv entries are self-describing mp_base85 records. 0.42.0 moved to an envelope -- key material once in the document's 'h' header, raw AEAD bytes per entry, nonce and AAD derived from the entry selector. Queued-Commit-Id: q-2jsj9wjxvdj9s21xzpmp838x93 --- packages/cipherstash-proxy/src/error.rs | 17 +-- .../src/proxy/zerokms/zerokms.rs | 109 ++++++++++++++++-- 2 files changed, 108 insertions(+), 18 deletions(-) diff --git a/packages/cipherstash-proxy/src/error.rs b/packages/cipherstash-proxy/src/error.rs index 8c8e2be8..d5f7a9eb 100644 --- a/packages/cipherstash-proxy/src/error.rs +++ b/packages/cipherstash-proxy/src/error.rs @@ -254,14 +254,15 @@ pub enum EncryptError { #[error("InvalidIndexTerm")] InvalidIndexTerm, - /// EQL v3 SteVec (jsonb) documents carry the key header once at the document - /// root and raw AEAD bytes per entry, so an `EncryptedRecord` has to be - /// reassembled from `h` + `sv[0].c` before it can be decrypted. - /// cipherstash-client exposes no `decrypt_eql_v3`, and reassembling the - /// record here would hard-code the envelope layout. Blocked until the client - /// provides a v3 decrypt path. - #[error("Decrypting EQL v3 jsonb (SteVec) columns is not yet supported")] - SteVecV3DecryptUnsupported, + /// `sv[0]` is the decryption root of a SteVec document, so an empty `sv` + /// array leaves nothing to decrypt. + #[error("Encrypted jsonb value has no root entry and cannot be decrypted")] + SteVecMissingRootEntry, + + /// A SteVec entry's selector is the source of both AEAD bindings (nonce and + /// AAD), so it must be exactly 16 hex-encoded bytes. + #[error("Encrypted jsonb entry has an invalid selector '{selector}'")] + SteVecSelectorInvalid { selector: String }, #[error( "KeysetId `{id}` could not be parsed using `SET CIPHERSTASH.KEYSET_ID`. KeysetId should be a valid UUID. For help visit {}#encrypt-keyset-id-could-not-be-parsed", diff --git a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs index 95804959..6fe4d484 100644 --- a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs +++ b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs @@ -16,8 +16,9 @@ use cipherstash_client::{ PreparedPlaintext, }, schema::column::IndexType, - zerokms::WithContext, + zerokms::{Decryptable, EncryptedRecord, RecordWithNonce, RetrieveKeyPayload}, }; +use std::convert::Infallible; use eql_mapper::EqlTermVariant; use metrics::{counter, histogram}; use moka::future::Cache; @@ -34,6 +35,75 @@ use super::{init_zerokms_client, ScopedCipher, ZerokmsClient}; /// Memory size of a single ScopedCipher instance for cache weighing const SCOPED_CIPHER_SIZE: usize = std::mem::size_of::(); +/// An EQL v3 stored payload reduced to something the cipher can decrypt. +/// +/// The two arms are not interchangeable, which is why this exists rather than a +/// plain `Vec`: `RecordWithNonce` unconditionally reports a +/// nonce override and an AAD selector, so wrapping a scalar record in one would +/// decrypt against a nonce the value was never encrypted with. +#[derive(Debug)] +enum V3Record { + /// A scalar payload's `c` — self-describing, nonce derived from the data + /// key's IV, nothing bound into the AAD. + Scalar(EncryptedRecord), + /// A SteVec document's root entry, reassembled from the document's `h` + /// header. Nonce and AAD both derive from the entry's selector. + SteVecRoot(RecordWithNonce), +} + +impl Decryptable for V3Record { + type Error = Infallible; + + fn keyset_id(&self) -> Option { + match self { + V3Record::Scalar(record) => record.keyset_id(), + V3Record::SteVecRoot(record) => record.keyset_id(), + } + } + + fn retrieve_key_payload(&self) -> Result, Self::Error> { + match self { + V3Record::Scalar(record) => record.retrieve_key_payload(), + V3Record::SteVecRoot(record) => record.retrieve_key_payload(), + } + } + + fn into_encrypted_record(self) -> Result { + match self { + V3Record::Scalar(record) => record.into_encrypted_record(), + V3Record::SteVecRoot(record) => record.into_encrypted_record(), + } + } + + fn nonce_override(&self) -> Option<[u8; 12]> { + match self { + V3Record::Scalar(_) => None, + V3Record::SteVecRoot(record) => record.nonce_override(), + } + } + + fn aad_selector(&self) -> Option<[u8; 16]> { + match self { + V3Record::Scalar(_) => None, + V3Record::SteVecRoot(record) => record.aad_selector(), + } + } +} + +/// Decode a SteVec entry's hex-encoded tokenized selector into the 16 bytes the +/// AEAD binding needs. +fn decode_ste_vec_selector(selector: &str) -> Result<[u8; 16], EncryptError> { + let bytes = hex::decode(selector).map_err(|_| EncryptError::SteVecSelectorInvalid { + selector: selector.to_string(), + })?; + + bytes + .try_into() + .map_err(|_| EncryptError::SteVecSelectorInvalid { + selector: selector.to_string(), + }) +} + #[derive(Clone)] pub struct ZeroKms { default_keyset_id: Option, @@ -263,20 +333,39 @@ impl EncryptionService for ZeroKms { // // cipherstash-client has no `decrypt_eql_v3` counterpart to // `encrypt_eql_v3` — the v2 `decrypt_eql` only accepts `EqlCiphertext`. - // For scalar payloads that costs us nothing: `EncryptedPayloadV3.c` is - // the same `EncryptedRecord` the v2 path would have unwrapped, and - // `EncryptedRecord` is itself `Decryptable`, so we hand it straight to - // the cipher. SteVec documents are the part that genuinely needs the - // client (see `SteVecV3DecryptUnsupported`). + // We assemble the decryptable record ourselves, which is what + // protect-ffi does too (`encrypted_record_from_value`). + // + // Scalar: `c` is already the `EncryptedRecord` the v2 path would have + // unwrapped, and `EncryptedRecord` is `Decryptable`. + // + // SteVec: the document holds the key material once in the `h` header + // and each entry carries only raw AEAD bytes, so the record has to be + // reassembled from the header plus the ROOT entry (`sv[0]`, the same + // decryption-root invariant v2 had). The selector is the AEAD binding — + // its first 12 bytes are the nonce and all 16 go into the AAD — which is + // why the reassembled record is a `RecordWithNonce`. let mut indices: Vec = Vec::new(); - let mut records_to_decrypt = Vec::new(); + let mut records_to_decrypt: Vec = Vec::new(); for (idx, ct_opt) in ciphertexts.iter().enumerate() { if let Some(ct) = ct_opt { let record = match ct { - EqlCiphertextV3::Encrypted(payload) => payload.ciphertext.clone(), - EqlCiphertextV3::SteVec(_) => { - return Err(EncryptError::SteVecV3DecryptUnsupported.into()) + EqlCiphertextV3::Encrypted(payload) => { + V3Record::Scalar(payload.ciphertext.clone()) + } + EqlCiphertextV3::SteVec(document) => { + let root = document + .ste_vec + .first() + .ok_or(EncryptError::SteVecMissingRootEntry)?; + + let selector = decode_ste_vec_selector(&root.selector)?; + V3Record::SteVecRoot( + document + .key_header + .record_with_selector(root.ciphertext.clone(), selector), + ) } }; indices.push(idx); From 479744c22afdde768996cc18a83e3f8f98959c4f Mon Sep 17 00:00:00 2001 From: James Sadler Date: Mon, 20 Jul 2026 17:00:14 +1000 Subject: [PATCH 4/4] refactor(eql): adopt the cipherstash-client 0.42.0 representation Completes the v3 move. The workspace compiles again; cargo fmt and clippy are clean. The load-bearing change is in data_row.rs. EQL v2's eql_v2_encrypted was a COMPOSITE type, so reads stripped a ('...') wrapper in text and a 12-byte rowtype header in binary. EQL v3's types are DOMAINS over jsonb (95 CREATE DOMAIN, zero composites), and a domain is wire-identical to its base type -- so both of those strips were wrong. Reads now take the jsonb representation: bare JSON in text, a 1-byte version header plus JSON in binary. Also: - Encrypt now carries EqlOutput, not EqlCiphertext, through the literal and param paths: a query operand is not a ciphertext. Both v3 enums are #[serde(untagged)], so the serialized wire form is unchanged. - backend/bind/data_row imported cipherstash_client::eql::EqlCiphertext directly, bypassing the crate alias and silently keeping the v2 type. They now use crate::EqlCiphertext. - EqlCiphertext::identifier is a method in 0.42.0, not a field. - Map the new EqlError variants. UnsupportedSteVecOreInV3 gets its own customer-facing error: v3 has no oc representation, so a ste_vec column left in Standard (CLLW-ORE) mode cannot be written at all. - cipherstash-config gained IndexType::SteVec { mode }. SteVecMode::Compat (CLLW-OPE) is the default and the v3-compatible mode; Standard is documented upstream as the legacy v2 protocol. - CouldNotDecryptDataForKeyset gained a #[source]; Proxy's variant does not carry it, so the chain stops at that boundary (noted in place). Queued-Commit-Id: q-48c15nyszqvw09e8m6gydxqvfd --- packages/cipherstash-proxy/src/error.rs | 31 +++++- .../src/postgresql/backend.rs | 10 +- .../src/postgresql/context/mod.rs | 4 +- .../src/postgresql/frontend.rs | 10 +- .../src/postgresql/messages/bind.rs | 4 +- .../src/postgresql/messages/data_row.rs | 98 +++++++------------ .../src/proxy/encrypt_config/manager.rs | 5 +- .../src/proxy/zerokms/zerokms.rs | 13 ++- 8 files changed, 92 insertions(+), 83 deletions(-) diff --git a/packages/cipherstash-proxy/src/error.rs b/packages/cipherstash-proxy/src/error.rs index d5f7a9eb..81876981 100644 --- a/packages/cipherstash-proxy/src/error.rs +++ b/packages/cipherstash-proxy/src/error.rs @@ -254,6 +254,13 @@ pub enum EncryptError { #[error("InvalidIndexTerm")] InvalidIndexTerm, + /// EQL v3 orders encrypted jsonb entries by the CLLW-OPE (`op`) term and has + /// no representation for CLLW-ORE (`oc`), so a column configured for + /// Standard-mode ste_vec cannot be encrypted. The column has to be + /// reconfigured and its data re-encrypted. + #[error("An encrypted jsonb column is configured for ORE ordering, which EQL v3 does not support. For help visit {}#encrypt-ste-vec-ore-mode-unsupported", ERROR_DOC_BASE_URL)] + SteVecOreModeUnsupported, + /// `sv[0]` is the decryption root of a SteVec document, so an empty `sv` /// array leaves nothing to decrypt. #[error("Encrypted jsonb value has no root entry and cannot be decrypted")] @@ -350,10 +357,28 @@ impl From for EncryptError { cipherstash_client::eql::EqlError::ColumnConfigurationMismatch { table, column } => { Self::ColumnConfigurationMismatch { table, column } } - cipherstash_client::eql::EqlError::CouldNotDecryptDataForKeyset { keyset_id } => { - Self::CouldNotDecryptDataForKeyset { keyset_id } - } + // cipherstash-client 0.42.0 added a `#[source]` zerokms::Error here + // so callers can walk the chain to the underlying RetrieveKeyError. + // Proxy's variant carries only the keyset id, so the chain stops at + // this boundary — worth threading through if a keyset decrypt + // failure ever needs diagnosing from Proxy's logs alone. + cipherstash_client::eql::EqlError::CouldNotDecryptDataForKeyset { + keyset_id, .. + } => Self::CouldNotDecryptDataForKeyset { keyset_id }, cipherstash_client::eql::EqlError::InvalidIndexTerm => Self::InvalidIndexTerm, + + // EQL v3 orders jsonb entries by the byte-comparable CLLW-OPE `op` + // term and has no CLLW-ORE (`oc`) representation, so a SteVec column + // still configured in Standard mode cannot be written at all. + cipherstash_client::eql::EqlError::UnsupportedSteVecOreInV3 => { + Self::SteVecOreModeUnsupported + } + + cipherstash_client::eql::EqlError::UnsupportedV3QueryTerm => Self::InvalidIndexTerm, + + // Only reachable by asking the client for a v2 payload, which Proxy + // never does — v3 is the only envelope it speaks. + cipherstash_client::eql::EqlError::UnsupportedSteVecInV2 => Self::InvalidIndexTerm, cipherstash_client::eql::EqlError::MissingCiphertext(identifier) => { Self::ColumnCouldNotBeDeserialised { table: identifier.table, diff --git a/packages/cipherstash-proxy/src/postgresql/backend.rs b/packages/cipherstash-proxy/src/postgresql/backend.rs index b1a5c4b1..616f456c 100644 --- a/packages/cipherstash-proxy/src/postgresql/backend.rs +++ b/packages/cipherstash-proxy/src/postgresql/backend.rs @@ -19,8 +19,8 @@ use crate::prometheus::{ ROWS_PASSTHROUGH_TOTAL, ROWS_TOTAL, SERVER_BYTES_RECEIVED_TOTAL, }; use crate::proxy::EncryptionService; +use crate::EqlCiphertext; use bytes::BytesMut; -use cipherstash_client::eql::EqlCiphertext; use metrics::{counter, histogram}; use std::time::Instant; use tokio::io::AsyncRead; @@ -538,7 +538,7 @@ where for (col, ct) in projection_columns.iter().zip(ciphertexts) { match (col, ct) { (Some(col), Some(ct)) => { - if col.identifier != ct.identifier { + if &col.identifier != ct.identifier() { return Err(EncryptError::ColumnConfigurationMismatch { table: col.identifier.table.to_owned(), column: col.identifier.column.to_owned(), @@ -553,8 +553,8 @@ where // ciphertext with no column configuration is bad (None, Some(ct)) => { return Err(EncryptError::ColumnConfigurationMismatch { - table: ct.identifier.table.to_owned(), - column: ct.identifier.column.to_owned(), + table: ct.identifier().table.to_owned(), + column: ct.identifier().column.to_owned(), } .into()); } @@ -749,7 +749,7 @@ mod tests { _keyset_id: Option, _plaintexts: Vec>, _columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { Ok(vec![]) } diff --git a/packages/cipherstash-proxy/src/postgresql/context/mod.rs b/packages/cipherstash-proxy/src/postgresql/context/mod.rs index ab3052b5..f9cd0ebe 100644 --- a/packages/cipherstash-proxy/src/postgresql/context/mod.rs +++ b/packages/cipherstash-proxy/src/postgresql/context/mod.rs @@ -752,7 +752,7 @@ where &self, plaintexts: Vec>, columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { let keyset_id = self.keyset_identifier(); self.encryption @@ -1077,7 +1077,7 @@ mod tests { _keyset_id: Option, _plaintexts: Vec>, _columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { Ok(vec![]) } diff --git a/packages/cipherstash-proxy/src/postgresql/frontend.rs b/packages/cipherstash-proxy/src/postgresql/frontend.rs index 87199677..3fb24af1 100644 --- a/packages/cipherstash-proxy/src/postgresql/frontend.rs +++ b/packages/cipherstash-proxy/src/postgresql/frontend.rs @@ -27,7 +27,7 @@ use crate::prometheus::{ STATEMENTS_PASSTHROUGH_TOTAL, STATEMENTS_UNMAPPABLE_TOTAL, }; use crate::proxy::EncryptionService; -use crate::EqlCiphertext; +use crate::EqlOutput; use bytes::BytesMut; use cipherstash_client::encryption::Plaintext; use eql_mapper::{self, EqlMapperError, EqlTerm, TypeCheckedStatement}; @@ -582,13 +582,13 @@ where /// # Returns /// /// Vector of encrypted values corresponding to each literal, with `None` for - /// literals that don't require encryption and `Some(EqlCiphertext)` for encrypted values. + /// literals that don't require encryption and `Some(EqlOutput)` for encrypted values. async fn encrypt_literals( &mut self, session_id: SessionId, typed_statement: &TypeCheckedStatement<'_>, literal_columns: &Vec>, - ) -> Result>, Error> { + ) -> Result>, Error> { let literal_values = typed_statement.literal_values(); if literal_values.is_empty() { debug!(target: MAPPER, @@ -643,7 +643,7 @@ where async fn transform_statement( &mut self, typed_statement: &TypeCheckedStatement<'_>, - encrypted_literals: &Vec>, + encrypted_literals: &Vec>, ) -> Result, Error> { // Convert literals to ast Expr let mut encrypted_expressions = vec![]; @@ -1042,7 +1042,7 @@ where session_id: Option, bind: &Bind, statement: &Statement, - ) -> Result>, Error> { + ) -> Result>, Error> { let plaintexts = bind.to_plaintext(&statement.param_columns, &statement.postgres_param_types)?; diff --git a/packages/cipherstash-proxy/src/postgresql/messages/bind.rs b/packages/cipherstash-proxy/src/postgresql/messages/bind.rs index a8dbf734..a4701974 100644 --- a/packages/cipherstash-proxy/src/postgresql/messages/bind.rs +++ b/packages/cipherstash-proxy/src/postgresql/messages/bind.rs @@ -5,10 +5,10 @@ use crate::postgresql::context::column::Column; use crate::postgresql::data::bind_param_from_sql; use crate::postgresql::format_code::FormatCode; use crate::postgresql::protocol::BytesMutReadString; +use crate::EqlOutput; use crate::{SIZE_I16, SIZE_I32}; use bytes::{Buf, BufMut, BytesMut}; use cipherstash_client::encryption::Plaintext; -use cipherstash_client::eql::EqlCiphertext; use postgres_types::Type; use std::fmt::{self, Display, Formatter}; use std::io::Cursor; @@ -81,7 +81,7 @@ impl Bind { Ok(plaintexts) } - pub fn rewrite(&mut self, encrypted: Vec>) -> Result<(), Error> { + pub fn rewrite(&mut self, encrypted: Vec>) -> Result<(), Error> { for (idx, ct) in encrypted.iter().enumerate() { if let Some(ct) = ct { let json = serde_json::to_value(ct)?; diff --git a/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs b/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs index e512eb66..f670d569 100644 --- a/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs +++ b/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs @@ -1,14 +1,18 @@ use super::{BackendCode, NULL}; +use crate::EqlCiphertext; use crate::{ error::{EncryptError, Error, ProtocolError}, log::DECRYPT, postgresql::Column, }; use bytes::{Buf, BufMut, BytesMut}; -use cipherstash_client::eql::EqlCiphertext; use std::io::Cursor; use tracing::{debug, error}; +/// Leading byte of `jsonb`'s binary wire format. PostgreSQL has only ever +/// emitted version 1. +const JSONB_BINARY_VERSION: u8 = 1; + #[derive(Debug, Clone)] pub struct DataRow { pub columns: Vec, @@ -179,61 +183,33 @@ impl TryFrom<&mut DataColumn> for EqlCiphertext { type Error = Error; fn try_from(col: &mut DataColumn) -> Result { - if let Some(bytes) = &col.bytes { - if &bytes[0..=1] == b"(\"" { - // Text encoding - // Encrypted record is in the form ("{}") - // json data can be extracted by dropping the first and last two bytes to remove (" and ") - let start = 2; - let end = bytes.len() - 2; - let sliced = &bytes[start..end]; - - let input = String::from_utf8_lossy(sliced).to_string(); - let input = input.replace("\"\"", "\""); - - match serde_json::from_str(&input) { - Ok(e) => return Ok(e), - Err(err) => { - debug!(target: DECRYPT, error = err.to_string()); - return Err(err.into()); - } - } - } else { - // BINARY ENCODING - // 12 bytes for the binary rowtype header - // plus 1 byte for the jsonb header (value of 1) - // [Int32] Number of fields (N) - // [Int32] OID of the field’s type - // [Int32] Length of the field (in bytes), or -1 for NULL - - let start = 4 + 4; - let end = 4 + 4 + 4; - - let mut len_bytes = [0u8; 4]; // Create a fixed-size array - len_bytes.copy_from_slice(&bytes[start..end]); - - let len = i32::from_be_bytes(len_bytes); - - if len == NULL { - return Err(EncryptError::ColumnIsNull.into()); - } - - let start = 12 + 1; - let sliced = &bytes[start..]; - - match serde_json::from_slice(sliced) { - Ok(e) => { - return Ok(e); - } - Err(err) => { - debug!(target: DECRYPT, error = err.to_string()); - return Err(err.into()); - } - } - } - } + // EQL v3 column types (`eql_v3_text_eq`, `eql_v3_integer_ord`, …) are + // DOMAINS over `jsonb`, so a value arrives with jsonb's representation. + // + // EQL v2's `eql_v2_encrypted` was a composite type, which is why this + // used to strip a `("…")` wrapper in text and a 12-byte rowtype header + // in binary. Neither exists any more — a domain is wire-identical to its + // base type. + // + // text — the JSON object itself, no wrapper and no doubled quotes + // binary — a 1-byte jsonb version header followed by the JSON text + // + // The two are told apart by the leading byte: the version header is + // `0x01`, and JSON text for an EQL payload always starts with `{`. + let Some(bytes) = &col.bytes else { + return Err(EncryptError::ColumnCouldNotBeParsed.into()); + }; + + let json = match bytes.first() { + Some(&JSONB_BINARY_VERSION) => &bytes[1..], + Some(_) => &bytes[..], + None => return Err(EncryptError::ColumnCouldNotBeParsed.into()), + }; - Err(EncryptError::ColumnCouldNotBeParsed.into()) + serde_json::from_slice(json).map_err(|err| { + debug!(target: DECRYPT, error = err.to_string()); + err.into() + }) } } @@ -283,8 +259,8 @@ mod tests { assert!(encrypted[1].is_some()); assert_eq!( - column_config[1].as_ref().unwrap().identifier, - encrypted[1].as_ref().unwrap().identifier + &column_config[1].as_ref().unwrap().identifier, + encrypted[1].as_ref().unwrap().identifier() ); } @@ -332,8 +308,8 @@ mod tests { assert!(encrypted[0].is_some()); assert_eq!( - column_config[0].as_ref().unwrap().identifier, - encrypted[0].as_ref().unwrap().identifier + &column_config[0].as_ref().unwrap().identifier, + encrypted[0].as_ref().unwrap().identifier() ); } @@ -373,8 +349,8 @@ mod tests { // etc assert_eq!( - column_config[2].as_ref().unwrap().identifier, - encrypted[2].as_ref().unwrap().identifier + &column_config[2].as_ref().unwrap().identifier, + encrypted[2].as_ref().unwrap().identifier() ); } diff --git a/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs b/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs index 65dcaf0d..f9eff615 100644 --- a/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs +++ b/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs @@ -248,7 +248,9 @@ fn canonical_to_map(canonical: CanonicalEncryptionConfig) -> Result> = vec![None; plaintexts.len()]; + let mut result: Vec> = (0..plaintexts.len()).map(|_| None).collect(); for (idx, ciphertext) in indices.into_iter().zip(encrypted.into_iter()) { result[idx] = Some(ciphertext); }