Skip to content

fix(ai): use injective serialization for tool approval HMAC payload#17493

Open
dnukumamras wants to merge 3 commits into
mainfrom
dnukumamras/tool-approval-injective-payload
Open

fix(ai): use injective serialization for tool approval HMAC payload#17493
dnukumamras wants to merge 3 commits into
mainfrom
dnukumamras/tool-approval-injective-payload

Conversation

@dnukumamras

@dnukumamras dnukumamras commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

The tool approval signature payload was built by joining fields with \n. Because fields like toolName and toolCallId can contain a newline, two different field tuples could serialize to identical bytes — so a signature issued for one tuple could verify against a different one. The payload is now serialized with JSON.stringify (with a constant domain-separation prefix), which escapes delimiter/control characters and makes the encoding injective.

Verification stays backwards compatible: it falls back to the old newline format only when no signed field contains a \n, so approvals signed by an older version still verify across an upgrade. This legacy fallback is marked TODO: remove in v8.

Happy path

  • A valid approval signs and verifies for its own (approvalId, toolCallId, toolName, input) tuple.
  • Input key ordering is still normalized, so equivalent inputs produce the same signature.
  • A signature in the old newline format still verifies (delimiter-free fields), so a pending approval that straddles an upgrade is not rejected.

Unhappy path

  • A tuple whose toolName contains a newline can no longer be retupled into toolCallId to reuse the same signature for a different tool — in both the new and the legacy fallback paths.
  • Any delimiter/control character (\n, \r, \t, \0, ", \\) shifted across a field boundary fails verification.
  • A legacy signature over a newline-bearing field no longer verifies (the fallback is refused when a field contains \n).

🤖 Generated with Claude Code

Serialize the signed approval fields with JSON.stringify instead of
newline concatenation so distinct field tuples cannot produce identical
signed bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lgrammel

Copy link
Copy Markdown
Collaborator

what happens re backwards compat? what about people who use v7 in prod and have the old signatures

@dnukumamras

Copy link
Copy Markdown
Contributor Author

what happens re backwards compat? what about people who use v7 in prod and have the old signatures

This only impacts for signatures that are pending ones.

The edge case is if a tool the model requested (signed by the old server) whose human approval arrives on a turn handled by the new server, before the tool has executed. New server recomputes the JSON payload, HMAC differs, and throws a hard error

…miter-free

Fall back to the legacy newline-joined payload on verify when no signed field
contains a newline, so approvals signed by an older version still verify across
an upgrade without reopening the collision. Marked for removal in v8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lgrammel

Copy link
Copy Markdown
Collaborator

Bugfix review

Outcome: approved

Fixes issue

Status: fully-addresses

The fixed-position JSON array gives each signed string field an unambiguous boundary and adds domain separation. Independent verification confirmed the old newline-retupled payloads collide while the new payloads and HMACs differ.

Side effects

Risk: low

Normal signing, verification, and canonical input ordering remain unchanged apart from the intentional signature-format replacement.

Concerns:

  • An approval issued by an older server will fail closed if verified by the new version and must be reissued.

Performance

Risk: low

JSON serialization remains linear in the field lengths and replaces an existing concatenated-string allocation with only small escaping and prefix overhead.

Backwards compatibility

Risk: low

Stored messages are not modified, but signatures stored in pending approval histories from the old format will no longer verify; this deliberate fail-closed migration is disclosed in the pull request.

Concerns:

  • Deployments with in-flight signed approvals may require users to repeat the approval flow.

Security

Risk: none

The change closes the field-boundary collision without weakening HMAC verification, canonical input hashing, or secret handling. The fixed domain prefix also prevents reuse across differently versioned payload domains.

Testing

Status: appropriate

Tests cover the original newline retupling attack, valid self-verification, distinct signatures, canonical input key ordering, and representative control and escaping characters in both Node and edge environments.

Verification

Reviewed the complete three-file diff and surrounding signing and replay-validation paths. The focused signature and approval-validation suites passed in Node and edge environments, package type checking passed, formatting and lint checks passed, and a bounded serialization check found no collisions across 2,000 representative tuples including delimiters and lone surrogates.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants