Skip to content

fix(sdk): BRC-104 response preimage encodes an empty body as 0 instead of -1, so every bodyless signed response fails verification - #550

Open
E-Jacko wants to merge 1 commit into
bsv-blockchain:mainfrom
E-Jacko:fix/brc104-empty-body-response-preimage
Open

E-Jacko wants to merge 1 commit into
bsv-blockchain:mainfrom
E-Jacko:fix/brc104-empty-body-response-preimage

Conversation

@E-Jacko

@E-Jacko E-Jacko commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

SimplifiedFetchTransport.writeGeneralResponsePayload encodes an empty HTTP response body as varint 0. BRC-104 requires -1. A conforming counterparty therefore signs a preimage this client cannot reproduce, and every signed response with no body fails verification — a bare 404, 204, or empty 401/403 all die with Signature is not valid.

What the spec requires

BRC-104 §6.7.3: "If the body is empty, specify a length of -1 in the payload."

BRC-104 §6.9, on HTTP response messages: "Body length + body bytes (or -1 if none)."

This repo's own specs/auth/brc103-mutual-auth.yaml response-payload table already states VarInt(len) or VarInt(-1); only the code and one summary line (also fixed here) disagreed.

What the code did

writer.writeVarIntNum(body.length)      // 0 when empty — spec says -1
if (body.length > 0) writer.write(body)

body is always an array (Array.from(new Uint8Array(await response.arrayBuffer()))), so an empty body reached this as length 0 and there was no -1 branch. The absent-value paths on the request side (AuthFetch.writeRequestBody, writeOptionalText) already write -1, and this package's own auth-express-middleware signs empty responses with -1 (buildResponsePayload), so the two halves of this repo could not verify each other's bodyless responses.

Independent confirmation against a conforming server

A conforming Rust implementation (bsv-auth-axum-middleware, serialize_response_payload) writes -1 for an empty body. Run end to end against such a server, an authenticated caller receiving a correctly signed bodyless 404 gets:

Error: Signature is not valid
    at ProtoWallet.verifySignature (.../wallet/ProtoWallet.js:203:23)
    at Peer.processGeneralMessage (.../auth/Peer.js:742:27)
    at SimplifiedFetchTransport.sendGeneralMessage (.../transports/SimplifiedFetchTransport.js:203:9)

Everything else in the same run verifies — non-empty signed responses round-trip fine — so the failure is isolated to the empty-body encoding. Also reproducible standalone against the published 2.6.1 artifact: the terminal varint of the response preimage for an empty body is 0 with no trailing bytes.

The fix

Encode -1 for an empty body, mirroring the middleware's response side. AuthFetch.parseAuthenticatedResponse already treats a non-positive length as "no body", so verified traffic parses identically — it just verifies now.

Regression tests pin the terminal varint to exactly -1 with nothing following it (with and without a preceding request id), and to true length plus bytes for a non-empty body. Checked against the pre-fix encoding: the empty-body assertions fail on the unfixed code (0 !== -1), so a reversion cannot stay green.

Compatibility note

A server that "fixed" this by signing 0 instead would move every conforming implementation off the spec — the correct side to change is this client's response verification preimage, which this PR does. Deployed pairs of this SDK talking to auth-express-middleware are unaffected for non-empty bodies (unchanged) and for empty bodies were already failing.

Sibling defect found while verifying this fix — deliberately NOT fixed here

The present-but-empty request body has the same class of bug on both sides of this repo, and the two currently agree with each other on the non-conforming encoding, so they must be fixed together, not in this PR:

  • AuthFetch.writeRequestBody's if (!body) guard is truthiness-based, so body: new Uint8Array(0) or [] skips the -1 branch and signs 0;
  • auth-express-middleware's writeBodyToWriter reconstructs the request preimage the same way (Buffer.alloc(0) from an empty raw body writes 0, never reaching its -1 fallback).

In-repo client↔server traffic matches today because both are wrong the same way; either one fixed alone would break the other, and both together are non-conformant against third parties. Happy to follow up with a coordinated PR for that pair if you want it.

BRC-104 §6.7.3 requires an absent or empty body to be encoded as a
length of -1, and §6.9 lists the response signature preimage's final
field as 'Body length + body bytes (or -1 if none)'. AuthFetch's own
request side already implements this (writeRequestBody and
writeOptionalText both write -1), but writeGeneralResponsePayload
encoded an empty body as 0 with no -1 branch. A conforming
counterparty therefore signs a preimage this client can never
reproduce, so every signed response with no body — a bare 404, 204 or
empty 401/403 — fails signature verification with 'Signature is not
valid'.

Regression tests pin the terminal varint to exactly -1 with nothing
following it for an empty body, and to true length plus bytes
otherwise; the empty-body case fails against the previous encoding.
AuthFetch's response reader already treats a non-positive length as
no body, so verified traffic is unchanged apart from now verifying.

Signed-off-by: Elis Jackson <elisjackson@icloud.com>
@sonarqubecloud

Copy link
Copy Markdown

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