Skip to content

Route gRPC to Connect and add safe bare-token verification - #26

Merged
antoinetoussaint-byte merged 7 commits into
mainfrom
feat/hop-connect-and-bare-verify
Oct 26, 2026
Merged

antoinetoussaint-byte merged 7 commits into
mainfrom
feat/hop-connect-and-bare-verify

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

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, Python verify_token/verify_token_async, and TypeScript verifyToken reuse 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_key reason; transport read outages use keys_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 the signature refusal. 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.

antoinetoussaint-byte and others added 6 commits October 26, 2026 04:22
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
antoinetoussaint-byte merged commit c46a1a4 into main Oct 26, 2026
15 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant