Repository navigation
Verify signed push deliveries - #22
Open
akselkvitberg wants to merge 7 commits into
Open
akselkvitberg wants to merge 7 commits into
akselkvitberg wants to merge 7 commits into
Conversation
NewDeliveryVerifier(...).Verify checks the X-PubSub-Token header that bcc-pubsub sends with every push: ES256 only, key from the issuer's JWKS, issuer, audience (your client id), expiry and issued-at with 60 seconds of skew, required claims and the SHA-256 body hash. Same rules and reason codes as the .NET and Node SDKs, checked with their shared test vectors. RequireDelivery is net/http middleware: 401 for a push without a valid token, 503 when the keys cannot be fetched, and the verified body and claims for the handler. Standard library only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TBbSY7a3FBE2CZNvFF44QH
The key set is now an immutable snapshot behind an atomic pointer. A request that finds its key in a fresh cache returns at once; only a request that needs a fetch takes the fetch mutex, and it re-reads the snapshot after getting it so queued requests use the result of the fetch that just finished. The concurrent test no longer sleeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The key set fetch is shared by all waiting requests, so it now runs on a context detached from the request that started it and is bounded by its own 10 second timeout (also for a caller-supplied HTTPClient without a Timeout). Before, a cancelled request failed the fetch and still set the refresh back-off, blocking the next fetch for a minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RequireDelivery returned 413 for every body read error. It now does so only for http.MaxBytesError; other read errors get 400. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Callers can now errors.Is/As the underlying cause of a failed key set fetch as well as ErrDeliveryKeysUnavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Header and payload are now decoded as JSON objects first, and individual members are typed only where they are used. A non-string alg gives invalid_signature and a non-string kid gives unknown_key, as before the key lookup in the other SDKs; non-JSON or non-object segments stay malformed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same change as the .NET and Node verifiers: when the key id is unknown and the latest JWKS fetch failed, return ErrDeliveryKeysUnavailable (5xx, retried) instead of rejecting with unknown_key. A rotated key is no longer rejected just because the refresh failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TBbSY7a3FBE2CZNvFF44QH
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
Part of bcc-code/bcc-pubsub#56. Go counterpart of the .NET and Node verifiers in bcc-code/bcc-pubsub#181 and bcc-code/bcc-pubsub#192.
bcc-pubsub is starting to sign every push with an
X-PubSub-Tokenheader. The token tells the receiver that the push came from bcc-pubsub, was meant for its client, and has the body bcc-pubsub sent. This PR lets Go services check it.What it does
NewDeliveryVerifier(DeliveryVerifierOptions{Issuer, Audience})creates a verifier.Verify(ctx, token, body)returns the claims (subscription or dispatch, message id, issue time), or:*DeliveryVerificationErrorwith aReason(wrong_audience,expired,body_mismatch, …) when the push must be rejected with 401;ErrDeliveryKeysUnavailablewhen bcc-pubsub's public keys can't be fetched, so the service answers 5xx and bcc-pubsub retries.RequireDelivery(verifier, maxBodyBytes, next)isnet/httpmiddleware. It answers 401 to a push without a valid token, without reading the body when the header is missing, and 503 when the keys are unavailable. It hands the verified body and claims (DeliveryClaimsFrom) to the handler. Other routers can callVerifydirectly.DeliveryIssuerProd,DeliveryIssuerSandboxandDeliveryIssuerDev. The token's issuer is thepubsub.api.*URL, which differs from theapi.bcc.no/pubsubgateway URLs the client uses for publishing, so it can't be taken from the client.pubsubsdk/testdata(Scripts/update-test-vectors.shrefreshes them).crypto/ecdsa,crypto/ecdhto check the key is on the curve). No new dependency, and it still builds withgo 1.21.net/httpexample. The README was empty before.Existing users
Nothing changes for code that only publishes or manages subscriptions. The verifier is new API that nothing calls unless a service chooses to.
Go-live
RequireDeliveryrejects every push.Follow-up fixes
After comparing with the .NET and Node verifiers:
RequireDeliveryanswers 413 only for a too large body; other read errors give 400.errors.Is/Aswork on it).alg/kidgiveinvalid_signature/unknown_key, as in Node, instead ofmalformed.time.Sleep.Part of the .NET parity work with #24. #23 raises
goin go.mod to 1.26; this PR still builds with 1.21.Verification
go test ./... -race: all pass. That covers the 14 shared test cases, refetching on a new key, the refetch rate limit, one fetch for 20 concurrent requests, unavailable keys, keeping cached keys on a failed refresh, missing claims, and the middleware (valid push, missing token, wrong audience, changed body).go vet ./...is clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01TBbSY7a3FBE2CZNvFF44QH
Generated by Claude Code