fix(spec): make the §5.3 verification example reject truncated signatures - #93
Merged
Merged
Conversation
…ures The normative webhook-verification example in SPECIFICATION.md §5.3 called `timingSafeEqual` on two buffers without checking their lengths. Node throws `RangeError: Input buffers must have the same byte length` when they differ, so an attacker-supplied truncated `webhook-signature` header produced an unhandled exception — surfacing as HTTP 500 rather than an authentication failure. The same example also parsed a single signature token, so it rejected valid traffic for the whole duration of any `delivery_secret` rotation, during which publishers send multiple space-delimited signatures. The reference libraries (`@eep-dev/signer`, `eep-signer`) already handle both cases correctly. Only the specification — which is what implementers copy from — taught the unsafe version. Changes: - Rewrite the §5.3 example: parse every space-delimited token, guard on buffer length before the constant-time compare, and enforce the 60s timestamp tolerance the surrounding text already mandates but the example omitted entirely. - Promote both behaviours to explicit receiving-platform MUSTs, and point readers at `@eep-dev/signer` rather than a hand-rolled compare. - Add conformance fixture `signature/truncated-signature` (a strict prefix of a valid signature) with the manifest entry and the TypeScript + Python harness cases. - Add regression tests asserting "returns false, does not throw" for truncated and over-long signatures in `@eep-dev/signer`, `eep-signer` and the compliance-cli verifier. No wire-format change: this corrects prose and example code to match the behaviour the reference implementations already ship. Refs: EEP audit 2026-08 finding A8 Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
This was referenced Aug 26, 2026
`pull_request.branches` was limited to `main`, so a PR based on another branch got no checks whatsoever. That is exactly the situation where a reviewer most needs them: a stacked change is only reviewable in isolation if CI has actually run against it. Stacking PRs is the practical way to ship a series of related changes without resolving the same conflicts N times, and it should not cost the series its test coverage. Pushes to `main` are unaffected. Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR 1 of a stacked series fixing findings from the 2026-08 EEP protocol audit. Each PR in the series is based on the previous one so the diffs stay reviewable in isolation and never conflict; GitHub retargets the base automatically as they merge. Not for merge without review — opened for discussion.
The normative webhook-verification example in
SPECIFICATION.md§5.3 is unsafe as written:Two defects:
timingSafeEqualthrowsRangeErrorwhen buffer lengths differ. An attacker sending a truncatedwebhook-signatureheader triggers an unhandled exception — HTTP 500 — instead of a clean 401. The input is entirely attacker-controlled.v1,old v1,new) during secret rotation, so this code rejects valid traffic for the full duration of every rotation.The example also silently omits the 60-second timestamp check that requirement 4 in the same section mandates.
@eep-dev/signer(src/index.ts:86-95) and its Python sibling already get all three right, as does the compliance-cli verifier. Only the specification — the thing implementers copy from — taught the unsafe version.What changed
@eep-dev/signerso the path of least resistance is importing the reference implementation, not re-deriving it.signature/truncated-signature— a strict prefix of a valid signature — registered inmanifest.jsonand wired into both the TypeScript and Python harnesses.@eep-dev/signer,eep-signer, andcompliance-cli.Scope
Checklist
Verification
Run locally against this branch:
tests/(vitest)@eep-dev/signer@eep-dev/compliance-clieep-signer-python(pytest)tests/cross-impl/test_conformance_fixtures.pynode scripts/codegen-schema-types.mjs --checkNotes for reviewers
The fixture is deliberately a prefix of the correct signature rather than random bytes — random bytes of the right length would exercise
signature_mismatchbut never reach the length-guard path that this finding is about.No schema under
schemas/v0.1/changed, so the drift gate is a no-op here.