feat(signer,spec): Ed25519 delivery signatures with JWKS key publication - #108
Open
ucekmez wants to merge 1 commit into
Open
feat(signer,spec): Ed25519 delivery signatures with JWKS key publication#108ucekmez wants to merge 1 commit into
ucekmez wants to merge 1 commit into
Conversation
Delivery signing was HMAC-SHA256 only: publisher and subscriber share one `delivery_secret`, so a signature proves that *someone holding that secret* sent the event — and the subscriber is one of them. 1. Events are not non-repudiable. A subscriber can forge an event and attribute it to the publisher. §15's commerce state machine and §16's signed audit trail both ride on this signature. 2. No third party can verify. A regulator or counterparty cannot check an audit entry without being handed the subscriber's secret, which would let them forge entries too. 3. Rotation has no surface: no key id, no published key set. 4. PQC readiness stopped at the gate. §11.7 defines algorithm negotiation including PQ-hybrid signatures — but only for agent→publisher gate proofs. The path carrying every event had no asymmetric option at all. Standard Webhooks, which this package claims alignment with, already specifies Ed25519 with `whsk_`/`whpk_` prefixes and a published key set. EEP implemented the symmetric half and inherited the claim for both. - `@eep-dev/signer` gains `generateSigningKeyPair`, `signEd25519`, `verifyEd25519` and `toJwks`. The signed content is unchanged, so §5.3's replay rules apply identically and a dual-signing publisher builds the payload once. - Signature tokens are `v1a,[kid:]base64`. Verifiers for one scheme skip the other scheme's tokens rather than failing on them — otherwise dual-signing, which is the entire migration path, breaks both verifiers. - A malformed key in a configured key set is skipped, not fatal, so one bad entry cannot stop the good ones from being tried. All hostile input returns false rather than throwing. - New §5.3.1, plus a rotation rule with a concrete bound: keep the outgoing key published for at least the §5.4 retry span (~6 hours), so a delivery signed before a rotation still verifies when its last retry lands. - `signing_jwks_url` on the manifest, REQUIRED when signing asymmetrically. - `WebhookDispatcher` dual-signs when a key is configured; unchanged otherwise. Explicitly prohibited: reusing a `delivery_secret` as an Ed25519 key or deriving one from the other. The security properties differ precisely because the subscriber may hold the symmetric key and must never hold the private one. Refs: EEP audit 2026-08 findings B3, O8 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 16 of a stacked series. Base is #107. Not for merge without review.
Delivery signing is HMAC-SHA256 only. Publisher and subscriber share one
delivery_secret, so a signature proves that someone holding that secret sent the event — and the subscriber is one of them.Four consequences, and they matter more the further EEP goes:
Standard Webhooks — which
@eep-dev/signer's own header comment cites — already specifies Ed25519 withwhsk_/whpk_prefixes and a published key set. EEP implemented the symmetric half and inherited the claim for both.What changed
@eep-dev/signer—generateSigningKeyPair,signEd25519,verifyEd25519,toJwks.The signed content is unchanged (
{webhook-id}.{webhook-timestamp}.{raw-body}), so §5.3's replay rules apply identically and a dual-signing publisher builds the payload once.webhook-signature: v1,BASE64_HMAC v1a,key-2026-08:BASE64_ED25519Verifiers for one scheme skip the other scheme's tokens rather than failing on them. Without that, dual-signing — the entire migration path — breaks both verifiers. There are tests on both sides of it.
New §5.3.1, including a rotation rule with a concrete bound: keep the outgoing key published for at least the §5.4 retry span (~6 hours), so a delivery signed before a rotation still verifies when its last retry lands. That interaction is easy to miss and produces failures hours after the rotation looks complete.
signing_jwks_urlon the manifest, REQUIRED when signing asymmetrically.WebhookDispatcherdual-signs when a key is configured; byte-identical behaviour otherwise.Scope
Checklist
signingPrivateKeyproduces exactly the header it did before. HMAC remains fully supported.Verification
@eep-dev/signer@eep-dev/middlewaretests/tests/cross-impl/codegen-schema-types --checkcheck-draft-schema-parityNotes for reviewers
One rule stated explicitly because it is a tempting shortcut: publishers MUST NOT reuse a
delivery_secretas an Ed25519 key, or derive one from the other. The security properties differ precisely because the subscriber may hold the symmetric key and must never hold the private one — deriving would silently hand every subscriber the publisher's signing identity.Robustness choices worth a look:
falserather than throwing; signature bytes are attacker-controlled. Tested for empty, truncated, non-base64, and unknown-scheme headers.Not done here, flagged rather than skipped:
eep-signerparity — the TS and Python signers now differ in capability./.well-known/jwks.jsonroute in the middleware.toJwks()renders the document, but wiring a route needs a key-management story (where the private key lives, how rotation is triggered) that I did not want to invent inside this PR.