Skip to content

fix(sdk): thread wrappingKeyAlgorithm through decrypt rewrap - #975

Open
dmihalcik-virtru wants to merge 1 commit into
mainfrom
DSPX-4221-pq-sessions
Open

fix(sdk): thread wrappingKeyAlgorithm through decrypt rewrap#975
dmihalcik-virtru wants to merge 1 commit into
mainfrom
DSPX-4221-pq-sessions

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Found while cross-SDK testing DSPX-4221 (Session Keys should support ML-KEM) against a live modified platform. decryptStreamFrom() builds its DecryptConfiguration with wrappingKeyAlgorithm (threaded in from both Client.decrypt() and the newer OpenTDF.read() CLI path), but never forwards it to the internal unwrapKey() call that actually builds the rewrap request:

const { metadata, reconstructedKey, requiredObligations } = await unwrapKey({
  fulfillableObligations: cfg.fulfillableObligations,
  manifest,
  auth: cfg.auth,
  allowedKases: allowList,
  dpopKeys: cfg.dpopKeys,
  cryptoService: cfg.cryptoService,
  // wrappingKeyAlgorithm: cfg.wrappingKeyAlgorithm,  <-- missing
});

unwrapKey()/tryKasRewrap() already correctly branch on wrappingKeyAlgorithm to generate EC/RSA/ML-KEM ephemeral session keys — the parameter was just silently dropped one call site up. Every decrypt call ignored whatever rewrapKeyType/wrappingKeyAlgorithm the caller requested and always negotiated RSA, regardless of CLI flag or API option.

How this was found

The OpenTDF platform's rewrap audit log now records eventMetaData.sessionKeyType (opentdf/platform#3814, part of this same DSPX-4221 effort). Running the xtest suite's new test_session_key_mlkem_roundtrip (opentdf/tests#571) against a real platform + this SDK's CLI showed the audit log recording "sessionKeyType":"rsa:2048" even when the test explicitly passed --rewrapKeyType mlkem:768.

The existing unit tests in lib/tests/mocha/encrypt-decrypt.spec.ts (parametrized across {encap, rewrap} algorithm pairs) didn't catch this: the mock KAS server just echoes back whatever clientPublicKey type it actually receives and validates consistently against that, so a client silently ignoring the requested algorithm and the mock server never disagree — the bug was fully masked at the unit-test layer. It only surfaced against a real server that independently records what it actually saw.

Second bug surfaced by the fix

Fixing the plumbing caused 4 previously-passing mock unit tests to start failing — all four {*, rewrap: ec:secp256r1} combinations. That's because lib/tests/server.ts's mock KAS EC-rewrap branch generates an ephemeral ECDH key pair server-side but never returned its public half (sessionPublicKey) in the response, which the client needs to complete its own ECDH derivation. This EC session-key path was never actually exercised by the unit suite before (same masking bug as above), so it was silently broken too. Fixed by exporting and returning the server's ephemeral public key.

Test plan

cd lib
npm run build
npm run lint
npx prettier --check tdf3/src/tdf.ts tests/server.ts
node dist/web/tests/server.js &   # mock KAS
npx mocha "dist/web/tests/mocha/**/*.spec.js" --timeout 60000

366 passing, 0 failing.

Confirmed against a live local platform (built from opentdf/platform#3814, with Go SDK from that same PR and Java SDK from opentdf/java-sdk#388), running opentdf/tests#571's test_session_key_mlkem_roundtrip with this branch's JS SDK across the full encrypt×decrypt matrix:

18 passed (go×go, go×java, go×js, java×go, java×java, java×js, js×go, js×java, js×js — each ×768/×1024)

All with real audit-log verification enabled (audit_logs.assert_rewrap_success(session_key_type=...)), confirming the platform genuinely negotiated the requested ML-KEM algorithm end-to-end, not just that decrypt happened to succeed.

Related work

Part of the DSPX-4221 series:

Ref: DSPX-4221

Summary by CodeRabbit

  • New Features

    • CLI version information now reports support for session-key ML-KEM.
    • Rewrap responses now include the session public key needed for client-side key derivation.
  • Bug Fixes

    • Configured wrapping-key algorithms are now honored during stream decryption and key rewrapping.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7217d487-e711-4bc7-b1a2-e5decbbdacf5

📥 Commits

Reviewing files that changed from the base of the PR and between 9a26030 and 0dbe270.

📒 Files selected for processing (3)
  • cli/src/cli.ts
  • lib/tdf3/src/tdf.ts
  • lib/tests/server.ts

📝 Walkthrough

Walkthrough

The CLI version metadata now declares session-key ML-KEM support. Decryption forwards the configured wrapping-key algorithm. The test rewrap server exports its ephemeral ECDH public key and returns it as sessionPublicKey.

Changes

Session-key support

Layer / File(s) Summary
Capability metadata
cli/src/cli.ts
Version metadata now includes supportedFeatures: ['session-key-mlkem'].
Session-key rewrap and decryption
lib/tests/server.ts, lib/tdf3/src/tdf.ts
The rewrap server exports its ephemeral ECDH public key in SPKI PEM format and returns it as sessionPublicKey. decryptStreamFrom passes cfg.wrappingKeyAlgorithm to unwrapKey.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RewrapServer
  participant decryptStreamFrom
  participant unwrapKey
  Client->>RewrapServer: Request rewrap
  RewrapServer-->>Client: Return wrapped key and sessionPublicKey
  Client->>decryptStreamFrom: Decrypt stream with configuration
  decryptStreamFrom->>unwrapKey: Pass wrappingKeyAlgorithm
  unwrapKey-->>decryptStreamFrom: Return unwrapped key
Loading

Possibly related PRs

Suggested reviewers: abarabash-virtru

Poem

A rabbit checked the feature list,
“ML-KEM support now exists!”
The key hopped through the rewrap door,
Public PEM joined the exchange once more.
Decryption chose the configured way—
Then nibbled code and praised the day.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: forwarding wrappingKeyAlgorithm through decrypt rewrap handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-4221-pq-sessions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

dmihalcik-virtru added a commit to opentdf/tests that referenced this pull request Aug 1, 2026
Add a "session-key-mlkem" feature flag and matching pytest coverage for
DSPX-4221: clients generating an ML-KEM (768/1024) ephemeral key pair as
the rewrap "session key" (the key KAS wraps the response DEK to), as
opposed to the existing mechanism-mlkem/mechanism-xwing/mechanism-secpmlkem
coverage which only exercises KAS-managed TDF wrapping keys.

- tdfs.py: new "session-key-mlkem" feature flag (detected alongside
  mechanism-mlkem, since both are gated by the platform's
  Preview.MLKEMTDFEnabled flag); SDK.decrypt() gains a
  session_key_algorithm param threaded through XT_WITH_SESSION_KEY_ALGORITHM.
- sdk/{go,java,js}/cli.sh: wire the new env var to each CLI's real flag
  (--session-key-algorithm / --rewrap-key-type / --rewrapKeyType), and add
  `supports session-key-mlkem` probes. The java probe joins wrapped help
  output before grepping: picocli line-wraps the long --rewrap-key-type
  choice list at ~80 columns, which can split "mlkem:768" across two lines
  and produce a false negative.
- test_pqc.py: new test_session_key_mlkem_roundtrip, parametrized over
  mlkem:768/1024, encrypting with a plain RSA-wrapped attribute so a
  failure can only be attributed to the session-key transport, not KAS
  mechanism support.

A successful decrypt alone does not prove ML-KEM was actually used for the
session key -- if an SDK silently ignored the requested algorithm and fell
back to RSA, the roundtrip would still pass. So the test also asserts on
KAS's rewrap audit log via a new audit_logs.assert_rewrap_success(
session_key_type=...) check:

- audit_logs.py: ParsedAuditEvent gains a session_key_type property
  (eventMetaData.sessionKeyType); matches_rewrap/assert_rewrap/
  assert_rewrap_success/assert_rewrap_failure gain a matching
  session_key_type parameter, mirroring the existing algorithm parameter.

This depends on the companion platform PR adding the sessionKeyType field
to the rewrap audit event.

Verified locally against a live platform + KAS built from the companion
PRs: all 18 encrypt x decrypt x algorithm combinations across go, java,
and js SDKs pass, including the session_key_type audit assertion. This
run also caught and led to a fix for a real web-sdk bug (opentdf/web-sdk#975)
where the requested session-key algorithm was silently dropped and every
decrypt negotiated RSA regardless of what was requested.

Ref: DSPX-4221

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
dmihalcik-virtru added a commit to opentdf/tests that referenced this pull request Aug 1, 2026
Add a "session-key-mlkem" feature flag and matching pytest coverage for
DSPX-4221: clients generating an ML-KEM (768/1024) ephemeral key pair as
the rewrap "session key" (the key KAS wraps the response DEK to), as
opposed to the existing mechanism-mlkem/mechanism-xwing/mechanism-secpmlkem
coverage which only exercises KAS-managed TDF wrapping keys.

- tdfs.py: new "session-key-mlkem" feature flag (detected alongside
  mechanism-mlkem, since both are gated by the platform's
  Preview.MLKEMTDFEnabled flag); SDK.decrypt() gains a
  session_key_algorithm param threaded through XT_WITH_SESSION_KEY_ALGORITHM.
- sdk/{go,java,js}/cli.sh: wire the new env var to each CLI's real flag
  (--session-key-algorithm / --rewrap-key-type / --rewrapKeyType), and add
  `supports session-key-mlkem` probes. The java probe joins wrapped help
  output before grepping: picocli line-wraps the long --rewrap-key-type
  choice list at ~80 columns, which can split "mlkem:768" across two lines
  and produce a false negative.
- test_pqc.py: new test_session_key_mlkem_roundtrip, parametrized over
  mlkem:768/1024, encrypting with a plain RSA-wrapped attribute so a
  failure can only be attributed to the session-key transport, not KAS
  mechanism support.

A successful decrypt alone does not prove ML-KEM was actually used for the
session key -- if an SDK silently ignored the requested algorithm and fell
back to RSA, the roundtrip would still pass. So the test also asserts on
KAS's rewrap audit log via a new audit_logs.assert_rewrap_success(
session_key_type=...) check:

- audit_logs.py: ParsedAuditEvent gains a session_key_type property
  (eventMetaData.sessionKeyType); matches_rewrap/assert_rewrap/
  assert_rewrap_success/assert_rewrap_failure gain a matching
  session_key_type parameter, mirroring the existing algorithm parameter.

This depends on the companion platform PR adding the sessionKeyType field
to the rewrap audit event.

Verified locally against a live platform + KAS built from the companion
PRs: all 18 encrypt x decrypt x algorithm combinations across go, java,
and js SDKs pass, including the session_key_type audit assertion. This
run also caught and led to a fix for a real web-sdk bug (opentdf/web-sdk#975)
where the requested session-key algorithm was silently dropped and every
decrypt negotiated RSA regardless of what was requested.

Addresses CodeRabbit review feedback:
- Make the "Not in focus" skip reason actionable (include encrypt_sdk/
  decrypt_sdk in the message).
- Use filecmp.cmp(..., shallow=False) so the final roundtrip assertion
  compares byte content, not just file stat metadata.
- Use `[[ ... ]]` instead of `[ ... ]` for the new
  XT_WITH_SESSION_KEY_ALGORITHM conditionals in go/java cli.sh
  (also flagged by SonarCloud).

Ref: DSPX-4221

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
dmihalcik-virtru added a commit to opentdf/tests that referenced this pull request Aug 1, 2026
Add a "session-key-mlkem" feature flag and matching pytest coverage for
DSPX-4221: clients generating an ML-KEM (768/1024) ephemeral key pair as
the rewrap "session key" (the key KAS wraps the response DEK to), as
opposed to the existing mechanism-mlkem/mechanism-xwing/mechanism-secpmlkem
coverage which only exercises KAS-managed TDF wrapping keys.

- tdfs.py: new "session-key-mlkem" feature flag (detected alongside
  mechanism-mlkem, since both are gated by the platform's
  Preview.MLKEMTDFEnabled flag); SDK.decrypt() gains a
  session_key_algorithm param threaded through XT_WITH_SESSION_KEY_ALGORITHM.
- sdk/{go,java,js}/cli.sh: wire the new env var to each CLI's real flag
  (--session-key-algorithm / --rewrap-key-type / --rewrapKeyType), and add
  `supports session-key-mlkem` probes. The java probe joins wrapped help
  output before grepping: picocli line-wraps the long --rewrap-key-type
  choice list at ~80 columns, which can split "mlkem:768" across two lines
  and produce a false negative.
- test_pqc.py: new test_session_key_mlkem_roundtrip, parametrized over
  mlkem:768/1024, encrypting with a plain RSA-wrapped attribute so a
  failure can only be attributed to the session-key transport, not KAS
  mechanism support.

A successful decrypt alone does not prove ML-KEM was actually used for the
session key -- if an SDK silently ignored the requested algorithm and fell
back to RSA, the roundtrip would still pass. So the test also asserts on
KAS's rewrap audit log via a new audit_logs.assert_rewrap_success(
session_key_type=...) check:

- audit_logs.py: ParsedAuditEvent gains a session_key_type property
  (eventMetaData.sessionKeyType); matches_rewrap/assert_rewrap/
  assert_rewrap_success/assert_rewrap_failure gain a matching
  session_key_type parameter, mirroring the existing algorithm parameter.

This depends on the companion platform PR adding the sessionKeyType field
to the rewrap audit event.

Verified locally against a live platform + KAS built from the companion
PRs: all 18 encrypt x decrypt x algorithm combinations across go, java,
and js SDKs pass, including the session_key_type audit assertion. This
run also caught and led to a fix for a real web-sdk bug (opentdf/web-sdk#975)
where the requested session-key algorithm was silently dropped and every
decrypt negotiated RSA regardless of what was requested.

Addresses CodeRabbit review feedback:
- Make the "Not in focus" skip reason actionable (include encrypt_sdk/
  decrypt_sdk in the message).
- Use filecmp.cmp(..., shallow=False) so the final roundtrip assertion
  compares byte content, not just file stat metadata.
- Use `[[ ... ]]` instead of `[ ... ]` for the new
  XT_WITH_SESSION_KEY_ALGORITHM conditionals in go/java cli.sh
  (also flagged by SonarCloud).

Also completes spec/DSPX-4221.md (was a template scaffold).

Ref: DSPX-4221

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
dmihalcik-virtru added a commit to opentdf/tests that referenced this pull request Aug 1, 2026
Add a "session-key-mlkem" feature flag and matching pytest coverage for
DSPX-4221: clients generating an ML-KEM (768/1024) ephemeral key pair as
the rewrap "session key" (the key KAS wraps the response DEK to), as
opposed to the existing mechanism-mlkem/mechanism-xwing/mechanism-secpmlkem
coverage which only exercises KAS-managed TDF wrapping keys.

- tdfs.py: new "session-key-mlkem" feature flag (detected alongside
  mechanism-mlkem, since both are gated by the platform's
  Preview.MLKEMTDFEnabled flag); SDK.decrypt() gains a
  session_key_algorithm param threaded through XT_WITH_SESSION_KEY_ALGORITHM.
- sdk/{go,java,js}/cli.sh: wire the new env var to each CLI's real flag
  (--session-key-algorithm / --rewrap-key-type / --rewrapKeyType), and add
  `supports session-key-mlkem` probes.
- test_pqc.py: new test_session_key_mlkem_roundtrip, parametrized over
  mlkem:768/1024, encrypting with a plain RSA-wrapped attribute so a
  failure can only be attributed to the session-key transport, not KAS
  mechanism support.

A successful decrypt alone does not prove ML-KEM was actually used for the
session key -- if an SDK silently ignored the requested algorithm and fell
back to RSA, the roundtrip would still pass. So the test also asserts on
KAS's rewrap audit log via a new audit_logs.assert_rewrap_success(
session_key_type=...) check:

- audit_logs.py: ParsedAuditEvent gains a session_key_type property
  (eventMetaData.sessionKeyType); matches_rewrap/assert_rewrap/
  assert_rewrap_success/assert_rewrap_failure gain a matching
  session_key_type parameter, mirroring the existing algorithm parameter.

This depends on the companion platform PR adding the sessionKeyType field
to the rewrap audit event.

Verified locally against a live platform + KAS built from the companion
PRs: all 18 encrypt x decrypt x algorithm combinations across go, java,
and js SDKs pass, including the session_key_type audit assertion. This
run also caught and led to a fix for a real web-sdk bug (opentdf/web-sdk#975)
where the requested session-key algorithm was silently dropped and every
decrypt negotiated RSA regardless of what was requested.

CI on the companion-branch-less baseline matrix (go/java/js @main, unpatched
platform) then caught a second, related bug: the java and js "session-key-mlkem"
probes grepped --help output for "mlkem:768", which both CLIs already listed
as a valid flag *value* well before their rewrap code actually implemented
it (the enum/choices list is shared with the pre-existing KAS-managed-key
mechanism). That gave a false positive on unpatched builds, so the test ran
instead of skipping and failed on the audit assertion (decrypt silently
succeeded via RSA fallback instead of ML-KEM). Fixed by switching both
probes to each CLI's own hardcoded, source-controlled feature-support
signal instead of scraping --help text:
- Java: `cmdline.jar supports session-key-mlkem` (existing `supports`
  subcommand, extended with this feature).
- JS: `--version`'s new `supportedFeatures` array (new, mirroring the
  same idea).
Go was already accurate here since its --session-key-algorithm choices are
literal switch cases I added directly, not inherited from a pre-existing enum.

Addresses CodeRabbit review feedback:
- Make the "Not in focus" skip reason actionable (include encrypt_sdk/
  decrypt_sdk in the message).
- Use filecmp.cmp(..., shallow=False) so the final roundtrip assertion
  compares byte content, not just file stat metadata.
- Use `[[ ... ]]` instead of `[ ... ]` for the new
  XT_WITH_SESSION_KEY_ALGORITHM conditionals in go/java cli.sh
  (also flagged by SonarCloud).
- The "Major: make ML-KEM capability detection algorithm-specific" comment
  was auto-marked resolved without an actual fix; the CI failure above
  confirms it was a real issue for the java/js SDK-side probes specifically,
  now fixed as described. The platform-side detection still coalesces
  mlkem:768/1024 and reuses the pre-existing mechanism-mlkem preview flag;
  documented as a known, currently-harmless imprecision in tdfs.py rather
  than adding a new well-known-config field for it in this pass.

Also completes spec/DSPX-4221.md (was a template scaffold).

Ref: DSPX-4221

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes decrypt rewrap behavior in the SDK by correctly propagating wrappingKeyAlgorithm into the internal rewrap/unwrap flow, ensuring the requested session-key algorithm (RSA/EC/ML-KEM) is actually used during decrypt. It also updates the mock KAS implementation to support EC rewrap flows by returning the server’s ephemeral EC public key, and exposes ML-KEM session-key support in the CLI version output.

Changes:

  • Forward wrappingKeyAlgorithm from decryptStreamFrom() into the internal unwrapKey() call so rewrap uses the requested algorithm.
  • Fix mock KAS EC rewrap responses to include sessionPublicKey so clients can complete ECDH derivation.
  • Add a supportedFeatures field to the CLI --version JSON output indicating ML-KEM session-key support.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
lib/tests/server.ts Returns KAS ephemeral EC public key for EC rewrap responses in the mock KAS server.
lib/tdf3/src/tdf.ts Threads wrappingKeyAlgorithm into unwrapKey() so decrypt rewrap negotiates the requested algorithm.
cli/src/cli.ts Adds supportedFeatures to CLI version JSON to reflect implemented ML-KEM session-key support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/tests/server.ts
dmihalcik-virtru added a commit to opentdf/tests that referenced this pull request Aug 1, 2026
Add a "session-key-mlkem" feature flag and matching pytest coverage for
DSPX-4221: clients generating an ML-KEM (768/1024) ephemeral key pair as
the rewrap "session key" (the key KAS wraps the response DEK to), as
opposed to the existing mechanism-mlkem/mechanism-xwing/mechanism-secpmlkem
coverage which only exercises KAS-managed TDF wrapping keys.

- tdfs.py: new "session-key-mlkem" feature flag (detected alongside
  mechanism-mlkem, since both are gated by the platform's
  Preview.MLKEMTDFEnabled flag); SDK.decrypt() gains a
  session_key_algorithm param threaded through XT_WITH_SESSION_KEY_ALGORITHM.
- sdk/{go,java,js}/cli.sh: wire the new env var to each CLI's real flag
  (--session-key-algorithm / --rewrap-key-type / --rewrapKeyType), and add
  `supports session-key-mlkem` probes.
- test_pqc.py: new test_session_key_mlkem_roundtrip, parametrized over
  mlkem:768/1024, encrypting with a plain RSA-wrapped attribute so a
  failure can only be attributed to the session-key transport, not KAS
  mechanism support.

A successful decrypt alone does not prove ML-KEM was actually used for the
session key -- if an SDK silently ignored the requested algorithm and fell
back to RSA, the roundtrip would still pass. So the test also asserts on
KAS's rewrap audit log via a new audit_logs.assert_rewrap_success(
session_key_type=...) check:

- audit_logs.py: ParsedAuditEvent gains a session_key_type property
  (eventMetaData.sessionKeyType); matches_rewrap/assert_rewrap/
  assert_rewrap_success/assert_rewrap_failure gain a matching
  session_key_type parameter, mirroring the existing algorithm parameter.

This depends on the companion platform PR adding the sessionKeyType field
to the rewrap audit event.

The new audit assertion only covered ML-KEM. Rather than sprinkling
session_key_type onto every existing rewrap assertion (which would make
otherwise-unrelated tests brittle to a future default-algorithm change),
extended coverage in exactly two places where the algorithm actually is
explicit:
- test_tdfs.py::test_tdf_roundtrip: the existing ecwrap block already
  requests EC explicitly (ecwrap=True), so pinned
  session_key_type="ec:secp256r1" on its existing audit assertion.
- test_pqc.py: new test_session_key_rsa_roundtrip, requesting "rsa:2048"
  explicitly (mirroring the ML-KEM test's shape) rather than relying on
  it being the implicit default.

Verified locally against a live platform + KAS built from the companion
PRs: all 18 encrypt x decrypt x algorithm combinations across go, java,
and js SDKs pass, including the session_key_type audit assertion. This
run also caught and led to a fix for a real web-sdk bug (opentdf/web-sdk#975)
where the requested session-key algorithm was silently dropped and every
decrypt negotiated RSA regardless of what was requested.

CI on the companion-branch-less baseline matrix (go/java/js @main, unpatched
platform) then caught a second, related bug: the java and js "session-key-mlkem"
probes grepped --help output for "mlkem:768", which both CLIs already listed
as a valid flag *value* well before their rewrap code actually implemented
it (the enum/choices list is shared with the pre-existing KAS-managed-key
mechanism). That gave a false positive on unpatched builds, so the test ran
instead of skipping and failed on the audit assertion (decrypt silently
succeeded via RSA fallback instead of ML-KEM). Fixed by switching both
probes to each CLI's own hardcoded, source-controlled feature-support
signal instead of scraping --help text:
- Java: `cmdline.jar supports session-key-mlkem` (existing `supports`
  subcommand, extended with this feature).
- JS: `--version`'s new `supportedFeatures` array (new, mirroring the
  same idea).
Go was already accurate here since its --session-key-algorithm choices are
literal switch cases I added directly, not inherited from a pre-existing enum.

Addresses CodeRabbit review feedback:
- Make the "Not in focus" skip reason actionable (include encrypt_sdk/
  decrypt_sdk in the message).
- Use filecmp.cmp(..., shallow=False) so the final roundtrip assertion
  compares byte content, not just file stat metadata.
- Use `[[ ... ]]` instead of `[ ... ]` for the new
  XT_WITH_SESSION_KEY_ALGORITHM conditionals in go/java cli.sh
  (also flagged by SonarCloud).
- The "Major: make ML-KEM capability detection algorithm-specific" comment
  was auto-marked resolved without an actual fix; the CI failure above
  confirms it was a real issue for the java/js SDK-side probes specifically,
  now fixed as described. The platform-side detection still coalesces
  mlkem:768/1024 and reuses the pre-existing mechanism-mlkem preview flag;
  documented as a known, currently-harmless imprecision in tdfs.py rather
  than adding a new well-known-config field for it in this pass.

Also completes spec/DSPX-4221.md (was a template scaffold).

Ref: DSPX-4221

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
decryptStreamFrom() built its DecryptConfiguration with wrappingKeyAlgorithm
(threaded in from both Client.decrypt() and the newer OpenTDF.read() CLI
path), but never forwarded it to the internal unwrapKey() call that actually
builds the rewrap request. unwrapKey()/tryKasRewrap() already correctly
branch on wrappingKeyAlgorithm to generate EC/RSA/ML-KEM ephemeral session
keys -- the parameter was just silently dropped one call site up, so every
decrypt used to ignore whatever rewrapKeyType/wrappingKeyAlgorithm the
caller requested and always negotiated RSA.

Found via DSPX-4221 cross-SDK testing: the OpenTDF platform's new rewrap
audit log (eventMetaData.sessionKeyType) showed "rsa:2048" even when the
xtest suite explicitly requested "mlkem:768"/"mlkem:1024" via --rewrapKeyType.
The existing unit tests in encrypt-decrypt.spec.ts didn't catch this because
the mock KAS server just echoes back whatever clientPublicKey type it
receives, so a client that silently ignores the requested algorithm and the
mock server still agree -- masking the bug.

Also fixes the mock KAS server's EC rewrap response, which never returned
kas's ephemeral sessionPublicKey needed for the client's ECDH derivation.
This path was never actually exercised by encrypt-decrypt.spec.ts before
(same masking bug), so it was silently broken; fixing the wrappingKeyAlgorithm
plumbing surfaced it immediately as 4 new test failures.

Adds a hardcoded, source-controlled `supportedFeatures` array to --version
output, listing "session-key-mlkem". Needed because --rewrapKeyType's
--help choices already listed mlkem:768/mlkem:1024 before this fix landed
(the choices list is shared with --encapKeyType's pre-existing KAS-managed-
key mechanism support), so xtest's CLI capability probe was grepping
--help text and getting a false positive on unpatched builds -- confirmed
by a real CI failure on the companion tests PR's baseline (unpatched)
xct matrix: the test ran instead of skipping and failed on the rewrap
audit assertion (decrypt silently succeeded via RSA fallback, not ML-KEM).

Fix a related bug the review surfaced in the mock server's EC path: the
ECDH session key pair was generated non-extractable, but exporting its
public key (added above, to return sessionPublicKey) requires it to be
extractable -- would have thrown InvalidAccessError in spec-compliant
WebCrypto implementations the moment the EC-rewrap branch actually ran.
Verified fixed: all 24 encrypt-decrypt.spec.ts cases pass, including the
rewrap: ec:secp256r1 combinations that exercise this exportKey call.

Ref: DSPX-4221

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@sonarqubecloud

sonarqubecloud Bot commented Aug 1, 2026

Copy link
Copy Markdown

@dmihalcik-virtru
dmihalcik-virtru marked this pull request as ready for review August 1, 2026 15:12
@dmihalcik-virtru
dmihalcik-virtru requested a review from a team as a code owner August 1, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants