* Trim stale guidance from AGENTS.md and the terminal CLAUDE.md
AGENTS.md drops its numbered headings and the build/test commands that
the pre-push checklist already covers. The terminal CLAUDE.md drops the
architecture, state and MCP sections that had drifted from the code, and
points the TUI testing tools at arkavo-mcp-macos, where they now live,
instead of the removed arkavo-test crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bump version to 0.101.2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
* Bump opentdf-rs to 0.16
0.16 writes the spec policy binding, Base64(HMAC-SHA256(DEK,
Base64(policy))), 44 characters, in place of the legacy Base64(hex(HMAC)).
Rewrapping what edge now writes needs a KAS that accepts the spec form:
platform.arkavo.net does through opentdf-platform#54; released upstream
KAS builds do not yet. The GGUF-TDF writer test now pins the spec form.
KeyAccessObject carries the split id 0.16 round-trips. Without it a
multi-KAS TDF from another SDK lost its sid on conversion. The field is
omitted when unset, so existing wire messages are unchanged. The three
copies of the KeyAccessObject <-> opentdf::KeyAccess conversion are now
one pair, so the sid cannot be dropped at one site and kept at another.
The kas feature of arkavo-tdf did not compile against 0.15 either:
KasClient::new takes an OpentdfConfiguration since 0.13. The client now
builds one with for_kas_legacy_rest, matching the legacy paths it
already uses for the public key. CI runs arkavo-tdf's tests with the kas
and a2a features, which nothing did before.
0.16 names the ZIP manifest entry manifest.json, where 0.15 wrote and
read only 0.manifest.json. GGUF-TDF archives are unaffected: edge's
writer names its own entry. Config bundles are affected: they are ZIP
TDFs from Tdf::encrypt, so a bundle sealed by this version cannot be
opened by an agent on 0.101.x or earlier, nor by released otdfctl or the
Go, Java and JS SDKs. Upgrade agents before the host that seals bundles.
Specs: TDF-010 (split id survives conversion) and TDF-011 (writers emit
the spec binding).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
* Verify the policy binding before the A2A KAS rewraps a key
The kas.rewrap handler checked only that a binding claimed HS256 and was
not empty. A caller holding a wrapped key could send it with any policy
its entitlements satisfied, keep the original binding, and receive the
DEK for data whose real policy denied it.
The handler now recovers the DEK from the wrapped key, deriving it as
opentdf-kas's ec_unwrap does (ECDH with the header's ephemeral key, then
HKDF-SHA256 with the NanoTDF version salt), and checks
HMAC-SHA256(DEK, policy) over the base64 policy as sent, in constant
time. It accepts the spec Base64(MAC) that opentdf-rs 0.16 and
OpenTDFKit 5 write and the legacy Base64(hex(MAC)), as
opentdf/platform#4081 does. The check runs before ABAC, so the policy a
decision uses is the one the data was sealed with. A failed check is
PolicyBindingInvalid (JSON-RPC -32003) and nothing is rewrapped.
Tests seal a DEK from the writer's side of the ECDH, so they do not
share the handler's derivation, and open the rewrapped key as a client
does to show the released DEK is the bound one. With the check
disabled, the swapped-policy, wrong-key and placeholder-binding tests
fail. The binding module also pins the opentdf-rs/OpenTDFKit known
answer and an openssl-computed legacy vector. The server's delegation
test now sends a real binding instead of "test-binding-hash".
TDFS-006 now states what the KAS does. Its "tampering attempt logged
with forensic details" step is gone: nothing logged it before or after
this change. TDF-008 lists the rewrap checks in order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
* Make the TDF specs match what the code does
An audit of tdf.spec and tdf-security.spec against the code found
scenarios that named error variants that do not exist, claimed behavior
nothing implements, and cited code that had moved.
Marked wip, each with an issue naming the gap:
- TDF-002 and TDF-009: OpenTdfService seals wrappedKey under a key it
discards and its decrypt always fails, so nothing it writes can be
decrypted. The agent-credentials plan's layer B replaces this path.
- TDF-006: encrypt_stream buffers the whole stream.
- TDFS-009: no policy time window; only token expiry is enforced.
- TDFS-013: the rewrap session key is ECDH with the KAS's static key, so
there is no forward secrecy.
TDFS-011 keeps the steps that hold (approved algorithms, no cleartext
DEK release, zeroized DEK). HSM custody and rotation move to a new wip
TDFS-016, which records that the agent server regenerates its KAS key
on every start.
Wording now matches the code: TdfError::Decryption, Decision::Deny and
KasError::AccessDenied, ChainBroken for a bad signature, Iroh as the
only BlobTransport, and no binding from PolicyBuilder. TDF-011 no longer
says OpenTdfService output can be rewrapped. Stale refs are updated.
Existing tests now carry the scenarios they exercise (TDF-003, 004, 005,
007, 011). Token-expiry tests move from TDFS-009 to TDF-005, and
keypair tests no longer claim TDFS-013. TDF-005 gains a test that a
token altered after signing is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
* Release the folded change as 0.102.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
* Check the terminal guide for the sections it now keeps
The guide trim dropped the stale "Model Connection States" section and
renamed "Testing Requirements" to "TUI Testing Tools", but the doc test
still required the old headings. It now requires the sections the guide
keeps and checks that the TUI tool location the guide names exists,
since a stale path there is what the trim fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
* Derive the test client's session key into the cipher's key type
CodeQL read the zeroed [u8; 32] HKDF output buffer in the rewrap test
helper as a hard-coded AES key. The buffer is now aes_gcm's Key, filled
by HKDF before use, so no literal flows to the cipher.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
* Keep the per-IP rate limiter within its entry cap
check_rate_limit read the table size, evicted if it was full, and inserted
later without holding anything in between. Threads that each read one
below max_ip_entries all inserted, so the table grew past the cap that
bounds its memory. The nightly fuzz_rate_limit_concurrent target has
failed on this every night since at least 2026-10-02 ("Entry count 51
exceeds max 50").
Adding an entry now happens under a mutex: the capacity check, eviction
and insert run as one step. Lookups of existing entries stay lock-free.
Inside the lock the insert reuses an entry a concurrent first request from
the same IP has just added, rather than replacing it and refunding the
tokens that request spent.
The regression test fills the table to one below the cap, then releases
eight threads at once with new IPs. It failed 6 of 6 runs before the fix
and passes after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DB6GyYUSA2kkefLRhAHMoi
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the KAS half of #3578. Split out of #3597, which depends on this.
The bug
The spec defines the key access object's policy binding as
Base64(HMAC(KEY, POLICY))(spec). #2095 added a branch so KAS would accept that alongside the legacyBase64(hex(HMAC)). It has never worked:DecodedLenover-allocates. Only the hex branch swaps in a correctly-sized slice, so the raw branch hands 33 bytes toVerifyBinding, which compares withhmac.Equalagainst a 32-byte digest.hmac.Equalis length-sensitive, so it is always false. Confirmed present in releasedservice/v0.9.0.Net effect: no released KAS can accept a spec-compliant policy binding, and any SDK that starts emitting one becomes undecryptable against it.
The fix
Extract
decodePolicyBinding, trimmed to the bytes actually decoded, and use it on the rewrap path. Both encodings are accepted; length disambiguates them (32 vs 64), so no version signal is needed. Legacy hex stays accepted indefinitely — TDFs are archival.The extraction is there to make the fix testable; the inline block it replaces was 17 lines and is now 6.
Checklist
Testing
TestDecodePolicyBindingIsTrimmedis the regression test, and it asserts the decoded length, with the over-allocation written out as a precondition. That matters: the buggy code produced the right bytes and only the wrong length, so a content comparison alone passes against it. Verified by reverting the one-line trim — the test fails with a trailing0x0.TestDecodePolicyBindingcovers raw, legacy hex, and invalid base64.go test ./service/kas/... -racegreen;golangci-lintclean on both files (the onesloglinthit inrewrap_test.gois pre-existing, just line-shifted by two added imports);make fmtclean.Scope
Deliberately KAS-only: it widens what the server accepts and changes no output, so it can merge and deploy independently.
Left out on purpose, as separate concerns:
!marker. Noterelease-please-config.sdk.jsonusesalways-bump-patch, so the version number won't signal it.verifyPolicyBinding. It has no production callers and is likely why the broken dual-accept looked correct — it reads like the verification path and its own hex handling is fine. Worth removing, but it is cleanup, not this fix.Worth checking any third-party KAS for the same untrimmed-buffer bug before relying on raw bindings against it.
🤖 Generated with Claude Code