ML-DSA hashes a domain-separation context into the signature, so a signature made under one context does not verify under another — same key, same message, different answer. Quantus uses that deliberately: extrinsics are signed under "QUANTUS_EXTRINSIC", while litep2p node-identity signatures stay on the empty context so mixed-version Noise handshakes keep working. chain:primitives/dilithium-crypto/src/signing_context.rs spells out the reasoning.
There is a version boundary. Spec ≥ 148 verifies under "QUANTUS_EXTRINSIC"; earlier specs use the empty context. The SDK encodes this as:
KeyringPair.sign(message, options) currently takes { withType?: boolean }. The context is a third thing the caller must supply, it is not derivable from anything the keyring knows, and getting it wrong produces a signature that is cryptographically valid and rejected by the chain — the worst failure shape available, because nothing local can detect it.
So: extend the options type with a context, and do not give it a default that silently works. A Quantus pair asked to sign with no context stated should either refuse, or use the empty context and be documented as doing so — but it must not quietly assume "QUANTUS_EXTRINSIC", because then the spec-147-and-earlier case becomes undiagnosable.
Note that only the caller knows the spec version: it arrives in the SignerPayloadJSON the dapp sends. The keyring's job is to carry the context faithfully, not to guess it.
Acceptance
a signature made under "QUANTUS_EXTRINSIC" verifies with the runtime's verify_ml_dsa_65 and fails under the empty context
the context is length-checked at ≤ 255 bytes, as FIPS 204 requires
non-Quantus pairs ignore the option entirely and produce byte-identical output to upstream
Depends on #2. The caller side is quantus/extension#4.
## Why a context at all
ML-DSA hashes a domain-separation context into the signature, so a signature made under one context does not verify under another — same key, same message, different answer. Quantus uses that deliberately: extrinsics are signed under `"QUANTUS_EXTRINSIC"`, while litep2p node-identity signatures stay on the empty context so mixed-version Noise handshakes keep working. `chain:primitives/dilithium-crypto/src/signing_context.rs` spells out the reasoning.
There is a version boundary. **Spec ≥ 148 verifies under `"QUANTUS_EXTRINSIC"`; earlier specs use the empty context.** The SDK encodes this as:
```rust
pub fn context_for_spec(spec_version: u32) -> Option<&'static [u8]> {
(spec_version >= EXTRINSIC_MIN_SPEC).then_some(EXTRINSIC)
}
```
## The API problem
`KeyringPair.sign(message, options)` currently takes `{ withType?: boolean }`. The context is a third thing the caller must supply, it is not derivable from anything the keyring knows, and getting it wrong produces a signature that is cryptographically valid and rejected by the chain — the worst failure shape available, because nothing local can detect it.
So: extend the options type with a context, and **do not give it a default that silently works**. A Quantus pair asked to sign with no context stated should either refuse, or use the empty context and be documented as doing so — but it must not quietly assume `"QUANTUS_EXTRINSIC"`, because then the spec-147-and-earlier case becomes undiagnosable.
Note that only the caller knows the spec version: it arrives in the `SignerPayloadJSON` the dapp sends. The keyring's job is to carry the context faithfully, not to guess it.
## Acceptance
- [ ] a signature made under `"QUANTUS_EXTRINSIC"` verifies with the runtime's `verify_ml_dsa_65` and fails under the empty context
- [ ] the context is length-checked at ≤ 255 bytes, as FIPS 204 requires
- [ ] non-Quantus pairs ignore the option entirely and produce byte-identical output to upstream
Context has no default:SignOptions.context exists, and an ML-DSA pair refuses to sign without one (pair/index.ts, with a spec).
Extension supplies it: the extension passes contextForSpec(specVersion), and quantus/extension#4 showed chain acceptance under spec 148 and rejection under the empty context.
255-byte limit: enforced and tested in @quantus/crypto.
Curve pairs: their context-less wrappers drop the argument.
Small gaps, not worth holding this open for:
the context test here covers ML-DSA-87 only;
nothing here signs a curve pair with a context and compares the bytes.
Verified against `main` (`7f23d0a60`), closing.
- **Context has no default:** `SignOptions.context` exists, and an ML-DSA pair refuses to sign without one (`pair/index.ts`, with a spec).
- **Extension supplies it:** the extension passes `contextForSpec(specVersion)`, and quantus/extension#4 showed chain acceptance under spec 148 and rejection under the empty context.
- **255-byte limit:** enforced and tested in `@quantus/crypto`.
- **Curve pairs:** their context-less wrappers drop the argument.
**Small gaps, not worth holding this open for:**
- the context test here covers ML-DSA-87 only;
- nothing here signs a curve pair with a context and compares the bytes.
The curve pairs go away in quantus/common#6 anyway.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Depends on #2. The caller side is quantus/extension#4.
Why a context at all
ML-DSA hashes a domain-separation context into the signature, so a signature made under one context does not verify under another — same key, same message, different answer. Quantus uses that deliberately: extrinsics are signed under
"QUANTUS_EXTRINSIC", while litep2p node-identity signatures stay on the empty context so mixed-version Noise handshakes keep working.chain:primitives/dilithium-crypto/src/signing_context.rsspells out the reasoning.There is a version boundary. Spec ≥ 148 verifies under
"QUANTUS_EXTRINSIC"; earlier specs use the empty context. The SDK encodes this as:The API problem
KeyringPair.sign(message, options)currently takes{ withType?: boolean }. The context is a third thing the caller must supply, it is not derivable from anything the keyring knows, and getting it wrong produces a signature that is cryptographically valid and rejected by the chain — the worst failure shape available, because nothing local can detect it.So: extend the options type with a context, and do not give it a default that silently works. A Quantus pair asked to sign with no context stated should either refuse, or use the empty context and be documented as doing so — but it must not quietly assume
"QUANTUS_EXTRINSIC", because then the spec-147-and-earlier case becomes undiagnosable.Note that only the caller knows the spec version: it arrives in the
SignerPayloadJSONthe dapp sends. The keyring's job is to carry the context faithfully, not to guess it.Acceptance
"QUANTUS_EXTRINSIC"verifies with the runtime'sverify_ml_dsa_65and fails under the empty contextVerified against
main(7f23d0a60), closing.SignOptions.contextexists, and an ML-DSA pair refuses to sign without one (pair/index.ts, with a spec).contextForSpec(specVersion), and quantus/extension#4 showed chain acceptance under spec 148 and rejection under the empty context.@quantus/crypto.Small gaps, not worth holding this open for:
The curve pairs go away in quantus/common#6 anyway.