Commit Graph
1980 Commits
Author SHA1 Message Date
rob thijssenandClaude Opus 5 f7dc72e32c feat(keyring,util-crypto): BIP44 derivation for ML-DSA, in place of junctions
createFromUri now derives ML-DSA pairs from the mnemonic along a hardened BIP44
path rather than from a seed along a junction chain. Lattice keys have no public
derivability, so there is no soft junction to emulate and the chain's own
Pair::derive refuses for the same reason.

The path is m/44'/189189'/<account>'/0'/<0 for 87 | 1 for 65>'. Two things about
it are unusual and both are deliberate: the account index sits at the third level
rather than the last, and the trailing index carries the scheme rather than an
address index. It is what quantus-cli and the mobile wallet already derive, so
anything else produces addresses no other Quantus tool can find.

The ML-DSA branch sits before the existing seeding, not after, because that
seeding is the wrong one for us. mnemonicToMiniSecret is Substrate's variant and
the default reach in this file; using it here yields a perfectly well-formed key
for an account nobody owns. We need the 64-byte BIP39 seed and the HMAC-SHA512
chain keyed with "Dilithium seed", which is what the hdwallet crate does.

Accepted suri forms are nothing (account 0, matching quantus-cli's default),
//<n> for an account index, and //m/44'/… for a full path. Everything else
throws. That matters more than it looks: the syntax was built for curve
junctions, where /foo is soft and //foo hard over arbitrary bytes, and quietly
reinterpreting //Alice as something would hand back an address no other tool
derives and no seed phrase obviously recovers.

A raw hex seed is taken undederived — that is how the dev-genesis accounts are
defined — and combining one with a derivation path is refused rather than
guessed, since whether such a seed is the master or already derived is
unanswerable.

pair.derive() refuses with its own message. Not "not implemented" but not
possible: a child would have to come from this pair's key material, and ML-DSA
keys are not derivable from one another at all. The caller needs the mnemonic, so
the error says so rather than being generically unhelpful.

Verified against quantus-cli for both schemes from the public Substrate dev
phrase, plus index separation, explicit paths agreeing with indices, passphrase
sensitivity, and every refusal.

Refs quantus/common#4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 08:56:20 +03:00
rob thijssenandClaude Opus 5 506b77351c fix(keyring): make account backup and restore work for ML-DSA pairs
Before this, an exported Quantus account could not be imported by anything.
decodePair located its divider by trying the two secret lengths every curve
scheme uses, 64 then 32; an ML-DSA secret is 4032 or 4896 bytes, so neither
matched and restore threw "Invalid encoding divider found in body". That is a
data-durability bug rather than a convenience one, which is why it lands before
any build lets a user create an account.

decodePair now takes an optional secretLength. The caller passes it rather than
this function searching for the divider: searching would work almost always and
fail catastrophically when it did not, since PAIR_DIV is five bytes and a
4032-byte secret contains a false match about once in 270 million keys — and the
result would be a silently wrong key rather than an error. The public key is read
as the remainder, because its length also varies and the body ends there.

The harder half is ordering. createFromJson returns a *locked* pair and callers
read pair.address off it long before any password appears. Every other scheme
manages because the address is the public key; an ML-DSA account id is a one-way
Poseidon2 hash and the public key is inside the encrypted blob. So PairInfo gains
an optional accountId, carried as data and used for the address while locked,
which decodePkcs8 clears once the real key arrives.

It also checks them against each other, and that check is not paranoia. For the
curve schemes a JSON file with an edited `address` field cannot decode at all.
Here it decodes perfectly and yields a pair reporting an address its key does not
control — a user would see someone else's address in their own wallet and believe
they held it. Pinned by a test that tampers exactly that field.

One more silent-wrong-key path closed: decodePkcs8 decided "secret key or seed?"
by length, so a 4032-byte ML-DSA secret took the seed branch and was fed to keygen
as entropy, producing a valid and entirely wrong key. The type knows the answer,
so it is asked.

Refs quantus/common#3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 08:50:56 +03:00
rob thijssenandClaude Opus 5 2bffbc62bc feat(keyring,util-crypto): ML-DSA arms for createPair, alongside the curve ones
Wires @quantus/crypto into the four TYPE_* maps, so a keyring pair of type
dilithium65/dilithium87 can be created, addressed, signed with and verified.
The curve arms are untouched: quantus/common#6 removes them, but only once
post-quantum signing is proven end to end, so that a rejected first extrinsic has
two suspects rather than three.

util-crypto gains thin dilithium/ wrappers so keyring composes primitives the way
it already does for ed25519, rather than reaching into @quantus/crypto directly.

TYPE_ADDRESS is the interesting arm. Every other entry is identity or a cheap
re-encoding because on Substrate the address *is* the public key; ML-DSA takes
1952 or 2592 bytes to 32 with no way back. dilithiumVerify therefore mirrors the
runtime's Verify::verify rather than inventing anything: split sig||pk, hash the
embedded public key, check it equals the account being verified against, and only
then check the signature. Skipping the account check would accept a valid
signature from *some* key rather than *the* key, which is the whole property.

TYPE_PREFIX gets 0 for dilithium87 and 1 for dilithium65. For the curve types
those index Substrate's MultiSignature; here they index the runtime's
DilithiumSignatureScheme — a different enum reached by the same withType
mechanism, so nothing upstream of createPair needs to know.

Signing takes a mandatory context, which is quantus/common#5 arriving early
because the two are inseparable: TYPE_SIGNATURE cannot have an ML-DSA arm without
one. There is deliberately no default. Extrinsics on spec >= 148 verify under
QUANTUS_EXTRINSIC and earlier ones under the empty context, and the wrong choice
is a valid signature the chain rejects, indistinguishable locally. Only the
caller knows the spec version.

The curve sign functions are wrapped rather than passed by reference: ed25519Sign
and sr25519Sign take a third argument of their own (onlyJs), and letting it line
up with the context slot would silently reinterpret a Uint8Array as a boolean.

VRF signing refuses for ML-DSA. The construction fakes a VRF from an ordinary
signature, which is sound only given properties ML-DSA's signature does not have;
producing something that looks like a VRF proof and cannot be verified as one is
worse than refusing.

Two runtime whitelists sat behind the KeypairType union and silently rejected the
new types — the Keyring constructor and createFromJson. The compiler cannot see
those, and the first failed at construction with a message listing four types.
Both now use one named KEYPAIR_TYPES so the next change to the union has a single
place to look and the message cannot drift from the check.

Verified against quantus-cli, not against ourselves: keyring.addFromSeed with 32
zero bytes yields crystal_alice's address, signs to the runtime's wire length,
verifies under the extrinsic context and fails under the spec-147 one, and
another account's signature is rejected for it.

Refs quantus/common#2, quantus/common#5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-10 18:43:25 +03:00
rob thijssenandClaude Opus 5 6ed8b1ea6d feat(util-crypto,networks): open KeypairType to ML-DSA, register Quantus
Adds dilithium65 and dilithium87 to KeypairType, and Quantus to the network
registry at SS58 prefix 189.

Two types rather than one because the parameter set is not an implementation
detail: it selects the signature enum variant byte the chain reads off the wire,
it selects the trailing hardened index of the derivation path, and it fixes the
key and signature lengths. New accounts use dilithium65; dilithium87 is legacy,
used by accounts predating the recorded scheme and by the dev-genesis accounts,
so it is supported but never chosen.

Opening the union turns every table keyed by KeypairType into a type error,
which is the point — it enumerates precisely the places needing a post-quantum
answer. Where that answer is not written yet, assertSubstrateType() expresses the
gap as a named, typed refusal at the call site rather than a silent undefined
from an object index. It throws rather than degrading because all of these paths
produce key material or an address, and a wrong-but-plausible value means funds
at an address nobody controls. Call sites disappear as quantus/common#2 and #4
land. keyFromPath throws for the same reason: silently ignoring a derivation
path would hand back the parent key under a child's name.

Quantus is appended to the registry rather than merged — @substrate/ss58-registry
does not carry it and nothing occupies prefixes 185..195, so there is nothing to
override, and appending means a registry bump cannot silently drop it.
standardAccount is 'ml-dsa', a value upstream's closed union does not allow,
hence the widened KnownSubstrate; it is load-bearing, since a missing
standardAccount makes toExpanded mark the network isIgnored.

Testnets are first-class in this fork. Upstream drops any testnet from
availableNetworks however complete its entry is, which suits a production chain
picker and not us: Quantus is under active development and a wallet that cannot
reach Heisenberg can only be tested against real funds. The exception is an
allowlist (exposedTestnet) rather than removing the rule — removing it exposed
all fifteen of upstream's testnets too, moving the sorted tail from 'ZERO' to
'ZERO Alphaville', which is a change we have no reason to make. isTestnet is
still reported honestly so consumers can label or filter; we just stop deciding
for them.

Genesis hashes come from blackbeard.observer's config, which records their
provenance: mainnet read from the node with chain_getBlockHash(0) on 2026-09-09,
its launch day, and explicitly not the staging chain that telemetry calls
"Quantus Staging Mainnet". Symbol and decimals (QTC, 12) agree between the
chain's chain_spec.rs and the Dart SDK's AppConstants.

Three upstream tests encoded rules this fork no longer follows. Each is rewritten
to assert the new rule rather than deleted:
  - genesis entries for testnets are now allowed, for ones we opted into
  - an exposed testnet must still be labelled a testnet, since a user cannot tell
    play money from real money by looking at an address
  - one network per SS58 prefix stops holding when a chain has a testnet;
    mainnet and Heisenberg are both 189 because an address is valid on either,
    and consumers key on genesisHash rather than prefix

Refs quantus/common#1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-10 14:50:49 +03:00
github-actions[bot] 9281daa14c [CI Skip] release/stable 14.0.3
Lock Threads / lock (push) Has been cancelled
skip-checks: true
2026-03-23 12:53:26 +00:00
Tarik Gul c7ed69fe55 14.0.3 (#2025) 2026-03-23 14:48:13 +02:00
github-actions[bot] 22a1246672 [CI Skip] bump/beta 14.0.3-0-x
skip-checks: true
2026-03-22 21:42:26 +00:00
github-actions[bot] 152d4609a4 [CI Skip] release/stable 14.0.2
skip-checks: true
2026-03-16 18:24:20 +00:00
Tarik Gul 4ab957e568 14.0.2 (#2023) 2026-03-16 20:19:11 +02:00
github-actions[bot] 610c8bb15f [CI Skip] bump/beta 14.0.2-1-x
skip-checks: true
2026-03-13 20:55:22 +00:00
github-actions[bot] a8732a99aa [CI Skip] bump/beta 14.0.2-0-x
skip-checks: true
2026-03-12 16:59:11 +00:00
Tarik Gul 8d3cb0eb6b Set headers to 2026 (#2021) 2026-03-12 18:53:56 +02:00
github-actions[bot] fe0886be23 [CI Skip] release/stable 14.0.1
skip-checks: true
2025-12-09 08:34:53 +00:00
rajk93 bac357ae10 14.0.1 (#2014)
* 14.0.1

* chore: update CHANGELOG.md
2025-12-09 14:01:18 +05:30
github-actions[bot] 4ab7a5bfe0 [CI Skip] bump/beta 13.5.10-1-x
skip-checks: true
2025-12-09 07:34:50 +00:00
github-actions[bot] ae0735ccda [CI Skip] bump/beta 13.5.10-0-x
skip-checks: true
2025-12-08 13:13:55 +00:00
github-actions[bot] bf63a0ebf6 [CI Skip] release/stable 13.5.9
skip-checks: true
2025-11-25 08:17:11 +00:00
rajk93 d78344d000 13.5.9 (#2012) 2025-11-25 13:43:32 +05:30
github-actions[bot] 169a702c88 [CI Skip] bump/beta 13.5.9-0-x
skip-checks: true
2025-11-25 08:02:38 +00:00
github-actions[bot] 21ef853fbd [CI Skip] release/stable 13.5.8
skip-checks: true
2025-11-11 06:04:48 +00:00
rajk93 a4d940cb69 13.5.8 (#2010) 2025-11-11 11:31:13 +05:30
github-actions[bot] 6423537dbe [CI Skip] bump/beta 13.5.8-0-x
skip-checks: true
2025-11-11 04:05:17 +00:00
github-actions[bot] 7d77f89f34 [CI Skip] release/stable 13.5.7
skip-checks: true
2025-10-13 14:54:53 +00:00
Valentin Fernandez bee7204f95 13.5.7 (#2008) 2025-10-13 11:51:23 -03:00
github-actions[bot] bb0715ec2f [CI Skip] bump/beta 13.5.7-0-x
skip-checks: true
2025-10-13 14:24:21 +00:00
Valentin Fernandez 2212f31f9a Revert "Set pbkdf2Encode rounds to default to 210,000 (#1983)" (#2007)
This reverts commit e5e6bdd814.
2025-10-13 11:20:32 -03:00
github-actions[bot] 047840319e [CI Skip] release/stable 13.5.6
skip-checks: true
2025-08-26 16:49:24 +00:00
Arjun Porwal 3fbf0b98c2 13.5.6 (#2004) 2025-08-26 13:45:40 -03:00
github-actions[bot] e6b0411e9c [CI Skip] bump/beta 13.5.6-2-x
skip-checks: true
2025-08-26 14:00:47 +00:00
github-actions[bot] 8ca4525570 [CI Skip] bump/beta 13.5.6-1-x
skip-checks: true
2025-08-22 14:13:55 +00:00
github-actions[bot] 9c1be4f19b [CI Skip] bump/beta 13.5.6-0-x
skip-checks: true
2025-08-20 15:59:22 +00:00
Tarik GulandValentin Fernandez e5e6bdd814 Set pbkdf2Encode rounds to default to 210,000 (#1983)
* Set pbkdf2Encode rounds to default to 210,000

* Cap amount of rounds to 2048 when onlyJS = true to prevent overflow issues.

* Patch tests to use a consistent amount of encoding rounds.

* lint

* Add additional docs

* lint

---------

Co-authored-by: Valentin Fernandez <tinchofernandez8@gmail.com>
2025-08-20 12:55:25 -03:00
github-actions[bot] 55a3201bd5 [CI Skip] release/stable 13.5.5
skip-checks: true
2025-08-11 15:47:48 +00:00
Valentin Fernandez de2538c18d 13.5.5. (#2000) 2025-08-11 12:44:05 -03:00
github-actions[bot] e8dced9fed [CI Skip] bump/beta 13.5.5-0-x
skip-checks: true
2025-08-04 17:54:26 +00:00
github-actions[bot] 5a04d67bb2 [CI Skip] release/stable 13.5.4
skip-checks: true
2025-07-28 16:16:48 +00:00
Arjun Porwal 41481c1e3a 13.5.4 (#1999) 2025-07-28 21:43:02 +05:30
github-actions[bot] 8a8b583f81 [CI Skip] bump/beta 13.5.4-0-x
skip-checks: true
2025-07-28 15:59:37 +00:00
github-actions[bot] f9ee358442 [CI Skip] release/stable 13.5.3
skip-checks: true
2025-07-01 15:36:43 +00:00
rajk93 ef5cfc728c 13.5.3 (#1996) 2025-07-01 12:32:55 -03:00
github-actions[bot] 88b8a54018 [CI Skip] bump/beta 13.5.3-2-x
skip-checks: true
2025-07-01 14:46:07 +00:00
github-actions[bot] daabdfac45 [CI Skip] bump/beta 13.5.3-1-x
skip-checks: true
2025-06-30 10:23:33 +00:00
github-actions[bot] df044669e2 [CI Skip] bump/beta 13.5.3-0-x
skip-checks: true
2025-06-18 16:10:04 +00:00
github-actions[bot] 5235009f4c [CI Skip] release/stable 13.5.2
skip-checks: true
2025-06-17 14:57:41 +00:00
Valentin Fernandez 21bfff5a51 13.5.2 (#1992) 2025-06-17 11:54:01 -03:00
github-actions[bot] 963025daab [CI Skip] bump/beta 13.5.2-2-x
skip-checks: true
2025-06-17 04:38:45 +00:00
github-actions[bot] ef94fed1fe [CI Skip] bump/beta 13.5.2-1-x
skip-checks: true
2025-06-13 13:20:09 +00:00
github-actions[bot] 2bc18cfdbb [CI Skip] bump/beta 13.5.2-0-x
skip-checks: true
2025-05-26 12:10:45 +00:00
github-actions[bot] 0ae44ed34e [CI Skip] release/stable 13.5.1
skip-checks: true
2025-05-19 15:50:11 +00:00
Tarik Gul 09d54ea200 13.5.1 (#1988) 2025-05-19 18:46:33 +03:00