Testkit tests for property encryption - #727
Conversation
…nd-trip stub test
Rename the single round-trip test; add a many-values-in-shuffled-order test that catches order-dependent fakes and type confusion, and a separate test asserting fresh-IV encryption of the same value twice yields different ciphertext.
Encrypts with one AAD then decrypts with a different one and expects a DriverError, rounding out the property-encryption error-path coverage alongside the existing round-trip tests.
New ImportEncapsulatedKey request (seeds a profile's repository with a pre-made key) and a fixedKek profile option, letting a driver decrypt values it never encrypted itself. decrypt_interop_fixtures.py holds one shared FIXTURES-style list, one entry per driver team, each produced by running generate_decrypt_interop_fixture.py against that driver's own backend; test_decrypts_values_produced_by_other_drivers decrypts every entry, proving each backend can read values encrypted by every other driver. Also covers unknown key alias/id and ambiguous-profile error paths, and a fixedKek/ImportEncapsulatedKey round-trip smoke test between two driver instances.
string_to_encrypt was hardcoded to "hello from dotnet!", which every other driver team running this same script would have printed verbatim instead of naming their own driver.
Line-length, blank-line, and import-formatting fixes; the failing testkit-style-check was cascading into every driver's composite build. Verified locally with pre-commit run.
The runner image unconditionally installed the Rust toolchain and ran cargo build --release regardless of which stub server the run actually uses, wasting disk and time on every non-rusty-stub job. This is the root cause behind several disk-space CI failures on this PR: jobs that never touch the Rust stub were still paying its full build cost. BUILD_RUST_STUB defaults to true (safe for a manual docker build with no extra args); runner.py now derives it from TEST_RUSTY_STUB using the same truthy check already used in tests/stub/shared.py. Verified locally: both branches build cleanly, the real path still produces a working boltstub binary, the skipped path installs no Rust toolchain at all.
…ation The previous entry was generated before the JS driver applied HKDF-SHA256 to the DEK (per ADR 037), so no spec-conforming driver could decrypt it. Regenerated against the fixed JS backend; both the dotnet and javascript entries now decrypt on both backends. Also document how to invoke the generator script (module invocation from the repo root, not a plain script path).
The generator used !r formatting, which fails flake8-quotes (inline-quotes=double) and E501 (unwrapped hex literals) as soon as the output is pasted. Print double-quoted strings and 32-char hex chunks, pre-indented to drop straight into DECRYPT_INTEROP_TEST_CASES verbatim. Reformat the javascript entry to match the generator output exactly (same bytes, new wrapping).
NewDriver.mockRandom lets TestKit ask the backend to replace the driver's CSPRNG seam with a mock for that driver's lifetime; EncryptToBytes.mockRandomBytes then supplies the exact bytes for one encrypt call. Verified: two encrypts of the same value with the same mock bytes produce identical ciphertext, and supplying mock bytes without mockRandom raises.
No feature flag and no NewDriver field: fixedIv pins the 12-byte IV for one encrypt call, omitting it draws real randomness. The backend raises if the IV is the wrong length or the operation doesn't consume it.
Every input is pinned - KEK, data-encryption key (via its recorded encapsulation) and per-entry IVs - so any conformant driver must encrypt these values to exactly these bytes. Entries cover the supported property types plus an AAD-bound value, initially generated with the .NET backend; cross-driver disagreement gets investigated when it appears.
The suite only ever used use_persisted_aad on values encrypted without AAD, so a driver that silently ignores the persisted AAD still passed.
When no AAD is bound, aad and its companion fields are omitted from the
metadata entirely ("The Authenticated Data, if available"); when AAD is
present, the companions use the current ADR names
aad_encoding_scheme_major/minor rather than aad_protocol_major/minor.
Renames the deterministic-testing protocol fields to their settled names, and switches iv/kek/encapsulation/encryptedBytes/encapsulatedBytes from CypherBytes envelopes to bare space-separated hex strings, matching the CypherVector precedent for protocol-level bytes that aren't themselves a Cypher property value. Response classes decode straight to bytes so callers work with real bytes objects instead of CypherBytes wrappers.
…DR PR #131) The .NET backend now writes Encrypted-structure metadata keys in ascending ordinal order per the ADR's new ordering requirement, so the byte-exact deterministic fixtures need regenerating against it. Freshly random KEK/DEK this run, so every value changed, not just the metadata ordering.
| def __init__(self, driver_id, alias, encapsulation, metadata, | ||
| profile_name=None): | ||
| self.driverId = driver_id | ||
| self.alias = alias |
There was a problem hiding this comment.
I think we should add the keyId here as well as the identifier goes into the metadata of the Encrypted Structure and it must be predictable for stub tests asserting on expected encrypted bytes. The message handler should take all of this in import the key directly into its key repository.
| self.encapsulated_bytes = bytes.fromhex(encapsulatedBytes) | ||
| self.metadata = metadata |
There was a problem hiding this comment.
encapsulatedBytes and metadata are not public API on DEK creation. I think it would be better if we did not require them here.
|
|
||
| self.assertEqual(decrypted, types.CypherString("hello world")) | ||
|
|
||
| def test_encrypts_to_known_bytes(self): |
There was a problem hiding this comment.
@RichardIrons-neo4j, I have been able to make it work with Java Driver, but only when I hardcode "0" key id on import. I think addressing the following will resolve this: https://github.com/neo4j-drivers/testkit/pull/727/changes#r3824978418
Property encryption
Feature flag:
Feature.API_PROPERTY_ENCRYPTION.There is a python script at
tests/stub/property_encryption/generate_decrypt_interop_fixture.py, which will communicate with the backend to generate the a test case for the cross-driver decryption test. If the stub tests are passing then this script will work.Protocol additions
Throughout this doc,
bytesmeans a bare space-separated lower-case hex string on the wire (e.g."0a 1b 2c"), the same conventionCypherVector'sdatafield uses — not aCypherBytesCypher-value envelope. This applies tokek,iv,encapsulation,encryptedBytes, andencapsulatedBytesbelow, since none of them are themselves a Cypher property value.valueandaadare the only fields that carry real property values, so they stay full<any Cypher value>envelopes.NewDriver
NewDrivergains one optional field:Each entry configures one property-encryption profile on the driver. The profile is of the Envelope kind, backed by a backend-owned fixture
KeyEncapsulationService/EncapsulatedKeyRepositorypair; the backend plays the role of the user, who in a real application owns and supplies these objects directly. Profile names must be unique within a driver. If the field is omitted, no encryption profiles are configured.kek, when given, constructs the profile's fixture key-encapsulation service with that exact key instead of a random one. This exists solely to support the deterministic and cross-driver decrypt tests; ordinary tests omit it.EncryptToBytes
Encrypts
value, bindingaadwhen supplied.profileNamemay be omitted only when exactly one profile is configured; see the ambiguous-profile scenario below.iv, when given, is the exact 12-byte IV the driver must use for this one encrypt call. The backend raises if the IV is not exactly 12 bytes or the operation doesn't consume it. With the data-encryption key pinned viaImportEncapsulatedKeyand the IV pinned here, an encrypt is fully deterministic, so tests can assert byte-exact ciphertext instead of only round-tripping. Likekek, this exists solely for tests that ask for determinism; ordinary tests omit it.test_encrypts_to_known_bytesuses it to check the driver's encryption output byte-for-byte against the known-answer fixtures indeterministic_fixtures.py(initially generated with the .NET backend).Decrypt
The profile is resolved from the encrypted bytes: each encrypted value records which profile produced it, so the caller does not name one.
CreateEncapsulatedKey
Generates a fresh data-encryption key through the profile's
KeyEncapsulationServiceand registers it in the profile'sEncapsulatedKeyRepositoryunderalias. A key must exist under a given alias before that alias can be used for encryption.ImportEncapsulatedKey
Seeds a profile's repository with a pre-made encapsulated key directly, skipping the key-generation step
CreateEncapsulatedKeyperforms, since the caller already has an encapsulation from elsewhere (a fixture, or a priorCreateEncapsulatedKeyresponse). This corresponds to what a real user would do to import a key obtained elsewhere: call their own repository's save method directly, without going through the driver'sPropertyEncryptionAPI at all. Unlike the other messages here,profileNameis mandatory; there is no sole-profile default.