> **Part 14 of 20** in the DSPX-2604 re-cut. Base branch:
`dspx-2604-13-unresolved-kas`.
>
> This stack replaces #3782 / #3865 / #3921, which stay open and
untouched
> until it lands. Nothing here is a rebase of those branches — the work
was
> re-cut from the ticket so each PR stands on its own.
### Proposed Changes
`sdk/experimental/tdf` carried its own copies of the manifest and
assertion
types -- `Manifest`, `Segment`, `KeyAccess`, `Assertion`, `Statement`,
`AssertionKey` and the rest -- structurally identical to the ones in
`sdk`
but distinct to the type system, so anything crossing the boundary
needed
conversion. Two copies of the JWT signing and verification logic also
had
to be kept in step by hand.
Replaces both files' definitions with type aliases. `sdk` owns the
definitions; this package re-exports them. Every exported name and every
method survives: `Assertion.Sign` / `Verify` / `GetHash`,
`Statement.UnmarshalJSON`, `AssertionKey.IsEmpty` / `Algorithm`,
`AssertionVerificationKeys.Get` / `IsEmpty` and the five `String()`
methods
all come along with the aliased types, so importers compile unchanged. A
manifest produced here can now be handed to the stable SDK without
conversion, which is what the follow-up delegation needs.
Two deliberate non-aliases:
`Policy`, `PolicyBody` and `PolicyAttribute` stay local.
`sdk.PolicyObject`
declares `Body` as an anonymous struct over an unexported element type,
so
there is no nameable sdk equivalent to alias to. Exporting those in
`sdk`
first would make the alias possible; that is a separate change.
`IntegrityAlgorithm` stays a distinct `int` type.
`sdk.IntegrityAlgorithm`
is itself `= int`, so no method can be attached to it, and aliasing
would
silently drop `String()` from this package's public API. The underlying
values match, so the two convert freely.
`kSplitKeyType`, `kPolicyBindingAlg`, `kGMACPayloadLength` and
`calculateSignature` are retained verbatim: this package still builds
its
own manifests and they have callers in `writer.go` and `key_access.go`.
The change that removes those callers removes these too.
No behavior change. The package's existing tests pass unmodified.
### Checklist
- [ ] I have added or updated unit tests
- [ ] I have added or updated integration tests (if appropriate)
- [ ] I have added or updated documentation
### Testing Instructions
```
cd sdk && go test ./experimental/... -race
```
No behavior change — the point is that the package's existing tests pass
unmodified against aliased types.
<details>
<summary><b>The full DSPX-2604 stack — 20 PRs</b></summary>
| # | PR | Based on |
|---|----|----------|
| 01 | #3930 chore: bump go.work toolchain to go1.25.12 and simplify an
rt_test condition | `main` |
| 02 | #3931 feat(sdk): make the zipstream clock injectable for
deterministic ZIP output | `main` |
| 03 | #3932 fix(sdk): reject a zipstream write set that omits segment 0
| #3931 |
| 04 | #3933 fix(sdk): map ReadAt plaintext offsets from cumulative
segment sizes | `main` |
| 05 | #3934 chore(sdk): extract integrityAlgorithmString,
createPolicyBinding, signAssertions | `main` |
| 06 | #3935 chore(sdk): add direct tests for createKeyAccess,
encryptMetadata and tdfSalt | `main` |
| 07 | #3936 fix(sdk): fill each segment with io.ReadFull and size the
buffer to the input | `main` |
| 08 | #3937 chore(cli): move streaming IO helpers into pkg | `main` |
| 09 | #3938 fix(cli): stream encrypt instead of buffering the whole
payload | #3937 |
| 10 | #3939 fix(cli): stream decrypt and inspect instead of buffering |
#3938 |
| 11 | #3940 feat(sdk): add a chunked segment writer (experimental) |
`dspx-2604-base-11` = #3932 + #3934 + #3935 |
| 12 | #3941 fix(sdk): stop GetManifest from splitting the key under the
lock | #3940 |
| 13 | #3942 fix(sdk): reject a chunked split naming a KAS with no
resolved public key | #3941 |
| 14 | #3943 chore(sdk): alias experimental/tdf manifest and assertion
types | #3942 |
| 15 | #3944 fix(sdk): emit spec-compliant key access in
experimental/tdf and delegate Writer | #3943 |
| 16 | #3945 feat(sdk): accept io.Reader in CreateTDF and drop the 64 GB
payload cap | #3936 |
| 17 | #3946 chore(sdk): rewrite CreateTDF on top of the chunked writer
| `dspx-2604-base-17` = #3944 + #3945 |
| 18 | #3947 chore(sdk): drop dead TDFConfig fields and deprecate the
TDFFormat enum | #3946 |
| 19 | #3948 fix(cli): drop the encrypt-side stdin spool |
`dspx-2604-base-19` = #3947 + #3939 |
| 20 | #3949 feat(sdk): graduate the chunked writer to stable API |
#3948 |
**Reviewable in parallel right now**, since they sit directly on `main`
and depend on
nothing else: 01, 02, 04, 05, 06, 07, 08.
**Why three PRs have a `dspx-2604-base-*` base.** A GitHub PR takes one
base branch,
but 11, 17 and 19 each build on more than one parent. The `base-*`
branches are empty
merge commits that exist only to join those parents so the PR diff shows
exactly its
own change and nothing else. They contain no code, have no PR of their
own, and go
away once their parents land — retarget the child onto `main` at that
point.
**Wants a cross-SDK xtest run before merge:** 15, 17 (and therefore 20).
They touch
the KAS wire format.
**Red checks you may see are network flakes, not this stack.** Four
distinct ones hit
this batch and all clear on re-run: `golangci-lint config verify` timing
out on
`https://golangci-lint.run/.../golangci.v2.8.jsonschema.json` (fails the
whole `go
(<module>)` job and fail-fast cancels its siblings), the bats installer
getting a 403,
Docker Hub timing out on `keycloak/keycloak:26.4`, and `buf` reporting
"the server
hosted at that remote is unavailable" while the Java SDK generates
sources. The
`govulncheck` step also emits `##[error]` annotations against the
go1.25.11 stdlib, but
it is `continue-on-error: true` and never fails a job — 01 bumps the
toolchain and
clears those annotations.
</details>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Compatibility**
* Experimental TDF manifest and assertion types now align with the
stable SDK, enabling direct interoperability without type conversion.
* Existing assertion and manifest behavior is provided through the
stable SDK definitions.
* **Documentation**
* Clarified the relationship between the experimental TDF package and
the stable SDK.
* Updated documentation for missing assertion verification keys to
reflect current behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Proposed Changes
chunkedWriter.GetManifestheldmu.RLockfor its whole body, and thatbody calls
KeySplitter.Split. A real splitter resolves KAS public keysover the network, so every
WriteSegmentracing aGetManifestsat on thewrite lock until those round-trips finished.
sync.RWMutexbars newreaders once a writer is queued, so a second
GetManifestbehind thatWriteSegmentblocked as well -- one manifest snapshot could stall thewhole writer.
Splits the manifest build in two.
snapshotLockedresolves the emissionorder and copies each segment's metadata;
buildManifestthen works fromthat snapshot and touches no mutable writer state, so it needs no lock --
the dek, the splitter, the integrity algorithms and the signature encoding
are all fixed at construction.
GetManifestnow releases the read lock assoon as the snapshot is taken.
The snapshot copies
Segmentvalues rather than the*Segmentpointersw.segmentsholds.WriteSegmentmutates those in place when the archiveaccepts a write, so ranging over the pointers after releasing the lock
would be a data race, not merely a stale read.
Finalizedeliberately keeps the write lock across the split. It isterminal -- no
WriteSegmentmay succeed after it returns -- so there isno concurrency to preserve, and dropping the lock would open a window for a
segment to reach the archive after the snapshot that fixes the manifest.
This makes explicit a semantic that was already true:
GetManifestreturnsa point-in-time view. A segment committed after the snapshot is absent from
that manifest and present in the next one. The new test pins both halves,
along with the property that motivated the change -- a
WriteSegmentissued while a split is in flight completes rather than blocking. It
deadlocks to a 10s timeout against the previous locking and passes against
this one.
Checklist
Testing Instructions
The new test deadlocks to its 10s timeout against the previous locking and
passes against this one.
The full DSPX-2604 stack — 20 PRs
mainmainmainmainmainmainmaindspx-2604-base-11= #3932 + #3934 + #3935dspx-2604-base-17= #3944 + #3945dspx-2604-base-19= #3947 + #3939Reviewable in parallel right now, since they sit directly on
mainand depend onnothing else: 01, 02, 04, 05, 06, 07, 08.
Why three PRs have a
dspx-2604-base-*base. A GitHub PR takes one base branch,but 11, 17 and 19 each build on more than one parent. The
base-*branches are emptymerge commits that exist only to join those parents so the PR diff shows exactly its
own change and nothing else. They contain no code, have no PR of their own, and go
away once their parents land — retarget the child onto
mainat that point.Wants a cross-SDK xtest run before merge: 15, 17 (and therefore 20). They touch
the KAS wire format.
Red checks you may see are network flakes, not this stack. Four distinct ones hit
this batch and all clear on re-run:
golangci-lint config verifytiming out onhttps://golangci-lint.run/.../golangci.v2.8.jsonschema.json(fails the wholego (<module>)job and fail-fast cancels its siblings), the bats installer getting a 403,Docker Hub timing out on
keycloak/keycloak:26.4, andbufreporting "the serverhosted at that remote is unavailable" while the Java SDK generates sources. The
govulncheckstep also emits##[error]annotations against the go1.25.11 stdlib, butit is
continue-on-error: trueand never fails a job — 01 bumps the toolchain andclears those annotations.
Summary by CodeRabbit
New Features
Bug Fixes