Repository navigation
Route gRPC to Connect and add safe bare-token verification - #26
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 17 encoding variants through Go stamped and bare authentication and the public hop verifier, sharing the existing Python and TypeScript fixture. Pin core PR #850 and synchronize its resource-owner vectors. Retain the saved merge of main after PR #25. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
antoinetoussaint-byte
added a commit
that referenced
this pull request
Oct 26, 2026
…ication PR #26 and PR #28 both reworked the module-side verifier. The merge keeps both behaviours rather than choosing one. #28's signed admission replaces the shared `hop/PROOF` secret everywhere, so `stamp.Context`, `workcontext.Authenticate`, Python `bound_token` / `authenticate` and TS `boundToken` / `authenticateClaims` lose their proof argument and remain identity-only readers. #26's typed refusals now cover those paths too: `stamp.Check`, Python `check` and TS `checkCall` refuse with `refusal.Reason` values, and the scheme gains `stamp_method`, `stamp_audience`, `stamp_window`, `stamp_advisory`, `stamp_replayed` and `stamp_store` for the admission failures #28 introduced. No reason names a proof, token, digest, nonce or claim. #26's `NewTokenVerifier` / `VerifyToken` survive as the bare-token path: a process that receives no hop calls supplies neither the hop public key nor a replay store, and `Verify` refuses with `configuration` because it has nothing to check an admission against. #28's advisory refusal stands: an admission carrying `x-codefly-hop-advisory` is refused by `stamp.Check`, so `Verify` never reads that header as identity. #26's Connect routing in the hop service and its generated-route validation merge unchanged, and its connect-listener test now asserts the signed admission for the procedure it arrives on instead of a shared secret. Every Go module keeps the single core pin v0.19.7-0.20261026092055-b23dc13543b2 (canonical base64url decoding). TypeScript gains `refusal.ts`, which holds `Reason` and `Unauthenticated` so `hop-call.ts` can refuse with the same codes without importing `work-context.ts` back; `work-context` re-exports both. The three client READMEs are updated for the merged construction and the new admission reason codes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
antoinetoussaint-byte
added a commit
that referenced
this pull request
Oct 26, 2026
…livery Security review r164 on 33c436b. Each blocker and major, and the #26 conflicts it listed. B3, and R1/R2 with it: the admission window is judged once, at the instant the call starts. grpc-go decodes a unary request fully before it runs the unary interceptor, so a module serving large or slow uploads refused admissions the authority had granted; Python's interceptors already ran before the body. Verifier.TapHandle, installed with grpc.InTapHandle, records the arrival instant when the stream is accepted and Verify judges against it. It reads no header and runs no cryptography, because it runs on the connection's I/O goroutine. A server installing no handle judges the window when Verify runs, as before, so nothing is admitted by its absence. The redundant re-checks after the key read and the reservation are gone in all three languages. A two-second clock allowance (stamp.Skew, Python and TypeScript SKEW) applies identically in Go, Python and TypeScript: an admission may be issued that far ahead of the verifier's clock and stays admissible that far past its expiry. Being issued further ahead has its own reason, stamp_issued, so a verifier running behind is no longer diagnosed as an expiry. The signed span is still capped at Lifetime exactly. Reservations are retained to expiry plus the allowance, which is sound because the window is judged at arrival: a replay can only be admitted if it too arrives inside the window, and inside the window the nonce is still retained. B2: a REST face transcoded into a module's gRPC or Connect listener cannot be admitted. The hop signs an http route as "VERB /path" — all a published http route tells it — while that listener's verifier observes the full procedure, and a module cannot verify at both layers because the second would consume the nonce again. Signing the procedure instead needs the published route to carry its producer's template-to-procedure mapping, and the hop-routes schema has no such field, so routing.Validate refuses the composition at hop startup (routing.ErrTranscodedREST) rather than refusing every REST call at runtime. A module serving HTTP natively, with no procedural face on the same service, is unaffected. B1, the replay half: libraries/hop/go/replaypg is the durable shared store the contract required and nothing delivered, over a store Codefly does deliver; INSERT ... ON CONFLICT DO NOTHING over a primary key is the atomic reservation, shared by every replica. The console composes its own Postgres for it (modules/console/services/store) and drops hop-verification/REPLAY_DIRECTORY, which asked for a shared volume the platform cannot mount and in practice meant a per-pod directory. NewVerifier now refuses a store that does not declare itself shared outside a local run (hop.ErrReplayStoreNotShared); NewFileReplayStore does not declare it, because an absolute path to per-pod storage is indistinguishable from a shared mount, and NewSharedFileReplayStore is how an operator asserts otherwise. R4 and the #26 conflict behind it: one identity-only reader per language. workcontext.Authenticate, Python authenticate and TS authenticateClaims are gone; AuthenticateToken / authenticate_token / authenticateToken remain. A reader that took a call's metadata read as though it admitted the call, and after the shared proof's removal it accepted any token plus a self-computed digest, so a caller fixing a build break by deleting the proof argument lost admission silently. A caller holding metadata now extracts the bound token with stamp.Context first: two visible calls, neither an admission. B1, the verification key, is NOT fixed and is stated as open. Nothing produces hop-verification/PUBLIC_KEY: `codefly config generate` makes random bytes only, and a configuration value template assembles its producer's own primitives, so neither can derive a public half from the hop's seed. The agreed direction is the authority publishing the hop's admission key through its own Keys, which the verifier already trusts, caches and re-reads on rotation, and which the Work Context trust set ignores because it filters on algorithm. Keys is part of interfaces/authority, so that is a contract change: proposed in the handbook first, and implemented by accounts alongside the local authority. Until then a run configures the key by hand, and libraries/hop/README.md, the console's manifest and the playground group all say so rather than implying a producer exists. The playground also gains its hop-verification group, and the README states the documents/records adoption order the review found unsequenced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
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.
This removes the authority-owned blockers for Runtime #311 and Tracing #103: a module's declared Connect listener now receives gRPC as well as Connect, issued bare tokens can be authenticated by the shared verifier, and verification failures expose stable caller-safe reasons.
The routing change uses the published Connect endpoint and its exact method inventory. gRPC/Connect overlap is refused as ambiguous. Existing audience, method-policy, transport, unknown-route and fail-closed refusals remain in place. Generated gRPC and Connect clients are tested against one Connect server through the hop, including denied calls.
Go
NewTokenVerifier/Verifier.VerifyToken, Pythonverify_token/verify_token_async, and TypeScriptverifyTokenreuse the stamped path's validation and key cache. Proof-free worker construction is supported; stamped verification stays refused without a configured proof. They verify signature, issuer, audience, validity and claim structure; stale keys wait for refresh and failed refreshes refuse. These APIs do not claim live revocation, method admission or nonce consumption. Consumers retain those gates where needed.Unusable key publications have a distinct safe
invalid_keyreason; transport read outages usekeys_unavailable. Exported typed reasons contain fixed codes/text and no token, key, claim value or diagnostic cause. Shared vectors cover forged, unsigned, expired, wrong-audience, wrong-issuer, unknown-key, lifetime, schema, attenuation and grant refusals. Stamp failures have distinct codes. Core remains the Go verifier; its pinned diagnostic categories are translated to fixed reasons with unknown categories refused as malformed.Encoding parity uses core PR #850 (
b23dc13543b28e1f8df65931fcead6f36d10699e): the shared decoder requires canonical unpadded base64url for both token segments. The shared fixture adds 17 cases for unused bits, padding, CR/LF, noncanonical protobuf and foreign JSON, exercised by Go stamped/bare authentication and all three public bare-token verifiers. Empty signatures retain thesignaturerefusal. Core's resource-owner fixture is synchronized with the new pin. Main through PR #25 (583cb190) is merged. Core merge and the final merge-commit repin are pending coordinator review.Classification: fixes at their owning integration seams. Reuses Core Inspector, the existing published-key refresh policy (including 943ae26), Codefly routing publication, shared host transport and generated clients. No new issuer, key cache or client generator.
Validation at
f0ad8b431e4c360a11d294e93af92b392f7e559c: all CI checks pass, including go vet and go test -race across all eleven Go modules, Python pytest (300 tests), and npm test (158 tests). The shared encoding cases agree across all three languages. Consumer adoption follows on the existing module PR branches.