Skip to content

feat(sdk,cmdline): settable segment and root integrity algorithms (DSPX-4736) - #400

Closed
dmihalcik-virtru wants to merge 1 commit into
mainfrom
DSPX-4736-integrity-algorithm-controls
Closed

dmihalcik-virtru wants to merge 1 commit into
mainfrom
DSPX-4736-integrity-algorithm-controls

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary

Adds explicit controls for the two integrity algorithms a ZTDF writer picks, and makes an unsupported choice fail loudly instead of being silently accepted.

Rationale

The two algorithms are not interchangeable, and the API previously did not say so:

allowed why
segment HS256, GMAC a segment hash is computed over ciphertext AES-GCM actually produced, so "GMAC" means reading back a real authentication tag
root HS256 only the root signature covers the aggregate of the segment hashes, which never passes through AES-GCM — there is no tag to recover, so GMAC would authenticate nothing

sdk

  • Config.withSegmentIntegrityAlgorithm(HS256|GMAC)
  • Config.withRootIntegrityAlgorithm(HS256) — throws IllegalArgumentException for anything else. The option exists so callers can state the choice explicitly and a CLI can surface the refusal, not to widen it. It is self-contained and does not reach into TDF.

cmdline

  • encrypt --root-integrity-algorithm and --segment-integrity-algorithm, with a case-insensitive ITypeConverter so the flags behave like the equivalents in the other OpenTDF CLIs, and ${COMPLETION-CANDIDATES} so the accepted values render in help.
  • The root algorithm is validated before buildSDK(), so an unsupported value is a picocli usage error (exit 2) rather than a failure after a platform round trip.
  • encryptHelp_advertisesIntegrityAlgorithmFlags pins the literal flag names into the rendered encrypt help, because the cross-SDK xtest feature detectors find them by grepping it. Merging this flips the integrity_algs / gmac_root_rejected gates on and stops those cells silently skipping.

Scope

This is the control surface for evaluating DSPX-4703, deliberately separated from the fix. It changes what a writer may choose; it does not change how a manifest that already declares a GMAC root is verified on read. That is the stacked follow-up.

Splitting it this way means the xtest exploit cases can run against this branch for a live "before" baseline, then against the fix branch to watch them go red → green.

Public API notes

Two new static factory methods on Config. Nothing existing changes shape or default behaviour: the segment default stays GMAC, the root default stays HS256.

Test plan

mvn -q compiler:compile compiler:testCompile -pl sdk,cmdline
mvn -q surefire:test -pl cmdline -Dtest=CommandTest

CommandTest: 26 tests, 0 failures — run with this commit as HEAD and the DSPX-4703 fix commit entirely absent, to confirm the branch stands alone.

Coverage added: GMAC rejected for root in three casings, HS256 accepted in two casings (and reaching the later missing-credentials error, proving it was not rejected), both algorithms accepted for segment, an unknown algorithm rejected, converter case-insensitivity, and the help-rendering assertion.

Summary by CodeRabbit

  • New Features

    • Added command-line options to configure root and segment integrity algorithms during encryption.
    • Integrity algorithm values now accept mixed casing and surrounding whitespace.
    • Segment integrity supports HS256 and GMAC, while root integrity supports HS256.
  • Bug Fixes

    • Invalid or unknown integrity algorithm values are rejected with clear command-line errors.
    • Encryption configuration validation now occurs before processing begins.

Cross-SDK coverage lives in opentdf/tests#594.

@dmihalcik-virtru
dmihalcik-virtru requested review from a team as code owners September 10, 2026 16:08
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 481d493a-dc66-49a3-bfd0-8cab3cc2bb1a

📥 Commits

Reviewing files that changed from the base of the PR and between c51dab1 and 37d2f45.

📒 Files selected for processing (4)
  • cmdline/src/main/java/io/opentdf/platform/Command.java
  • cmdline/src/test/java/io/opentdf/platform/CommandTest.java
  • sdk/src/main/java/io/opentdf/platform/sdk/Config.java
  • sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The SDK adds root and segment integrity algorithm configuration. The encrypt command adds case-insensitive algorithm options, validates root algorithm support, and tests accepted and rejected values.

Changes

Integrity algorithm configuration

Layer / File(s) Summary
SDK integrity configuration
sdk/src/main/java/io/opentdf/platform/sdk/Config.java, sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java
Config adds configurators for segment and root integrity algorithms. Root configuration accepts only HS256. Tests cover configuration behavior and rejection of GMAC.
Encrypt command parsing and validation
cmdline/src/main/java/io/opentdf/platform/Command.java, cmdline/src/test/java/io/opentdf/platform/CommandTest.java
The encrypt command accepts both algorithm options through a case-insensitive converter. Tests cover help output, valid values, unsupported root values, unknown values, and casing.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EncryptCommand
  participant IntegrityAlgorithmConverter
  participant Config
  participant TDFConfig
  EncryptCommand->>IntegrityAlgorithmConverter: Convert option value
  IntegrityAlgorithmConverter-->>EncryptCommand: Return IntegrityAlgorithm
  EncryptCommand->>Config: Build integrity configuration
  Config-->>EncryptCommand: Return TDFConfig option
  EncryptCommand->>TDFConfig: Apply configuration before SDK creation
Loading

Suggested reviewers: mkleene

Merge Risk: ⚪ Minimal · up to 37d2f

The new integrity-algorithm controls validate and apply supported values consistently, with no unresolved merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding configurable segment and root integrity algorithms in the SDK and command line interface.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-4736-integrity-algorithm-controls

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

A rabbit trims the algorithm's name
GMAC and HS256 hop into the frame
Root paths guard their chosen key
Segment paths leap happily
Tests tap softly: values are right
The encrypt command shines bright

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

@github-actions

Copy link
Copy Markdown
Contributor

@dmihalcik-virtru
dmihalcik-virtru added this pull request to stack #402 September 10, 2026 19:32
…PX-4736)

Adds explicit controls for the two integrity algorithms a ZTDF writer picks,
and makes an unsupported choice fail loudly instead of being silently accepted.

The two are not interchangeable:
  - A segment hash is computed over ciphertext AES-GCM actually produced, so
    "GMAC" there means reading back a real authentication tag. Both HS256 and
    GMAC are valid.
  - The root signature covers the aggregate of the segment hashes, which never
    passes through AES-GCM. There is no tag to recover, so HS256 is the only
    meaningful choice.

sdk:
  - Config.withSegmentIntegrityAlgorithm(HS256|GMAC)
  - Config.withRootIntegrityAlgorithm(HS256), which throws
    IllegalArgumentException for anything else. The option exists so callers
    can state the choice explicitly and a CLI can surface the refusal, not to
    widen it.

cmdline:
  - `encrypt --root-integrity-algorithm` and `--segment-integrity-algorithm`,
    with a case-insensitive converter so the flags behave like the equivalents
    in the other OpenTDF CLIs, and ${COMPLETION-CANDIDATES} in the description
    so the accepted values appear in help.
  - The root algorithm is validated before buildSDK(), so an unsupported value
    is reported as a picocli usage error (exit 2) rather than after a platform
    round trip.
  - A test pins the literal flag names into the rendered encrypt help, because
    the cross-SDK xtest feature detectors find them by grepping it.

This is the control surface for evaluating DSPX-4703. It does not itself change
how a manifest that already declares a GMAC root is verified on read.

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

chore: reduce verbosity of documentation
@dmihalcik-virtru
dmihalcik-virtru force-pushed the DSPX-4736-integrity-algorithm-controls branch from c51dab1 to 37d2f45 Compare September 11, 2026 03:01
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

@dmihalcik-virtru
dmihalcik-virtru removed this pull request from stack #402 September 11, 2026 19:19
dmihalcik-virtru added a commit that referenced this pull request Sep 14, 2026
## Summary

### The bug

A ZTDF's root signature is the only thing that authenticates the
manifest's **ordered list** of segment hashes. AES-GCM tags bind a
segment's own bytes and say nothing about its index, its neighbours, or
how many segments exist — so segment-level integrity structurally cannot
notice a truncated, reordered, or duplicated segment list.

One signature routine served both jobs. For a **segment**, "GMAC"
correctly means reading back the AES-GCM tag the cipher just computed
over that segment's ciphertext. For the **root** it means nothing: the
aggregate hash never passes through AES-GCM, so there is no tag to
recover, and the code returned a copy of the trailing bytes of its own
input — the last segment hash. Manifest data compared against manifest
data, with the payload key never used.

### Why this is not opt-in

The algorithm was read from `rootSignature.alg` in the manifest, which
is unauthenticated, and unknown values were *coerced* to HS256 rather
than rejected. So an attacker **with no key** could take any
HS256-rooted TDF, rewrite the root to `"GMAC"` with a signature copied
from the last segment hash, and then truncate, reorder, or duplicate
segments — with the file still verifying.

### The fix

* Split the routine into `segmentIntegrity` (HS256 or GMAC) and
`rootIntegrity` (**HS256 only**), so tag extraction can no longer be
pointed at a non-AEAD input.
* `rootIntegrityAlgorithmFromManifest` resolves the root algorithm
against an allowlist and throws `SDK.RootSignatureValidationException`
for anything else, instead of coercing.
* `segmentIntegrityAlgorithmFromManifest` allows both algorithms,
because both are meaningful in that position, but still refuses an
unrecognised name rather than defaulting it. A GMAC segment hash proves
nothing on its own; what makes it trustworthy is that it is bracketed by
two keyed checks — the HS256 root signature over the whole hash list,
validated in `loadTDF` before any payload is read, and the real AES-GCM
tag check under the payload key at decrypt time. The root has neither
backstop, which is why the asymmetry between the two positions is
structural rather than stylistic.
* `createTDF` validates both configured algorithms before writing a
byte, so the SDK cannot emit a file it would refuse to read, and cannot
leave a partial TDF behind on a bad config.

### On the guard inside `rootIntegrity`

`rootIntegrity` validates its own argument even though `createTDF` and
the manifest resolver have both already checked. That is deliberate: the
check is what makes the function safe to call, so it belongs with the
function rather than only at today's call sites.

It is genuinely redundant today, though, and the mutation results below
say so plainly — with the resolver in place, a GMAC root cannot reach
`rootIntegrity` through any public entry point. It is defence against a
future edit, not against a current input. Because nothing else exercises
it, this PR adds tests that call the guards directly; without those, a
regression that reintroduced tag extraction inside `rootIntegrity` would
leave the entire suite green.

The two guards throw `IllegalArgumentException` rather than
`SDK.TamperException`, matching how the config layer already reports bad
caller-supplied values (`Config.withSegmentSize`). On a read they are
unreachable, so if one ever does fire it means a bug in `TDF`, not a
hostile file — it should escape loudly rather than arrive at callers
wearing an exception type they routinely catch.

## Compatibility

**No well-formed file is affected.** Every golden TDF in the cross-SDK
corpus is `rootSignature.alg = "HS256"` with `segmentHashAlg = "GMAC"`.

Nothing in the ecosystem emits a GMAC root:

* **Go SDK** — uses position-specific types. `RootIntegrityAlg` admits
only `RootHS256`; the GMAC constant is deprecated there and annotated as
not a legal root algorithm.
* **JS SDK** — `type RootIntegrityAlgorithm = 'HS256'`, so it is not
expressible.
* **Java SDK** — `Config` has never exposed a setter for the root
algorithm, and `newTDFConfig` defaults it to HS256. The one way to have
produced such a file is a caller who reached past the builder and
assigned `TDFConfig.integrityAlgorithm = GMAC` on the public field.
Those callers now get an `IllegalArgumentException` from `createTDF`
instead of silently writing an unverifiable file.

**There is no escape hatch, by design** — no flag, no compatibility
mode, no target-mode exemption. A file this rejects is one no honest
writer produces, and accepting it would restore the vulnerability for
every reader. Anyone holding such a file must re-encrypt it. The legacy
hex-encoded root path is still supported, and is covered by a test that
confirms it did not become a way around the allowlist.

## Test plan

```bash
mvn -q compiler:compile compiler:testCompile -pl sdk,cmdline
mvn -q surefire:test -pl sdk       # 261 tests, 0 failures, 8 skipped
mvn -q surefire:test -pl cmdline   # 13 tests, 0 failures
```

> Note: `mvn test` runs `generateSources`, which shells out to `buf`
against the BSR and is rate-limited. Invoking the plugin goals directly,
as above, skips that phase. Note also that `-pl sdk` on a *lifecycle
phase* trips the `ReactorModuleConvergence` enforcer rule, since the
parent is then outside the reactor; direct goal invocation avoids that
too.

`TDFRootSignatureTest`: **30 tests, 0 failures**, in two groups.

**25 behavioural tests** drive the public API end to end — truncation,
reordering, GMAC in several casings, an unknown root algorithm that must
not be coerced, an unknown segment algorithm, a legacy hex-encoded GMAC
root, a config-level GMAC root, plus controls that must keep passing for
reasons unrelated to this change.

**5 guard tests** call `rootIntegrity` / `segmentIntegrity` and their
argument checks directly. These reference methods this PR introduces, so
unlike the group above they cannot be run against `main`.

### Verified by reverting, and by mutation

With this commit's `TDF.java` reverted to `main` and the test file
otherwise untouched, the 25 behavioural tests **compile unmodified** and
**9 fail** across 7 methods — the vulnerability is live and
reproducible, and the tests are not merely asserting the shape of the
new code:

```
gmacRootIsRejected
gmacRootIsRejectedInAnyCasing[1..3]
gmacDowngradeWithTruncatedSegmentsIsRejected
gmacDowngradeWithReorderedSegmentsIsRejected
unknownRootAlgorithmIsRejected
unknownSegmentAlgorithmIsRejected      # wrong type on main: SegmentSignatureMismatch, by coercion accident
createTdfRefusesAGmacRootSetDirectlyOnTheConfig
```

The other 16 stay green, so the suite is discriminating rather than
strict.

Mutating one layer at a time locates which check is load-bearing:

| variant | `TDFRootSignatureTest` |
| --- | --- |
| baseline | 30 pass |
| resolver coerces unknown → HS256, `rootIntegrity` intact | **3 fail**
|
| `rootIntegrity` accepts GMAC again, resolver intact | **1 fail** (the
new direct guard test) |
| both reverted (`main`) | **9 of 25 fail** |

The resolver is the layer that actually stops a hostile file. The
`rootIntegrity` guard is caught only by the test written for it — which
is the argument for having written it, and an honest statement of what
that check is and is not doing.

## Related

* Cross-SDK coverage and the attack corpus: `spec/DSPX-4703.md` in
`opentdf/tests`, and opentdf/tests#594
* Write-side controls: #400 (DSPX-4736) — independent, no longer
stacked; this PR targets `main`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Strengthened integrity verification for encrypted segments and TDF
root signatures.
* Rejected unknown, missing, unsupported, or invalid integrity algorithm
declarations.
* Prevented GMAC-based root-signature downgrades during TDF creation and
verification.
* Improved detection of modified or tampered TDF content, including
legacy signature formats.
* Added validation for configured integrity algorithms while preserving
support for valid HS256 root signatures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

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

Copy link
Copy Markdown
Member Author

these should be fixed values for now

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.

1 participant