Skip to content

feat(sdk): add opt-in unprefixed manifest entry name - #405

Open
pflynn-virtru wants to merge 1 commit into
mainfrom
fix/manifest-json-entry-name
Open

pflynn-virtru wants to merge 1 commit into
mainfrom
fix/manifest-json-entry-name

Conversation

@pflynn-virtru

@pflynn-virtru pflynn-virtru commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds an opt-in that writes the TDF manifest under the zip entry name manifest.json, the name the TDF spec defines, instead of the legacy 0.manifest.json. Default output is unchanged.

  • Config.withUnprefixedManifestName() and the cmdline flag --unprefixed-manifest-name.
  • A pre-4.3.0 Config.withTargetMode overrides the opt-in and keeps 0.manifest.json. The override is resolved where the writer is built, so option order doesn't matter.
  • Readers already accept both names (fix(sdk): read a manifest.json entry from TDF archives #406). Readers without that support can't open output written with this option; the javadoc says so.
  • The TDFWriter constructor that takes an entry name is package-private.

This replaces the earlier version of this PR, which changed the default and was a breaking change. It mirrors opentdf/platform#4049 (Go) and opentdf/web-sdk#1040 (JS).

Testing

  • mvn -pl sdk,cmdline -am test: sdk 314, sdk-pqc-bc 28, cmdline 14, all passing.
  • New tests: TDFWriterTest.writesTheManifestUnderTheGivenEntryName, TDFTest.unprefixedManifestNameRoundTrips, TDFTest.legacyTargetModeOverridesUnprefixedManifestName (both option orders), TDFTest.defaultManifestNameIsUnchanged, ConfigTest.withUnprefixedManifestName_shouldSetUnprefixedManifestName, CommandTest.encrypt_unprefixedManifestNameFlag_acceptedByPicocli.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an option for encryption to name the manifest manifest.json instead of the default 0.manifest.json.
    • The generated archive remains readable, and compatible target modes retain the default manifest name.
  • Documentation
    • Clarified that manifest.json is an opt-in manifest name.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in 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: 59ed4d41-9bab-4d1e-80e3-754e7d499266
📥 Commits

Reviewing files that changed from the base of the PR and between ce5f6f7 and 487dedc.

📒 Files selected for processing: 8
  • 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/main/java/io/opentdf/platform/sdk/TDF.java
  • sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java
  • sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java
  • sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
  • sdk/src/test/java/io/opentdf/platform/sdk/TDFWriterTest.java

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

📝 Walkthrough

The SDK adds an opt-in configuration and encrypt command option for writing the manifest as manifest.json. It retains 0.manifest.json by default and when target mode 4.2.2 is enabled. Tests cover archive entry names and TDF readability.

Changes: Manifest name option

Layer / File(s) Summary
Configure the manifest name
sdk/src/main/java/io/opentdf/platform/sdk/Config.java, cmdline/src/main/java/io/opentdf/platform/Command.java, sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java, cmdline/src/test/java/io/opentdf/platform/CommandTest.java
TDFConfig adds an opt-in setting, and the encrypt command exposes it as --unprefixed-manifest-name. Tests check the configuration setting and CLI option recognition.
Select and write the manifest entry
sdk/src/main/java/io/opentdf/platform/sdk/TDF.java, sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java, sdk/src/test/java/io/opentdf/platform/sdk/TDFWriterTest.java
TDF.createTDF selects the manifest entry name from the configuration and target-mode setting. TDFWriter writes the manifest under the selected name. A writer test checks the spec entry name.
Verify archive naming and readability
sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
Tests cover spec-name output and TDF readability, the legacy name with target mode 4.2.2, and the default legacy name.

Priority: ➖ Normal

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Command
  participant TDFConfig
  participant TDF
  participant TDFWriter
  Command->>TDFConfig: Apply unprefixed manifest option
  TDFConfig->>TDF: Provide manifest-name setting
  TDF->>TDFWriter: Pass selected manifest entry name
  TDFWriter->>TDFWriter: Write manifest under selected name
Loading

Suggested reviewers: mkleene

Merge Risk: 🟡 Moderate · up to 487de

Default encryption retains the legacy filename, but opting in can produce archives that older readers cannot consume. The Java integration cells are reported failing while external tests are updated, so resolve or explicitly accept that integration risk before merging.

Security architecture risk: 🔵 Low · up to ce5f6

New readers preserve legacy compatibility, and the inspected loading paths retain their existing verification controls. Newly written archives nevertheless require upgraded readers, so mixed-version deployments and reader rollback need care. External consumer behavior remains unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The directly affected exposure is attacker-supplied ZIP input processed by SDK consumers. Supported archive names and recognition expand, but the inspected change grants no new credentials or KAS authority. External callers that route decisions using isTDF were not established by the available repository evidence.

Trust Boundaries and Controls

  • observed — Both manifest names feed the same downstream parsing and loading path; recognition does not substitute for its controls. KAS allowlist enforcement remains conditional on the existing ignoreKasAllowlist configuration, and segment integrity remains checked before each segment is released.

Resilience and Maintainability Implications

  • observed — Duplicate entry names still fail the reader's duplicate-sensitive map construction. Manifest reads obtain fresh entry streams, while payload reading consumes a single stream and emits verified segments incrementally. A later failure can therefore leave an output prefix; the filename change does not add transactional rollback or retry semantics.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 27.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 40 functions across 12 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: adding an opt-in unprefixed manifest entry name in the SDK.
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.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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 checks the archive's name,
The spec path joins the file-name game.
The old path stays when options rest,
A payload reads and passes the test.
Then hops away, content in tow.

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

@pflynn-virtru
pflynn-virtru marked this pull request as ready for review September 15, 2026 19:04
@pflynn-virtru
pflynn-virtru requested review from a team as code owners September 15, 2026 19:04
@pflynn-virtru
pflynn-virtru force-pushed the fix/manifest-json-entry-name branch from 446d8ec to 9503fa7 Compare September 15, 2026 19:06
@pflynn-virtru pflynn-virtru changed the title fix(sdk)!: name the ZTDF manifest entry manifest.json per spec fix(sdk)!: name the TDF manifest entry manifest.json per spec Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

X-Test Failure Report

pflynn-virtru added a commit that referenced this pull request Oct 1, 2026
Refs opentdf/platform#3513. The read half of #405, split out so it can
land on its own.

## Problem

The [OpenTDF spec](https://opentdf.io/spec#tdf-structure) names the
manifest entry in a `.tdf` archive `manifest.json`:

> The `manifest.json` file MUST be in JSON format and reside within the
root of the OpenTDF Zip archive.

This SDK writes and reads `0.manifest.json`. On the read side
`TDFReader` does an exact `containsKey` with no fallback, so a TDF
produced by any implementation written against the published spec is
rejected with `tdf doesn't contain a manifest` before any schema check
runs. `SDK.isTDF` carries its own copy of the literal, so such an
archive is screened out one step earlier still.

The `0.` prefix is a holdover from an early design that anticipated
several payload/manifest pairs per archive. That design never shipped.

## Change

**Read** — `TDFReader` prefers `manifest.json` and falls back to
`0.manifest.json`. Preference matters rather than first-match: an
archive carrying both must not have its conformant entry passed over for
the superseded one.

**Sniffing** — `SDK.isTDF` accepts either name, so it does not reject an
archive the reader can now read. It also no longer requires the archive
to hold *exactly* two entries. That check was scoped out at first as
unrelated to the entry name, but review pointed out it is not: an
archive carrying both manifest names holds three entries, and
`prefersTheSpecNameWhenAnArchiveCarriesBoth` is precisely the case the
reader now handles — so the sniffer would reject what the reader it
screens for accepts. The count was never part of the structure `isTDF`
describes; the spec fixes where the manifest lives, not what else the
archive may hold.

**Duplicate entry names** — a zip may legally list one name twice, and
`Collectors.toMap` turned that into an `IllegalStateException` out of
`TDFReader`'s constructor: outside its declared `throws SDKException,
IOException`, outside what a caller screening untrusted input catches,
and outside the fuzz targets' catch list. Dropping the count check above
made it newly reachable through `isTDF`, so it is rejected here as an
`IllegalArgumentException` alongside the constructor's other
malformed-input paths.

An archive carrying *both* manifest names is still read, not rejected.
Those are two distinct entries and the spec settles which one wins; a
single name listed twice offers no principled choice. Worth being
explicit about the tradeoff, since the root signature covers only
`integrityInformation.segments` — not `policy`, not `keyAccess[].url` —
and no hash covers the entry name: a DEK holder can file two
internally-valid manifests over one ciphertext and have this SDK enforce
one while a `0.`-keyed peer enforces the other. That ambiguity predates
this PR; preferring the spec name relocates which side Java lands on
rather than creating it. Failing closed would be a cross-SDK decision,
not a Java-only one.

**Write is untouched.** `TDFWriter` still emits `0.manifest.json`, so
this release produces byte-identical archives and every existing reader
keeps working. The new `TDF_MANIFEST_FILE_NAME_SPEC` constant is
read-side only. Flipping the writer is the breaking half and stays in
#405.

`0.payload` is untouched. It is recorded in the manifest's
`payload.url`, so renaming it would alter manifest contents rather than
just archive layout, and neither the spec nor opentdf/platform#4049
fixes it.

## Testing

Ported from #405, minus the write-side test:

| Test | Failure without the change |
|---|---|
| `TDFReaderTest.readsManifestUnderTheSpecName` |
`IllegalArgumentException: tdf doesn't contain a manifest` |
| `TDFReaderTest.readsThePayloadAlongsideASpecNamedManifest` | same |
| `TDFReaderTest.prefersTheSpecNameWhenAnArchiveCarriesBoth` | returned
the non-aligned manifest |
| `SDKTest.testExaminingTDFWithSpecManifestName` | `false` |
| `SDKTest.testExaminingTDFWithBothManifestNames` | `false` -- three
entries |
| `SDKTest.testExaminingTDFWithAnExtraEntry` | `false` -- three entries
|
| `TDFReaderTest.rejectsAnArchiveThatListsOneNameTwice` |
`IllegalStateException: Duplicate key 0.payload` |
| `TDFReaderTest.rejectsAnArchiveThatListsTheManifestNameTwice` | same,
for `manifest.json` |

`prefersTheSpecNameWhenAnArchiveCarriesBoth` files a *different*
manifest under each name, so it cannot pass by reading whichever entry
the reader happened to pick.

Guarding behavior that must not change:
`TDFReaderTest.readsManifestUnderTheOffspecName`,
`TDFReaderTest.rejectsAnArchiveWithNoManifestUnderEitherName`,
`SDKTest.testExaminingZipWithNoManifest`,
`SDKTest.testExaminingZipWithNoPayload`, and the pre-existing
`SDKTest.testExaminingValidZTDF` / `testExaminingManifest`, which run
against the checked-in non-aligned-named `sample.txt.tdf` fixture. The
two negative `isTDF` cases hold two entries each, so they failed for the
name they were missing rather than for their entry count even before the
count check came out.

Review of this branch added four tests that pin rules nothing else held
down: `rejectsNearMissManifestEntryNames` (a basename or
case-insensitive lookup passed the suite before, so `sub/manifest.json`
would have been read as the manifest despite the spec's root-only rule),
`SDKTest.testExaminingLargerZipWithNoManifest` (both negatives held two
entries, so "any zip over two entries is a TDF" passed), and the two
duplicate-name cases above.

**Verification:** Local Java execution was unavailable because this
machine has no JDK or Maven. GitHub CI passed `mavenverify`, Platform
Integration (including FIPS), and the full cross-SDK xtest matrix on the
PR head; the previously failing matrix job passed on rerun. The new
tests were also watched failing against unmodified production code on
#405's branch, where the shared code is identical.

## Downstream impact

None. Readers gain a name they accept and `isTDF` gains archive shapes
it recognizes; nothing either previously accepted is taken away, and no
archive this SDK writes changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* **Compatibility**
* Improved support for TDF archives using the standard `manifest.json`
entry name.
* Continued support for archives using the SDK’s existing
`0.manifest.json` entry name.
* When both manifest names are present, the standard `manifest.json`
entry is used.
* Archives may include additional entries beyond the manifest and
payload.

* **Bug Fixes**
* TDF detection now correctly recognizes valid archives and rejects
those missing a manifest or payload.
* Archives containing duplicate entry names are now rejected with a
clear error.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
Co-authored-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Paul Flynn <pflynn-virtru@users.noreply.github.com>
@pflynn-virtru
pflynn-virtru force-pushed the fix/manifest-json-entry-name branch from ce5f6f7 to 487dedc Compare October 19, 2026 22:59
@pflynn-virtru pflynn-virtru changed the title fix(sdk)!: name the TDF manifest entry manifest.json per spec feat(sdk): add opt-in unprefixed manifest entry name Oct 19, 2026
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

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