Repository navigation
Conversation
authenticate() signs buildPartial(), so a field assigned after the call sits outside the signature while the server still verifies over it, and the request comes back UNAUTHENTICATED. Correct ordering was the only thing holding this together, and nothing in the type system or the tests checked it. buildAuthenticated() signs and builds in one step, leaving no builder to set a field on afterwards. All 62 call sites across 17 files move to it; the emitted bytes are unchanged. Two tests hold the ordering. BuildAuthenticatedTest verifies a real signature over the request with auth cleared, and its second case executes the failure the helper prevents. AuthenticateCallSiteTest fails if a new API reaches for authenticate() directly, allowlisting ReportingApi, the live violator being fixed alongside a wider reporting change, and BlobStorageApi, which passes authenticate into getBlobsRequest where it is assigned last under an existing test. Signing in a unit test needs real crypto, so the module picks up the ed25519 shadow that services/opencode already uses.
report() signed the builder while it still held only the description, then set the target oneof, so nothing identifying what was being reported — user, chat, message or blob — fell inside the signature. The server verifies over the whole request, so every report failed UNAUTHENTICATED. Building through buildAuthenticated() takes the signature after the oneof is assigned, which drops ReportingApi from the AuthenticateCallSiteTest allowlist.
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.
authenticate()signsbuildPartial()— the builder as it stands when the call is made. Any field assigned after it is outside the signature, while the server verifies against the whole request withauthcleared, so the call failsUNAUTHENTICATED. This is positional, applies to every API in the module, and nothing enforced it.The live bug
ReportingApi.report()signed the builder while it held only the description, then set thetargetoneof. Nothing identifying what was being reported — user, chat, message or blob — was inside the signature, so every report failed, whatever was reported.Audit
All 64 call sites, including the one outside
network/api/that a*/network/api/*.ktgrep misses (EventStreamingService.kt:69). I checked.apply {}blocks, builders passed to helpers, and conditional branches that set a field on only one path.ReportingApiwas the only violator.The same hazard exists in
sign()(services/flipcashandservices/opencode, identical code). I audited its 15 call sites too; all assign the signature last.The guard
The three options weighed:
authenticate()— included, but it only helps someone who reads it while writing the call, which is exactly when this mistake gets made.buildAuthenticated()signs and builds together, so there is no builder left to set a field on. Call sites are a wash in length:It takes
attachAuthas a lambda because the generated request types share nosetAuthsupertype. Emitted bytes are unchanged at every site that was already correct.Two tests keep it that way.
BuildAuthenticatedTestverifies a real Ed25519 signature the way the server does, and its second case writes out the hazard and asserts verification fails — the bug is reproducible in CI rather than only describable.AuthenticateCallSiteTestfails if a new API callsauthenticate()directly.What this doesn't close
authenticate()staysinternalrather thanprivate, becauseBlobStorageApipasses it intogetBlobsRequest, which assigns it last underGetBlobsRequestSigningTest. That is the one entry left in theAuthenticateCallSiteTestallowlist. Folding that helper intobuildAuthenticatedwould letauthenticate()go private and empty the allowlist, but it means rewriting a test that deliberately injects a fake auth, so I left it.sign()is left alone. It is called inside nested sub-builders during intent and action construction, where the signed unit often isn't the request being built, so the terminal shape doesn't transfer mechanically.