feat(varsig): put the signing context in the payload, not the challenge - #37
Merged
Conversation
The challenge was SHA-256(label ‖ payload), which separated the three signing contexts but meant it was no longer the hash of the payload. The varsig WebAuthn draft has a verifier re-hash the payload and compare against the challenge (step 5), so nothing outside this package could ever reproduce it. Measured against the draft: no multihash prefix, and a different hash preimage. The label is not a mistake, though. The same credential signs an identity, a public key and an oplog entry, and a signature over one must not be reusable as a signature over another. That separation cannot move into the varsig header either: the WebAuthn signature only covers authenticatorData ‖ SHA-256(clientDataJSON), so anything carried beside the challenge is unauthenticated. So the context moves into the bytes that are signed. bindContext() frames it length-prefixed ahead of the payload, and buildChallengeBytes() is now a plain SHA-256 multihash of those bytes. Both properties hold: the three contexts still produce different challenges, and a spec verifier can reproduce the digest. The length prefix matters — without it, context 'a' with payload 'bc' and context 'ab' with payload 'c' are the same bytes, and the separation could be sidestepped by choosing a payload that absorbs the label. identity.js is untouched; the three labels flow through as before, they just enter the calculation somewhere else. BREAKING: this changes the signed bytes, so entries written by earlier versions no longer verify. It belongs in the same release as the CURVE_ED25519 correction rather than shipping on its own, and it is a bet on a draft — ChainAgnostic/varsig#11 is still open with `webauthn-varsig-header = TODO`. Kept off main until that settles. Tests: 5 new cases pinning the multihash shape, step-5 reproducibility, context separation and frame unambiguity. 45 node tests pass; varsig e2e, focused and integration unchanged.
0.3.0 corrects the Ed25519 curve code, so `^0.2.0` no longer covers it. Verified before publishing by installing the 0.3.0 candidate from a local tarball: 45 node tests and the varsig e2e pass against it together with the challenge change on this branch. The lockfile is deliberately left at its previous state — 0.3.0 is not on npm yet, so `pnpm install` has to run once it is.
Completes fa24234, which raised the range but left the lockfile alone because 0.3.0 was not on npm yet. Verified against the published tarball: 45 node tests and the varsig e2e pass.
Merged
NiKrause
added a commit
that referenced
this pull request
Aug 15, 2026
* chore(release): 0.5.0 A major-breaking release: the bytes a credential signs change, so this is not an upgrade peers can do one at a time. Two breaks, one of them narrower than it looks. The varsig signing context moves out of the challenge and into the signed payload (#37), so the challenge is once more a plain SHA-256 multihash of what was signed and a spec verifier can reproduce it. And iso-webauthn-varsig 0.3.0 corrects the Ed25519 curve code from 0xed01 — the varint encoding of ed25519-pub, not the code itself — to 0xed. That second one leaves P-256 bytes untouched. Consequence for consumers: entries and identities written under 0.4.x no longer verify, and a relay that verifies identities has to move in the same step as the peers it serves. The CHANGELOG spells out the migration. Kept honest about the risk: this follows a draft that is still open (ChainAgnostic/varsig#11 has webauthn-varsig-header = TODO), so the wire format may move again. 45 node tests pass against the bumped version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: satisfy prettier in the 0.5.0 changelog entry Emphasis has to be _underscores_, not *asterisks*. `format:check` is a gate in Run Tests, so this failed the whole job — the tests themselves never ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Breaking: this changes the bytes that get signed. Entries written by earlier versions no longer verify. It also raises
iso-webauthn-varsigto^0.3.0, which itself changed the Ed25519 curve field.What was wrong
The challenge was
SHA-256(label ‖ payload)— a domain label mixed into the hash preimage. That separated the three signing contexts, but it meant the challenge was no longer the hash of the payload.The varsig WebAuthn draft has a verifier re-hash the payload and compare it against the challenge (step 5 of its procedure). Nothing outside this package could ever reproduce our value. Measured against the draft:
Two differences: no multihash prefix, and a different preimage.
Why the label is not simply a mistake
The same credential signs an identity, a public key and an oplog entry (
domain.js). A signature over one must not be reusable as a signature over another — a real property the draft does not provide.It cannot move into the varsig header either: the WebAuthn signature covers only
authenticatorData ‖ SHA-256(clientDataJSON), so anything carried beside the challenge is unauthenticated and forgeable.So the context moves into the signed bytes.
bindContext()frames it length-prefixed ahead of the payload;buildChallengeBytes()is now a plain SHA-256 multihash of those bytes.The length prefix matters: without it, context
'a'with payload'bc'and context'ab'with payload'c'are identical bytes, and the separation could be sidestepped by choosing a payload that absorbs the label.identity.jsis untouched — the three labels flow through as before, they just enter the calculation elsewhere.Verification
45 node tests and the varsig e2e pass, against the published
iso-webauthn-varsig@0.3.0. Five new cases pin the properties that are easy to lose: multihash shape, step-5 reproducibility, context separation, frame unambiguity, and that the payload stays recoverable.The judgement call
This is a bet on a draft.
ChainAgnostic/varsig#11has been open since 2023 andwebauthn-varsig-headeris stillTODOthere, so the header codepoint we emit (0x300001) remains a private-use value of our own choosing. If that discussion lands differently, this needs recutting.Merging is reasonable because nothing is in production and every consumer is ours — but it is a decision, not a routine bump.