KeypairType is now 'dilithium65' | 'dilithium87'. Upstream's four types are
gone with their primitives, not merely unoffered. Each falls to Shor's
algorithm, and a keyring that can hold such a key invites someone to keep funds
under it, inside a tool whose premise is that this is unsafe.
This was deferred until post-quantum signing was proven end to end (real
transfers on Heisenberg, a real mainnet wallet in the extension), so that
tearing out half of util-crypto could not muddy the diagnosis of a first
rejected extrinsic. That has happened.
util-crypto, removed:
- ed25519/, sr25519/, secp256k1/, and signature/ (signatureVerify, which only
knew those three; dilithiumVerify is the verifier);
- hd/ethereum and hd/ledger;
- key/fromPath and keyHdkd{Ecdsa,Ed25519,Sr25519}, the junction derivation;
- address/derive (sr25519 soft derivation);
- mnemonic/toMiniSecret, Substrate's classical seeding.
util-crypto, kept because none of it holds a key:
- ethereumEncode, isEthereumAddress and isEthereumChecksum. @polkadot/types and
the identicon renderer format 20-byte addresses with them, and a wallet has
to be able to show an Ethereum address to recognise and refuse one.
ethereumEncode now refuses a secp256k1 public key, saying why.
- evm ↔ substrate address conversion (its blake2/keccak hasher moved out of
secp256k1/ into address/), derived and multi addresses, BIP39, and suri
parsing.
keyring:
- Only ML-DSA arms remain. The default type is dilithium65.
- Every entry point (constructor, createFromUri, createFromPair, addFromAddress
and, above all, createFromJson for a backup the user chose) refuses a
quantum-unsafe type with "<type> keys are not quantum-safe and cannot be held
here", not "unknown crypto type", which reads like a bug in this software.
- pair.verify handles ML-DSA, a bare signature or signature ‖ publicKey, under
a context that defaults to the empty raw-bytes one. Derivation and VRF
refuse.
- The test keyring is the Quantus dev accounts (crystal_alice, dilithium_bob,
crystal_charlie: ML-DSA-87 from seeds of 0, 1 and 2) in place of sr25519
Alice…Ferdie and ethereum Alith…Faith.
Fixed along the way: addFromAddress passed the decoded address as a public key.
That is the same bytes on Substrate. Here it is the account id, a hash of the
key, so every watch-only account reported the hash of its own address. It now
carries the address as an account id.
Specs:
- Upstream's per-scheme keyring specs (index, pair, encode, decode, toJson,
vrf, suri, testingPairs) are replaced by keyring.spec.ts. It covers refusals
at every entry point (including a polkadot{.js} ed25519 JSON backup), the
dev-account addresses, watch-only addresses, JSON round trips for both
schemes, verify in both signature forms and failing under the wrong context
or signer, and the absence of derivation and VRF.
- The "classical paths unchanged" pins in the ML-DSA specs are gone with the
paths.
- The BIP39 vectors toEntropy.spec used moved from sr25519/ to
mnemonic/bip39Vectors.spec.ts.
yarn test: 2747 passing, 0 failing. yarn lint: clean.
hw-ledger and hw-ledger-transports remain. They are device transports holding
no primitives, and nothing consumes them.
Closes#6. Refs #1: signatureVerify is gone rather than made to take a
context; dilithiumVerify is the replacement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
Replaces the `link:../wasm/packages/quantus-crypto/build` resolution with an
npmScopes entry routing @quantus to https://git.lair.cafe/api/packages/quantus/npm/.
Routing the whole scope is safe here because we own every name in it. The forked
@polkadot packages cannot be handled the same way — most of that scope is
unforked and lives on npm — which is why downstream repos pin those to tarball
URLs instead.
The symlink approach had run out of road. node resolves a portal or link to its
realpath, so a consumer's transitive dependencies then resolve inside the *linked
package's* tree, where workspace entries point at source directories with no
exports map. That cost one workaround here and four in quantus/ui.
Reads from the registry are anonymous; publishing uses
`pass gitea/package-publisher-token`. Note that a version cannot be overwritten
in place — npm refuses — so republishing is delete-then-publish, which that token
can also do.
Refs quantus/extension#2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
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