Skip to content

Commit 940ee8b

Browse files
committed
test(property): cross-ciphertext equality over per-type doubles fixtures (hm + ORE)
1 parent c1ec065 commit 940ee8b

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! fixture-suite (CIP-3141) cross-ciphertext equality test.
2+
//!
3+
//! Proves "two independent encryptions of one value compare equal" using the
4+
//! committed `fixtures.eql_v2_<T>_doubles` tables — each plaintext encrypted
5+
//! twice, so the table carries equal-plaintext / distinct-ciphertext rows. No
6+
//! fresh encryption, no creds: it reads the already-encrypted doubles, so it
7+
//! runs in the credential-free `mise run test:sqlx` path. Distinct from the
8+
//! matrix (which reads the curated `fixtures.eql_v2_<T>`) and from the e2e suite
9+
//! (which re-encrypts fresh duplicates each run).
10+
//!
11+
//! Each type asserts, on its doubles rows:
12+
//! 1. a distinct-ciphertext pair exists (an equal-plaintext pair whose
13+
//! `payload_json` differs) — so the equality assertions below are non-trivial;
14+
//! 2. `=` TRUE / `<>` FALSE across every pair through the `_eq` (hm/HMAC) domain
15+
//! (`assert_eq_oracle`);
16+
//! 3. the ordering operators agree with the plaintext oracle on both ordered
17+
//! twins (`assert_ord_oracle`), PLUS `=` TRUE / `<>` FALSE on an equal pair
18+
//! through `_ord` and `_ord_ore` — the ORE (`ob`) equality path, which routes
19+
//! `=` through `compare_ore_block_256_terms(...) = 0` (GUARANTEED equal for
20+
//! two independent encryptions of one value; see the ORE finding in the plan).
21+
//!
22+
//! `#[sqlx::test]` per type (its own migrated scratch DB), like the rest of the
23+
//! fixture suite.
24+
25+
use super::fixture_oracle::load_doubles_rows;
26+
use anyhow::Result;
27+
use eql_tests::property::{assert_eq_oracle, assert_ord_oracle, Row};
28+
use eql_tests::scalar_domains::{ScalarDomainSpec, ScalarType, Variant};
29+
use sqlx::PgPool;
30+
31+
/// Find two rows with equal plaintext but DIFFERENT ciphertext, or fail. The
32+
/// doubles fixture encrypts each plaintext independently, so an equal-plaintext
33+
/// pair is expected to differ in ciphertext; a failure here means the fixture
34+
/// was not regenerated.
35+
fn first_distinct_ciphertext_pair<T: ScalarType>(rows: &[Row<T>]) -> Result<(&Row<T>, &Row<T>)> {
36+
for i in 0..rows.len() {
37+
for j in (i + 1)..rows.len() {
38+
if rows[i].plaintext == rows[j].plaintext
39+
&& rows[i].payload_json != rows[j].payload_json
40+
{
41+
return Ok((&rows[i], &rows[j]));
42+
}
43+
}
44+
}
45+
anyhow::bail!(
46+
"doubles fixture for {} has no equal-plaintext/distinct-ciphertext pair; \
47+
regenerate via mise run test:sqlx:prep",
48+
T::PG_TYPE
49+
)
50+
}
51+
52+
/// Assert `=` TRUE / `<>` FALSE for one equal-plaintext distinct-ciphertext pair
53+
/// on `variant`'s domain. Used for the ORE path (`Ord` / `OrdOre`), which routes
54+
/// `=` through `compare_ore_block_256_terms(...) = 0` — the assertion the
55+
/// plaintext ordering oracle does not itself make on the ordered twins.
56+
async fn assert_pair_eq_on<T: ScalarType>(
57+
pool: &PgPool,
58+
variant: Variant,
59+
a: &Row<T>,
60+
b: &Row<T>,
61+
) -> Result<()> {
62+
let domain = ScalarDomainSpec::new::<T>(variant).sql_domain;
63+
// `'<json>'::jsonb::<domain>` for each side; escape single quotes the same
64+
// way property.rs's `cast` does.
65+
let a_cast = format!("'{}'::jsonb::{domain}", a.payload_json.replace('\'', "''"));
66+
let b_cast = format!("'{}'::jsonb::{domain}", b.payload_json.replace('\'', "''"));
67+
let sql = format!("SELECT ({a_cast}) = ({b_cast}), ({a_cast}) <> ({b_cast})");
68+
let (eq, neq): (Option<bool>, Option<bool>) = sqlx::query_as(&sql).fetch_one(pool).await?;
69+
anyhow::ensure!(
70+
eq == Some(true),
71+
"cross-ciphertext `=` on {domain} must be TRUE for equal plaintext, got {eq:?}"
72+
);
73+
anyhow::ensure!(
74+
neq == Some(false),
75+
"cross-ciphertext `<>` on {domain} must be FALSE for equal plaintext, got {neq:?}"
76+
);
77+
Ok(())
78+
}
79+
80+
/// The full cross-ciphertext check for an ordered scalar `T`.
81+
async fn assert_cross_ciphertext<T: ScalarType>(pool: &PgPool) -> Result<()> {
82+
let rows = load_doubles_rows::<T>(pool).await?;
83+
84+
// (1) the doubles really are distinct ciphertext.
85+
let (a, b) = first_distinct_ciphertext_pair::<T>(&rows)?;
86+
87+
// (2) hm/HMAC equality path across all pairs.
88+
assert_eq_oracle::<T>(pool, &rows).await?;
89+
90+
// (3) ordering oracle on both ordered twins, plus the explicit ORE-path
91+
// equality on the distinct-ciphertext pair.
92+
assert_ord_oracle::<T>(pool, Variant::Ord, &rows).await?;
93+
assert_ord_oracle::<T>(pool, Variant::OrdOre, &rows).await?;
94+
assert_pair_eq_on::<T>(pool, Variant::Ord, a, b).await?;
95+
assert_pair_eq_on::<T>(pool, Variant::OrdOre, a, b).await?;
96+
Ok(())
97+
}
98+
99+
macro_rules! cross_ciphertext_test {
100+
($name:ident, $ty:ty) => {
101+
#[sqlx::test]
102+
async fn $name(pool: PgPool) -> Result<()> {
103+
assert_cross_ciphertext::<$ty>(&pool).await
104+
}
105+
};
106+
}
107+
108+
cross_ciphertext_test!(cross_ciphertext_int2, i16);
109+
cross_ciphertext_test!(cross_ciphertext_int4, i32);
110+
cross_ciphertext_test!(cross_ciphertext_int8, i64);
111+
cross_ciphertext_test!(cross_ciphertext_date, chrono::NaiveDate);
112+
cross_ciphertext_test!(cross_ciphertext_timestamptz, chrono::DateTime<chrono::Utc>);
113+
cross_ciphertext_test!(cross_ciphertext_numeric, rust_decimal::Decimal);
114+
cross_ciphertext_test!(cross_ciphertext_text, String);

tests/sqlx/tests/encrypted_domain/property/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ mod edge_cases;
2727
mod fixture_oracle;
2828
// fixture suite: example-based bloom match smoke over the text `_match` fixtures.
2929
mod match_smoke;
30+
// fixture suite: cross-ciphertext equality over the per-type doubles fixtures
31+
// (each plaintext encrypted twice) — proves two independent encryptions of one
32+
// value compare equal through both the hm (`_eq`) and ORE (`_ord`/`_ord_ore`)
33+
// paths.
34+
mod cross_ciphertext;
3035
// e2e suite: oracle over freshly generated + batch-encrypted values.
3136
#[cfg(feature = "proptest-e2e")]
3237
mod e2e_oracle;

0 commit comments

Comments
 (0)