From 6e4cf33690471d27cf55ac43fa6b5f0799415c9a Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 8 Jul 2026 17:31:43 -0400 Subject: [PATCH] chore(xtest): cross-SDK test for duplicate KAO on same KAS+split (DSPX-3379) Adds test_tdf_with_duplicate_kao_same_kas + duplicate_first_kao manifest mutation. Gated behind a new 'multikao' feature so SDK versions without the fix are skipped instead of failing the workflow: go/java always support it; web-sdk (js) reports support at >= 0.20.0 (where DSPX-3379 ships). Signed-off-by: Dave Mihalcik --- xtest/sdk/js/cli.sh | 7 ++++++ xtest/tdfs.py | 8 +++++++ xtest/test_tdfs.py | 56 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/xtest/sdk/js/cli.sh b/xtest/sdk/js/cli.sh index b5d8bbef..ee5fa8fb 100755 --- a/xtest/sdk/js/cli.sh +++ b/xtest/sdk/js/cli.sh @@ -96,6 +96,13 @@ if [ "$1" == "supports" ]; then npx $CTL encrypt --help | grep -i 'mlkem:768' exit $? ;; + multikao) + # Tolerate a KAO array where the same KAS wraps the same split more than + # once (DSPX-3379). Shipped in web-sdk >= 0.20.0. + set -o pipefail + npx $CTL --version | jq -re '.["@opentdf/sdk"]' | awk -F. '{ if ($1 > 0 || ($1 == 0 && $2 >= 20)) exit 0; else exit 1; }' + exit $? + ;; mechanism-xwing) set -o pipefail npx $CTL help | grep -i xwing diff --git a/xtest/tdfs.py b/xtest/tdfs.py index d09ca470..ab3be057 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -149,6 +149,10 @@ def is_sdk_type(val: str) -> TypeIs[sdk_type]: "mechanism-secpmlkem", # Support for pure (non-hybrid) ML-KEM key wrapping: mlkem:768 and mlkem:1024. "mechanism-mlkem", + # Decrypt a KAO array that has two entries with the same split id and KAS + # URI (the same KAS wrapping the same split more than once). go/java have + # always tolerated this; web-sdk gained it in 0.20.0 (DSPX-3379). + "multikao", "ns_grants", "obligations", ] @@ -625,6 +629,10 @@ def _uncached_supports(self, feature: feature_type) -> bool: return False case ("autoconfigure", ("go" | "java")): return True + case ("multikao", ("go" | "java")): + # go/java reconstruct by skipping already-satisfied splits, so a + # duplicate KAS on the same split has always decrypted. + return True case ("better-messages-2024", ("js" | "java")): return True case ("ns_grants", ("go" | "java")): diff --git a/xtest/test_tdfs.py b/xtest/test_tdfs.py index 1b528ab5..ab41ee18 100644 --- a/xtest/test_tdfs.py +++ b/xtest/test_tdfs.py @@ -473,6 +473,16 @@ def malicious_kao(manifest: tdfs.Manifest) -> tdfs.Manifest: return manifest +def duplicate_first_kao(manifest: tdfs.Manifest) -> tdfs.Manifest: + # DSPX-3379: append a byte-identical copy of the first KAO so the same KAS + # holds two copies of the same split (same sid + url + wrappedKey). This is + # a valid, decryptable duplicate (either copy unwraps the split). + assert manifest.encryptionInformation.keyAccess + first = manifest.encryptionInformation.keyAccess[0] + manifest.encryptionInformation.keyAccess.append(first.model_copy(deep=True)) + return manifest + + ### TAMPER TESTS @@ -831,3 +841,49 @@ def test_tdf_with_malicious_kao( # Note: We don't assert on audit logs here because the SDK should reject # the malicious KAO client-side before making a rewrap request to the KAS + + +def test_tdf_with_duplicate_kao_same_kas( + encrypt_sdk: tdfs.SDK, + decrypt_sdk: tdfs.SDK, + pt_file: Path, + in_focus: set[tdfs.SDK], + attribute_default_rsa: Attribute, + encrypted_tdf: EncryptFactory, +) -> None: + """DSPX-3379: when a KAO array has two elements with the same split id and + the same KAS URI, the file must still decrypt (the same KAS is allowed to + wrap the same split more than once). It should behave exactly as it would if + the copies lived on distinct URIs. + """ + if not in_focus & {encrypt_sdk, decrypt_sdk}: + pytest.skip("Not in focus") + pfs = tdfs.get_platform_features() + tdfs.skip_connectrpc_skew(encrypt_sdk, decrypt_sdk, pfs) + tdfs.skip_hexless_skew(encrypt_sdk, decrypt_sdk) + # Only the decrypt side needs to tolerate the duplicate KAO; the encrypt + # side just produces a normal TDF that we mutate below. + if not decrypt_sdk.supports("multikao"): + pytest.skip( + f"{decrypt_sdk} sdk doesn't support duplicate KAOs on the same KAS+split" + ) + ct_file = encrypted_tdf( + encrypt_sdk, + target_mode=tdfs.select_target_version(encrypt_sdk, decrypt_sdk), + attr_values=attribute_default_rsa.value_fqns, + ) + original = tdfs.manifest(ct_file).encryptionInformation.keyAccess + assert original, "expected at least one KAO to duplicate" + + b_file = tdfs.update_manifest( + "duplicate_kao_same_kas", ct_file, duplicate_first_kao + ) + # Confirm the mutation produced a genuine same-(sid, url) duplicate. + kaos = tdfs.manifest(b_file).encryptionInformation.keyAccess + assert len(kaos) == len(original) + 1 + assert kaos[0].url == kaos[-1].url + assert kaos[0].sid == kaos[-1].sid + + rt_file = encrypted_tdf.rt_file(b_file, decrypt_sdk) + decrypt_sdk.decrypt(b_file, rt_file, "ztdf") + assert filecmp.cmp(pt_file, rt_file)