Signing: choose the context from specVersion, and return sig ‖ pk for raw bytes #4
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #1. Depends on quantus/common#5.
Extrinsic signing
packages/extension-base/src/background/RequestExtrinsicSign.tsis three lines:Everything under that is already correct for Quantus —
ExtrinsicPayload.signblake2-hashes the payload when it exceeds 256 bytes, exactly asunchecked_extrinsic.rsdoes, and prepends the type byte becauseMultiSignatureis anEnum. What is missing is the signing context.The context depends on the runtime: spec ≥ 148 signs under
"QUANTUS_EXTRINSIC", earlier specs under the empty context.this.payload.specVersioncarries it, so this class is the right place to make the choice — but the context then has to travel throughExtrinsicPayload.sign(pair)down topair.sign(), and that call passes no options today. Getting the plumbing right, or working around it, is the substance of this issue.Signing under the wrong context produces a perfectly valid signature that the chain rejects, with nothing local able to tell the difference. Do not guess a default — see quantus/common#5.
Raw bytes signing
RequestBytesSign.tsreturns a bare signature:For Quantus that is unverifiable by anyone. The account id is a hash of the public key, so a verifier given only an address and a signature cannot recover the key to check it — which is precisely why the chain's own signature type carries
sig ‖ pk. Raw signing must do the same.That is an interface change visible to dapps, so decide and document what a Quantus
signRawreturns: whether it carries the variant byte as well, and how a verifier is expected to use it. Note thatu8aWrapByteswrapping and the ML-DSA context are two different domain-separation mechanisms doing overlapping jobs — state which one is authoritative rather than applying both by accident.Upstream recently reworked this area (
e66446ed, "derive raw vs extrinsic signing from the request channel"), so rebase before starting.Acceptance
balances.transfer_keep_aliveis accepted by a node running spec ≥ 148signRawresult verifies against the signer's address without the caller needing the public key from elsewhereDone, on
quantus-accounts(7fa26252). 79 tests pass; typecheck, lint andbuild:chromeclean.The plumbing question this issue posed
Answer: don't use that helper. It is two lines, and they are reproduced here with the context added —
Forking
@polkadot/typesto thread an option through was the alternative, and is a much larger commitment for the same result. The 256-byte BLAKE2b rule is Substrate's own, fromunchecked_extrinsic.rs; signer and runtime must apply it identically or nothing verifies, so it is spelled out rather than inherited.Before this, the extension did not merely sign wrongly — it threw, because the keyring refuses to sign an ML-DSA pair without a context (quantus/common#5).
Raw bytes
Empty context, deliberately not the extrinsic one. A signature made for a dapp login must never be replayable as a transfer, and the context is bound into the signature itself.
This issue asked which of
u8aWrapBytesand the context is authoritative. The context is.u8aWrapBytesstays — it is polkadot-js's<Bytes>…</Bytes>convention, kept so a verifier written against it still sees what it expects — but it is an in-band wrapper a verifier can strip or forget, where the context cannot be.No Quantus-specific bytes context is invented. Nothing in the ecosystem defines one; the chain and the SDK name only
QUANTUS_EXTRINSIC, and making one up would produce signatures no other Quantus tool could verify.Raw signing returns
sig ‖ pk— not a convenience, since the account id is a one-way hash and a verifier holding only an address cannot recover the key.Acceptance
1for ML-DSA-65, thensig ‖ pkat the fixed lengthQUANTUS_EXTRINSICand fails under the empty context — and the converse at spec 147signRawresult verifies against the signer's address with nothing else suppliedThe one criterion this cannot close by itself: "a signed
balances.transfer_keep_aliveis accepted by a node running spec ≥ 148". Everything here says the bytes are right; only a node can say they are accepted. That is #7 tier 1.Note on the earlier rebase warning
This issue said to rebase before starting, because upstream reworked the area in
e66446ed("derive raw vs extrinsic signing from the request channel"). That rework is already in the fork's base —Request/index.tsxbranches onisExtrinsicRequest(request)rather than on payload shape — so there was nothing to reconcile.Verified against
main, closing.specVersion:RequestExtrinsicSignusescontextForSpec, rebuilt on@quantus/codecin34759485.transfer_keep_alivesigned under the spec-148 context was included on Heisenberg, at block 1050475 (be691499) and again at 1050581 after the extension was rewired (34759485).1010: bad signature(tier-1 case,4f759c40), andRequestExtrinsicSign.spec.tscovers it too.signRawreturnssig ‖ pkunder the empty context and verifies from the address alone. The specs also check that it does not verify under the extrinsic context.The on-chain proof went through the tier-1 script, which uses the same codec, keyring and context. An extension-popup-signed extrinsic seen in a block is still owed, under #7 tier 2 and qapi#1.