feat(quantus-crypto): JS surface, build scripts and CSP-safe sync init
Wraps the crate for JS consumers and adds the build that produces it.
Init deliberately avoids fetch and avoids @polkadot/wasm-bridge. The consumer is
an MV3 service worker under `script-src 'self' 'wasm-unsafe-eval'`, which can
compile WASM but not usefully fetch it, and which can be cold-started between any
two messages; callers like pair.sign() are synchronous and have no await to give.
So the WASM is zlib-compressed and base64'd into bytes.js at build time and
instantiated with wasm-bindgen's initSync. Bridge is not usable here regardless:
it implements the 0.2.79 JS-heap ABI and this crate builds with 0.2.128, which
uses externref tables.
build-quantus.sh is separate from build-wasm.sh rather than folded into it,
because that script drives the nightly-2022-06-24 + xargo build wasm-crypto
needs. install-build-deps.sh gains a second wasm-bindgen for the same reason —
the two ABIs cannot share a binary. No asm.js step: wasm2js over ML-DSA would be
enormous and slow, and every context we ship into permits wasm.
bytes.js is emitted in both module systems, with the CJS copy under a directory
carrying its own {"type":"commonjs"} — the package is "type": "module" and node
otherwise refuses to load an exports.-style file from it.
Proven end to end against the real build output: base64 -> inflate -> initSync
with no fetch, crystal_alice's account id matching the CLI through the JS path,
sig||pk matching the runtime's fixed-array size, and JsError surfacing as a JS
exception across the boundary.
Sizes are read from the crate rather than exposed as constants to copy. They are
consensus-critical and a drifted JS constant would re-frame every byte after the
signature while looking entirely healthy.
Refs quantus/wasm#1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
68
packages/quantus-crypto/README.md
Normal file
68
packages/quantus-crypto/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# @quantus/crypto
|
||||
|
||||
Quantus post-quantum crypto for the browser: ML-DSA-65 and ML-DSA-87 signatures,
|
||||
Poseidon2-over-Goldilocks account-id hashing, and hardened BIP44 key derivation.
|
||||
|
||||
Every function delegates to the crates the Quantus runtime itself uses —
|
||||
`qp-rusty-crystals-dilithium`, `qp-poseidon-core`, `qp-rusty-crystals-hdwallet` —
|
||||
rather than reimplementing them. A browser wallet that disagreed with the chain
|
||||
about a key or a signature would produce perfectly well-formed output that the
|
||||
chain rejects, and nothing on this side could tell.
|
||||
|
||||
## Why a separate package from `@polkadot/wasm-crypto`
|
||||
|
||||
They cannot share a Cargo build. `wasm-crypto` is compiled with
|
||||
`nightly-2022-06-24` against a 2019-era dependency set; the ML-DSA crates use
|
||||
inline `const {}` blocks that require Rust >= 1.79. Modernising the older build
|
||||
would mean rewriting upstream's sr25519/ed25519 crypto, which is the thing most
|
||||
worth leaving untouched so rebases onto upstream stay boring.
|
||||
|
||||
What *is* shared is the packaging: the WASM is zlib-compressed and base64'd into
|
||||
the JS at build time, so nothing is fetched at runtime. That matters because the
|
||||
consumer is an MV3 service worker under `script-src 'self' 'wasm-unsafe-eval'`,
|
||||
which can compile WASM but cannot usefully fetch it, and because callers like
|
||||
`pair.sign()` are synchronous and have no `await` to give.
|
||||
|
||||
`@polkadot/wasm-bridge` is deliberately not used: its `Bridge` implements
|
||||
wasm-bindgen 0.2.79's JS-heap ABI, while this crate builds with 0.2.128, which
|
||||
uses externref tables. wasm-bindgen's own generated glue plus `initSync` is both
|
||||
smaller and correct.
|
||||
|
||||
## Scheme selector
|
||||
|
||||
`Scheme.MlDsa87 = 0`, `Scheme.MlDsa65 = 1` — these are the chain's own
|
||||
`DilithiumSignatureScheme` variant indices, so the number threaded through this
|
||||
API is the byte that ends up on the wire. New accounts use ML-DSA-65; ML-DSA-87
|
||||
is legacy and must be supported but never chosen.
|
||||
|
||||
## Signing context
|
||||
|
||||
ML-DSA hashes a context into the signature. Quantus extrinsics on spec >= 148 are
|
||||
verified under `QUANTUS_EXTRINSIC`, earlier specs under the empty context. A
|
||||
signature made under the wrong one is cryptographically valid, rejected by the
|
||||
chain, and indistinguishable locally — so use `contextForSpec(specVersion)`
|
||||
rather than picking one by hand. Nothing here guesses on your behalf.
|
||||
|
||||
## Sizes come from the crate
|
||||
|
||||
`sizes(scheme)` returns the public/secret/signature lengths rather than exposing
|
||||
constants to copy. They are consensus-critical — the runtime decodes a fixed-size
|
||||
array with no compact length prefix — and a JS constant that drifted would
|
||||
re-frame every byte after the signature while looking entirely healthy.
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
yarn install-build-deps # downloads wasm-bindgen 0.2.128 and binaryen
|
||||
./scripts/build-quantus.sh
|
||||
```
|
||||
|
||||
The Rust toolchain is pinned in `rust-toolchain.toml` to the same channel the
|
||||
chain builds its runtime with.
|
||||
|
||||
## Tests
|
||||
|
||||
`cargo test` runs conformance tests whose expected values come from the `quantus`
|
||||
CLI, not from this crate — the dev-genesis account ids, HD derivation at both
|
||||
schemes' default paths, and context separation. A test that pinned our own output
|
||||
would keep passing through exactly the drift they exist to catch.
|
||||
24
packages/quantus-crypto/package.json
Normal file
24
packages/quantus-crypto/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"author": "Quantus Network Developers <hello@quantus.com>",
|
||||
"bugs": "https://git.lair.cafe/quantus/wasm/issues",
|
||||
"description": "Quantus post-quantum crypto (ML-DSA, Poseidon2, HD derivation) for the browser",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"homepage": "https://git.lair.cafe/quantus/wasm/src/branch/main/packages/quantus-crypto#readme",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@quantus/crypto",
|
||||
"repository": {
|
||||
"directory": "packages/quantus-crypto",
|
||||
"type": "git",
|
||||
"url": "https://git.lair.cafe/quantus/wasm.git"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
}
|
||||
}
|
||||
6
packages/quantus-crypto/src/bytes.d.ts
vendored
Normal file
6
packages/quantus-crypto/src/bytes.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export declare const lenIn: number;
|
||||
export declare const lenOut: number;
|
||||
export declare const bytes: string;
|
||||
10
packages/quantus-crypto/src/bytes.js
Normal file
10
packages/quantus-crypto/src/bytes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Generated as part of the build, do not edit
|
||||
|
||||
export const lenIn = 0;
|
||||
|
||||
export const lenOut = 0;
|
||||
|
||||
export const bytes = '';
|
||||
6
packages/quantus-crypto/src/cjs/bytes.d.ts
vendored
Normal file
6
packages/quantus-crypto/src/cjs/bytes.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export declare const lenIn: number;
|
||||
export declare const lenOut: number;
|
||||
export declare const bytes: string;
|
||||
10
packages/quantus-crypto/src/cjs/bytes.js
Normal file
10
packages/quantus-crypto/src/cjs/bytes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Generated as part of the build, do not edit
|
||||
|
||||
exports.lenIn = 0;
|
||||
|
||||
exports.lenOut = 0;
|
||||
|
||||
exports.bytes = '';
|
||||
3
packages/quantus-crypto/src/cjs/package.json
Normal file
3
packages/quantus-crypto/src/cjs/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
143
packages/quantus-crypto/src/crypto.ts
Normal file
143
packages/quantus-crypto/src/crypto.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { ext_mldsa_derive, ext_mldsa_from_seed, ext_mldsa_is_scheme, ext_mldsa_sign, ext_mldsa_sizes, ext_mldsa_verify, ext_poseidon_hash } from './generated/quantus_crypto.js';
|
||||
import { initWasm } from './init.js';
|
||||
import { Scheme } from './scheme.js';
|
||||
|
||||
export interface Keypair {
|
||||
publicKey: Uint8Array;
|
||||
secretKey: Uint8Array;
|
||||
}
|
||||
|
||||
export interface Sizes {
|
||||
/** Public key length: 1952 for ML-DSA-65, 2592 for ML-DSA-87. */
|
||||
publicKey: number;
|
||||
/** Secret key length: 4032 / 4896. */
|
||||
secretKey: number;
|
||||
/** Signature length: 3309 / 4627. */
|
||||
signature: number;
|
||||
/** `signature ‖ publicKey`, the runtime's wire form: 5261 / 7219. */
|
||||
signatureWithPublicKey: number;
|
||||
}
|
||||
|
||||
function ready (): void {
|
||||
const error = initWasm();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`@quantus/crypto: WASM unavailable: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key and signature sizes for a scheme, read from the crate rather than
|
||||
* hardcoded here.
|
||||
*
|
||||
* These are consensus-critical — the runtime decodes a fixed-size array off the
|
||||
* wire — so a constant that drifted from the crate would re-frame every byte
|
||||
* after the signature while looking entirely healthy. Ask, don't assume.
|
||||
*/
|
||||
export function sizes (scheme: Scheme): Sizes {
|
||||
ready();
|
||||
|
||||
const raw = new DataView(ext_mldsa_sizes(scheme).buffer);
|
||||
|
||||
return {
|
||||
publicKey: raw.getUint32(0, true),
|
||||
secretKey: raw.getUint32(4, true),
|
||||
signature: raw.getUint32(8, true),
|
||||
signatureWithPublicKey: raw.getUint32(12, true)
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether this build supports `scheme`. */
|
||||
export function isScheme (scheme: number): scheme is Scheme {
|
||||
ready();
|
||||
|
||||
return ext_mldsa_is_scheme(scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* The account id for a public key — Poseidon2 over Goldilocks, 32 bytes out.
|
||||
*
|
||||
* This is the step that has no Substrate equivalent. There, an `AccountId32`
|
||||
* *is* the public key; here it is a one-way hash of it, which is why a Quantus
|
||||
* signature has to carry its public key inside itself and why nothing can
|
||||
* recover a key from an address.
|
||||
*/
|
||||
export function accountFromPublicKey (publicKey: Uint8Array): Uint8Array {
|
||||
ready();
|
||||
|
||||
return ext_poseidon_hash(publicKey);
|
||||
}
|
||||
|
||||
/** A keypair from 32 bytes of entropy — FIPS 204 `ML-DSA.KeyGen_internal`. */
|
||||
export function keypairFromSeed (seed: Uint8Array, scheme: Scheme): Keypair {
|
||||
ready();
|
||||
|
||||
return split(ext_mldsa_from_seed(seed, scheme), scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* A keypair from a BIP39 mnemonic at a hardened Quantus derivation path.
|
||||
*
|
||||
* `path` must be hardened at every level — lattice keys have no public
|
||||
* derivability, so there is no soft-junction equivalent and one is rejected
|
||||
* rather than reinterpreted.
|
||||
*/
|
||||
export function keypairFromMnemonic (mnemonic: string, password: string, path: string, scheme: Scheme): Keypair {
|
||||
ready();
|
||||
|
||||
return split(ext_mldsa_derive(mnemonic, password, path, scheme), scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign under a FIPS 204 context.
|
||||
*
|
||||
* `context` is not optional in spirit: extrinsics on spec >= 148 verify under
|
||||
* `QUANTUS_EXTRINSIC` and earlier ones under the empty context, and the wrong
|
||||
* choice yields a valid signature that the chain rejects with nothing locally
|
||||
* able to tell. Use `contextForSpec` rather than picking one by hand.
|
||||
*
|
||||
* Returns the bare signature. The runtime's wire form is `signature ‖ publicKey`
|
||||
* — see {@link signatureWithPublicKey} — but only the caller knows which it
|
||||
* wants.
|
||||
*/
|
||||
export function sign (message: Uint8Array, { publicKey, secretKey }: Keypair, context: Uint8Array, scheme: Scheme): Uint8Array {
|
||||
ready();
|
||||
|
||||
return ext_mldsa_sign(secretKey, publicKey, message, context, scheme);
|
||||
}
|
||||
|
||||
/** Verify a bare signature under a context. */
|
||||
export function verify (message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array, context: Uint8Array, scheme: Scheme): boolean {
|
||||
ready();
|
||||
|
||||
return ext_mldsa_verify(publicKey, message, signature, context, scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* `signature ‖ publicKey` — what a signed extrinsic actually carries.
|
||||
*
|
||||
* The runtime encodes this as a fixed-size array with **no compact length
|
||||
* prefix**, preceded by the scheme's enum variant byte. Getting that framing
|
||||
* wrong re-frames every byte after it into something that still decodes.
|
||||
*/
|
||||
export function signatureWithPublicKey (signature: Uint8Array, publicKey: Uint8Array): Uint8Array {
|
||||
const out = new Uint8Array(signature.length + publicKey.length);
|
||||
|
||||
out.set(signature);
|
||||
out.set(publicKey, signature.length);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The crate returns `secretKey ‖ publicKey`, matching `ext_ed_from_seed`. */
|
||||
function split (pair: Uint8Array, scheme: Scheme): Keypair {
|
||||
const { secretKey } = sizes(scheme);
|
||||
|
||||
return {
|
||||
publicKey: pair.subarray(secretKey),
|
||||
secretKey: pair.subarray(0, secretKey)
|
||||
};
|
||||
}
|
||||
172
packages/quantus-crypto/src/generated/quantus_crypto.d.ts
vendored
Normal file
172
packages/quantus-crypto/src/generated/quantus_crypto.d.ts
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* Derive a keypair from a BIP39 mnemonic at a hardened derivation path.
|
||||
*
|
||||
* Lattice keys have no public derivability, so there is no soft-junction
|
||||
* equivalent and the crate rejects any unhardened path outright. The Quantus
|
||||
* convention is:
|
||||
*
|
||||
* ```text
|
||||
* m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
|
||||
* ```
|
||||
*
|
||||
* with the account index at the third level and the *scheme* carried in the
|
||||
* trailing index. That is unusual, and it is what `quantus-cli` and the mobile
|
||||
* wallet already use — deriving anything else produces addresses no other
|
||||
* Quantus tool can find.
|
||||
*
|
||||
* The seeding matters as much as the path. This goes mnemonic → 64-byte BIP39
|
||||
* seed → HMAC-SHA512 chain keyed with the literal string `"Dilithium seed"`.
|
||||
* Substrate's own `mnemonicToMiniSecret` is a *different* derivation and is the
|
||||
* default reach in the polkadot-js codebase; using it here would yield a
|
||||
* well-formed key for an account nobody owns.
|
||||
*
|
||||
* * mnemonic: BIP39 phrase, 12/15/18/21/24 words
|
||||
* * password: BIP39 passphrase; empty string for none
|
||||
* * path: hardened derivation path, e.g. `m/44'/189189'/0'/0'/1'`
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, as
|
||||
* `ext_mldsa_from_seed` returns.
|
||||
*/
|
||||
export function ext_mldsa_derive(mnemonic: string, password: string, path: string, scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Generate a keypair from 32 bytes of entropy.
|
||||
*
|
||||
* This is FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step: the
|
||||
* crate expands the seed as `SHAKE256(seed ‖ k ‖ ℓ)`, so the parameter set is
|
||||
* absorbed into the expansion and the same 32 bytes yield independent keys per
|
||||
* scheme. That is why the dev accounts (`[0u8; 32]`, `[1u8; 32]`, `[2u8; 32]`)
|
||||
* and HD-derived accounts can share this one entry point.
|
||||
*
|
||||
* * seed: UIntArray with 32 elements
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, matching the
|
||||
* ordering `ext_ed_from_seed` uses. Split it at the secret length from
|
||||
* `ext_mldsa_sizes`.
|
||||
*/
|
||||
export function ext_mldsa_from_seed(seed: Uint8Array, scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Whether `scheme` names a parameter set this build supports.
|
||||
*
|
||||
* `dispatch!` falls back to ML-DSA-87 for anything unrecognised, which is the
|
||||
* right default but a poor way to discover a typo. Callers that accept a scheme
|
||||
* from storage or from a user should check here first.
|
||||
*/
|
||||
export function ext_mldsa_is_scheme(scheme: number): boolean;
|
||||
|
||||
/**
|
||||
* Sign a message under a FIPS 204 context.
|
||||
*
|
||||
* Signing is deterministic — no hedging randomness — because that is what the
|
||||
* runtime does (`hedge: None`), and a wallet that hedged would produce a
|
||||
* different signature each time for the same input, which makes the
|
||||
* byte-for-byte agreement tests in quantus/wasm#2 impossible to write.
|
||||
*
|
||||
* `ctx` is domain separation and it is **not** optional in practice: extrinsics
|
||||
* on spec >= 148 are verified under `QUANTUS_EXTRINSIC`, earlier specs under the
|
||||
* empty context, and a signature made under the wrong one is valid, rejected by
|
||||
* the chain, and indistinguishable locally. The caller chooses; this function
|
||||
* does not guess.
|
||||
*
|
||||
* * secret: UIntArray, secret-key length for the scheme
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the signature alone. The runtime's wire format is
|
||||
* `signature ‖ public`; concatenating is the caller's job because only the
|
||||
* caller knows whether it wants the wire form or the bare signature.
|
||||
*/
|
||||
export function ext_mldsa_sign(secret: Uint8Array, _public: Uint8Array, message: Uint8Array, ctx: Uint8Array, scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Key and signature sizes for a parameter set, as
|
||||
* `[public, secret, signature, signature_with_public]`.
|
||||
*
|
||||
* Exported so that nothing on the JS side has to hardcode 1952/4032/3309/5261 or
|
||||
* 2592/4896/4627/7219. Those numbers are consensus-critical — the runtime reads a
|
||||
* fixed-size array off the wire — and a JS constant that drifted from the crate
|
||||
* would mis-frame every byte after the signature while looking entirely healthy.
|
||||
* Ask the crate instead.
|
||||
*
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is four u32 lengths, little-endian, 16 bytes total.
|
||||
*/
|
||||
export function ext_mldsa_sizes(scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Verify a signature against a message and public key under a context.
|
||||
*
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * signature: UIntArray, signature length for the scheme
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*/
|
||||
export function ext_mldsa_verify(_public: Uint8Array, message: Uint8Array, signature: Uint8Array, ctx: Uint8Array, scheme: number): boolean;
|
||||
|
||||
/**
|
||||
* Poseidon2-over-Goldilocks hash of arbitrary bytes.
|
||||
*
|
||||
* This is the account-id derivation. On Substrate an `AccountId32` *is* the
|
||||
* public key; on Quantus it is `hash_bytes(public_key)`, which is why a Quantus
|
||||
* signature has to carry its public key along — the address cannot give it back.
|
||||
*
|
||||
* `qp_poseidon_core::hash_bytes` is `IdentifyAccount for DilithiumSigner` in the
|
||||
* runtime, so this is the same function the chain uses to decide who signed
|
||||
* something, reached through the same crate rather than a port of it.
|
||||
*
|
||||
* * data: arbitrary length UIntArray
|
||||
*
|
||||
* * returned vector is 32 bytes.
|
||||
*/
|
||||
export function ext_poseidon_hash(data: Uint8Array): Uint8Array;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly ext_mldsa_derive: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
|
||||
readonly ext_mldsa_from_seed: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly ext_mldsa_is_scheme: (a: number) => number;
|
||||
readonly ext_mldsa_sign: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number];
|
||||
readonly ext_mldsa_sizes: (a: number) => [number, number];
|
||||
readonly ext_mldsa_verify: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
|
||||
readonly ext_poseidon_hash: (a: number, b: number) => [number, number];
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
437
packages/quantus-crypto/src/generated/quantus_crypto.js
Normal file
437
packages/quantus-crypto/src/generated/quantus_crypto.js
Normal file
@@ -0,0 +1,437 @@
|
||||
/* @ts-self-types="./quantus_crypto.d.ts" */
|
||||
|
||||
/**
|
||||
* Derive a keypair from a BIP39 mnemonic at a hardened derivation path.
|
||||
*
|
||||
* Lattice keys have no public derivability, so there is no soft-junction
|
||||
* equivalent and the crate rejects any unhardened path outright. The Quantus
|
||||
* convention is:
|
||||
*
|
||||
* ```text
|
||||
* m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
|
||||
* ```
|
||||
*
|
||||
* with the account index at the third level and the *scheme* carried in the
|
||||
* trailing index. That is unusual, and it is what `quantus-cli` and the mobile
|
||||
* wallet already use — deriving anything else produces addresses no other
|
||||
* Quantus tool can find.
|
||||
*
|
||||
* The seeding matters as much as the path. This goes mnemonic → 64-byte BIP39
|
||||
* seed → HMAC-SHA512 chain keyed with the literal string `"Dilithium seed"`.
|
||||
* Substrate's own `mnemonicToMiniSecret` is a *different* derivation and is the
|
||||
* default reach in the polkadot-js codebase; using it here would yield a
|
||||
* well-formed key for an account nobody owns.
|
||||
*
|
||||
* * mnemonic: BIP39 phrase, 12/15/18/21/24 words
|
||||
* * password: BIP39 passphrase; empty string for none
|
||||
* * path: hardened derivation path, e.g. `m/44'/189189'/0'/0'/1'`
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, as
|
||||
* `ext_mldsa_from_seed` returns.
|
||||
* @param {string} mnemonic
|
||||
* @param {string} password
|
||||
* @param {string} path
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_derive(mnemonic, password, path, scheme) {
|
||||
const ptr0 = passStringToWasm0(mnemonic, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passStringToWasm0(path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_derive(ptr0, len0, ptr1, len1, ptr2, len2, scheme);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v4 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a keypair from 32 bytes of entropy.
|
||||
*
|
||||
* This is FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step: the
|
||||
* crate expands the seed as `SHAKE256(seed ‖ k ‖ ℓ)`, so the parameter set is
|
||||
* absorbed into the expansion and the same 32 bytes yield independent keys per
|
||||
* scheme. That is why the dev accounts (`[0u8; 32]`, `[1u8; 32]`, `[2u8; 32]`)
|
||||
* and HD-derived accounts can share this one entry point.
|
||||
*
|
||||
* * seed: UIntArray with 32 elements
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, matching the
|
||||
* ordering `ext_ed_from_seed` uses. Split it at the secret length from
|
||||
* `ext_mldsa_sizes`.
|
||||
* @param {Uint8Array} seed
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_from_seed(seed, scheme) {
|
||||
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_from_seed(ptr0, len0, scheme);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `scheme` names a parameter set this build supports.
|
||||
*
|
||||
* `dispatch!` falls back to ML-DSA-87 for anything unrecognised, which is the
|
||||
* right default but a poor way to discover a typo. Callers that accept a scheme
|
||||
* from storage or from a user should check here first.
|
||||
* @param {number} scheme
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function ext_mldsa_is_scheme(scheme) {
|
||||
const ret = wasm.ext_mldsa_is_scheme(scheme);
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a message under a FIPS 204 context.
|
||||
*
|
||||
* Signing is deterministic — no hedging randomness — because that is what the
|
||||
* runtime does (`hedge: None`), and a wallet that hedged would produce a
|
||||
* different signature each time for the same input, which makes the
|
||||
* byte-for-byte agreement tests in quantus/wasm#2 impossible to write.
|
||||
*
|
||||
* `ctx` is domain separation and it is **not** optional in practice: extrinsics
|
||||
* on spec >= 148 are verified under `QUANTUS_EXTRINSIC`, earlier specs under the
|
||||
* empty context, and a signature made under the wrong one is valid, rejected by
|
||||
* the chain, and indistinguishable locally. The caller chooses; this function
|
||||
* does not guess.
|
||||
*
|
||||
* * secret: UIntArray, secret-key length for the scheme
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the signature alone. The runtime's wire format is
|
||||
* `signature ‖ public`; concatenating is the caller's job because only the
|
||||
* caller knows whether it wants the wire form or the bare signature.
|
||||
* @param {Uint8Array} secret
|
||||
* @param {Uint8Array} _public
|
||||
* @param {Uint8Array} message
|
||||
* @param {Uint8Array} ctx
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_sign(secret, _public, message, ctx, scheme) {
|
||||
const ptr0 = passArray8ToWasm0(secret, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passArray8ToWasm0(_public, wasm.__wbindgen_malloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ptr3 = passArray8ToWasm0(ctx, wasm.__wbindgen_malloc);
|
||||
const len3 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_sign(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, scheme);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v5 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key and signature sizes for a parameter set, as
|
||||
* `[public, secret, signature, signature_with_public]`.
|
||||
*
|
||||
* Exported so that nothing on the JS side has to hardcode 1952/4032/3309/5261 or
|
||||
* 2592/4896/4627/7219. Those numbers are consensus-critical — the runtime reads a
|
||||
* fixed-size array off the wire — and a JS constant that drifted from the crate
|
||||
* would mis-frame every byte after the signature while looking entirely healthy.
|
||||
* Ask the crate instead.
|
||||
*
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is four u32 lengths, little-endian, 16 bytes total.
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_sizes(scheme) {
|
||||
const ret = wasm.ext_mldsa_sizes(scheme);
|
||||
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signature against a message and public key under a context.
|
||||
*
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * signature: UIntArray, signature length for the scheme
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
* @param {Uint8Array} _public
|
||||
* @param {Uint8Array} message
|
||||
* @param {Uint8Array} signature
|
||||
* @param {Uint8Array} ctx
|
||||
* @param {number} scheme
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function ext_mldsa_verify(_public, message, signature, ctx, scheme) {
|
||||
const ptr0 = passArray8ToWasm0(_public, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passArray8ToWasm0(signature, wasm.__wbindgen_malloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ptr3 = passArray8ToWasm0(ctx, wasm.__wbindgen_malloc);
|
||||
const len3 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_verify(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, scheme);
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poseidon2-over-Goldilocks hash of arbitrary bytes.
|
||||
*
|
||||
* This is the account-id derivation. On Substrate an `AccountId32` *is* the
|
||||
* public key; on Quantus it is `hash_bytes(public_key)`, which is why a Quantus
|
||||
* signature has to carry its public key along — the address cannot give it back.
|
||||
*
|
||||
* `qp_poseidon_core::hash_bytes` is `IdentifyAccount for DilithiumSigner` in the
|
||||
* runtime, so this is the same function the chain uses to decide who signed
|
||||
* something, reached through the same crate rather than a port of it.
|
||||
*
|
||||
* * data: arbitrary length UIntArray
|
||||
*
|
||||
* * returned vector is 32 bytes.
|
||||
* @param {Uint8Array} data
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_poseidon_hash(data) {
|
||||
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_poseidon_hash(ptr0, len0);
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
function __wbg_get_imports() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
__wbg_Error_67e7344beaa85059: function(arg0, arg1) {
|
||||
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_init_externref_table: function() {
|
||||
const table = wasm.__wbindgen_externrefs;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
},
|
||||
};
|
||||
return {
|
||||
__proto__: null,
|
||||
"./quantus_crypto_bg.js": import0,
|
||||
};
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return decodeText(ptr >>> 0, len);
|
||||
}
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||
WASM_VECTOR_LEN = arg.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_externrefs.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||
let numBytesDecoded = 0;
|
||||
function decodeText(ptr, len) {
|
||||
numBytesDecoded += len;
|
||||
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
numBytesDecoded = len;
|
||||
}
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
|
||||
if (!('encodeInto' in cachedTextEncoder)) {
|
||||
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let wasmModule, wasmInstance, wasm;
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasmInstance = instance;
|
||||
wasm = instance.exports;
|
||||
wasmModule = module;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (!module.ok) {
|
||||
throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
|
||||
}
|
||||
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
} catch (e) {
|
||||
const validResponse = expectedResponseType(module.type);
|
||||
|
||||
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else { throw e; }
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedResponseType(type) {
|
||||
switch (type) {
|
||||
case 'basic': case 'cors': case 'default': return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module !== undefined) {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
async function __wbg_init(module_or_path) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module_or_path !== undefined) {
|
||||
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||
({module_or_path} = module_or_path)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
if (module_or_path === undefined) {
|
||||
module_or_path = new URL('quantus_crypto_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||
module_or_path = fetch(module_or_path);
|
||||
}
|
||||
|
||||
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync, __wbg_init as default };
|
||||
7
packages/quantus-crypto/src/index.ts
Normal file
7
packages/quantus-crypto/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { accountFromPublicKey, isScheme, keypairFromMnemonic, keypairFromSeed, sign, signatureWithPublicKey, sizes, verify } from './crypto.js';
|
||||
export type { Keypair, Sizes } from './crypto.js';
|
||||
export { initWasm, isReady } from './init.js';
|
||||
export { contextForSpec, EXTRINSIC_CONTEXT, EXTRINSIC_MIN_SPEC, Scheme, SCHEME_NAME } from './scheme.js';
|
||||
59
packages/quantus-crypto/src/init.ts
Normal file
59
packages/quantus-crypto/src/init.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { base64Decode, unzlibSync } from '@polkadot/wasm-util';
|
||||
|
||||
import { bytes, lenOut } from './bytes.js';
|
||||
import { initSync } from './generated/quantus_crypto.js';
|
||||
|
||||
/**
|
||||
* Instantiate the WASM, synchronously, from bytes compiled into this file.
|
||||
*
|
||||
* Three constraints shape this, and all three rule out the obvious approach:
|
||||
*
|
||||
* - the background context is an **MV3 service worker**, so there is no DOM, no
|
||||
* reliable `fetch` of extension-relative URLs at arbitrary times, and the
|
||||
* worker can be killed and cold-started between any two messages
|
||||
* - the extension CSP is `script-src 'self' 'wasm-unsafe-eval'`, which permits
|
||||
* compiling WASM but not fetching it from anywhere interesting
|
||||
* - callers are synchronous — `pair.sign()` in the keyring has no `await` to give
|
||||
*
|
||||
* So the WASM is zlib-compressed, base64'd into `bytes.js` at build time, and
|
||||
* instantiated here with wasm-bindgen's `initSync`. Nothing is fetched, and the
|
||||
* whole module is ready before the first call returns.
|
||||
*
|
||||
* Deliberately *not* using `@polkadot/wasm-bridge`: its `Bridge` implements
|
||||
* wasm-bindgen 0.2.79's JS-heap ABI, and this crate is built with 0.2.128, which
|
||||
* uses externref tables. See quantus/wasm#1.
|
||||
*/
|
||||
|
||||
let initialised = false;
|
||||
let initError: string | null = null;
|
||||
|
||||
/**
|
||||
* Ensure the WASM is instantiated. Idempotent and cheap after the first call.
|
||||
*
|
||||
* Returns `null` on success, or the failure reason. It does not throw: a caller
|
||||
* deciding whether to offer a Quantus account at all wants to ask, and an
|
||||
* exception thrown from module scope in a service worker is hard to attribute.
|
||||
*/
|
||||
export function initWasm (): string | null {
|
||||
if (initialised) {
|
||||
return initError;
|
||||
}
|
||||
|
||||
initialised = true;
|
||||
|
||||
try {
|
||||
initSync({ module: unzlibSync(base64Decode(bytes, new Uint8Array(lenOut))) });
|
||||
} catch (error) {
|
||||
initError = (error as Error).message;
|
||||
}
|
||||
|
||||
return initError;
|
||||
}
|
||||
|
||||
/** Whether the WASM is available. Callers that can fall back should ask first. */
|
||||
export function isReady (): boolean {
|
||||
return initWasm() === null;
|
||||
}
|
||||
46
packages/quantus-crypto/src/scheme.ts
Normal file
46
packages/quantus-crypto/src/scheme.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Which ML-DSA parameter set a call refers to.
|
||||
*
|
||||
* These are the chain's own `DilithiumSignatureScheme` variant indices, not a
|
||||
* private numbering — the value here is the byte a signed extrinsic carries on
|
||||
* the wire. Keeping them identical means the keyring's `TYPE_PREFIX` is the
|
||||
* identity function on a scheme, with no table to get backwards.
|
||||
*/
|
||||
export enum Scheme {
|
||||
/** ML-DSA-87. Legacy: accounts created before the scheme was recorded, and the dev-genesis accounts. */
|
||||
MlDsa87 = 0,
|
||||
/** ML-DSA-65. What new accounts use. */
|
||||
MlDsa65 = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* The name each scheme is stored under, matching `quantus-cli` and the mobile
|
||||
* wallet so a wallet exported from one tool imports into another.
|
||||
*/
|
||||
export const SCHEME_NAME: Record<Scheme, string> = {
|
||||
[Scheme.MlDsa87]: 'ml-dsa-87',
|
||||
[Scheme.MlDsa65]: 'ml-dsa-65'
|
||||
};
|
||||
|
||||
/**
|
||||
* FIPS 204 context for on-chain extrinsic signatures.
|
||||
*
|
||||
* Only from spec 148 onward — earlier runtimes verify under the empty context,
|
||||
* and a signature made under the wrong one is valid, rejected by the chain, and
|
||||
* indistinguishable locally. Callers pass the spec version and get the right
|
||||
* answer from {@link contextForSpec}; nothing here guesses.
|
||||
*/
|
||||
export const EXTRINSIC_CONTEXT = new TextEncoder().encode('QUANTUS_EXTRINSIC');
|
||||
|
||||
/** First spec version that verifies extrinsics under {@link EXTRINSIC_CONTEXT}. */
|
||||
export const EXTRINSIC_MIN_SPEC = 148;
|
||||
|
||||
/** The signing context a runtime at `specVersion` expects. */
|
||||
export function contextForSpec (specVersion: number): Uint8Array {
|
||||
return specVersion >= EXTRINSIC_MIN_SPEC
|
||||
? EXTRINSIC_CONTEXT
|
||||
: new Uint8Array();
|
||||
}
|
||||
39
scripts/build-quantus.sh
Executable file
39
scripts/build-quantus.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 @quantus/crypto authors & contributors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Builds packages/quantus-crypto. Deliberately NOT part of build-wasm.sh: that
|
||||
# script drives the nightly-2022-06-24 + xargo build that `wasm-crypto` needs,
|
||||
# and this crate needs a modern compiler (inline `const {}` blocks, Rust >= 1.79).
|
||||
# Keeping the two builds separate is what lets wasm-crypto stay byte-identical to
|
||||
# upstream. See quantus/wasm#1.
|
||||
#
|
||||
# There is no asm.js step here. wasm2js over ML-DSA would be enormous and slow,
|
||||
# and every context we ship into sets 'wasm-unsafe-eval', so wasm is always
|
||||
# available where this runs.
|
||||
|
||||
set -e
|
||||
|
||||
PKG=quantus-crypto
|
||||
CRATE=quantus_crypto
|
||||
BINDGEN_VER=0.2.128
|
||||
|
||||
WASM=packages/$PKG/build-wasm/${CRATE}_bg.wasm
|
||||
OPT=packages/$PKG/build-wasm/${CRATE}_opt.wasm
|
||||
|
||||
echo "*** Building Rust sources"
|
||||
# The toolchain comes from packages/quantus-crypto/rust-toolchain.toml, which
|
||||
# pins the same channel the chain builds its runtime with.
|
||||
(cd packages/$PKG && cargo build --target wasm32-unknown-unknown --release --locked)
|
||||
|
||||
echo "*** Converting to WASM"
|
||||
./bindgen-quantus/wasm-bindgen \
|
||||
packages/$PKG/target/wasm32-unknown-unknown/release/$CRATE.wasm \
|
||||
--out-dir packages/$PKG/build-wasm \
|
||||
--target web
|
||||
|
||||
echo "*** Optimising WASM output"
|
||||
./binaryen/bin/wasm-opt $WASM -Oz -o $OPT
|
||||
|
||||
echo "*** Packing WASM into baseX"
|
||||
PKG_NAME=$PKG CRATE_NAME=$CRATE node ./scripts/pack-quantus-base.mjs
|
||||
@@ -61,3 +61,16 @@ if [ ! -d "bindgen" ]; then
|
||||
mv $BINDGEN_ZIP bindgen
|
||||
# ls -alR bindgen
|
||||
fi
|
||||
|
||||
# Quantus: a second wasm-bindgen, matching packages/quantus-crypto's wasm-bindgen
|
||||
# dependency. It cannot share the 0.2.79 binary above — that ABI predates
|
||||
# externref tables — and the two crates are built separately anyway.
|
||||
# See quantus/wasm#1.
|
||||
QUANTUS_BINDGEN_VER=0.2.128
|
||||
QUANTUS_BINDGEN_ZIP=wasm-bindgen-$QUANTUS_BINDGEN_VER-x86_64-unknown-linux-musl
|
||||
|
||||
if [ ! -d "bindgen-quantus" ]; then
|
||||
echo "*** Downloading bindgen for quantus-crypto"
|
||||
curl -L $BINDGEN_REPO/releases/download/$QUANTUS_BINDGEN_VER/$QUANTUS_BINDGEN_ZIP.tar.gz | tar xz
|
||||
mv $QUANTUS_BINDGEN_ZIP bindgen-quantus
|
||||
fi
|
||||
|
||||
45
scripts/pack-quantus-base.mjs
Normal file
45
scripts/pack-quantus-base.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// A near-copy of pack-wasm-base.mjs. Kept separate rather than parameterised
|
||||
// because that script is upstream's and rebases should not have to reconcile our
|
||||
// changes with theirs — the duplication is the cheaper of the two costs. It also
|
||||
// emits no deno variant, which we do not ship.
|
||||
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { zlibSync } from 'fflate/node';
|
||||
|
||||
const PKG_NAME = process.env['PKG_NAME'];
|
||||
const CRATE_NAME = process.env['CRATE_NAME'];
|
||||
const DIR_CJS = `./packages/${PKG_NAME}/build/cjs`;
|
||||
const DIR_ESM = `./packages/${PKG_NAME}/build`;
|
||||
const HDR = `// Copyright 2026 @quantus/crypto authors & contributors\n// SPDX-License-Identifier: Apache-2.0\n\n// Generated as part of the build, do not edit\n`;
|
||||
|
||||
const data = fs.readFileSync(`./packages/${PKG_NAME}/build-wasm/${CRATE_NAME}_opt.wasm`);
|
||||
const compressed = Buffer.from(zlibSync(data, { level: 9 }));
|
||||
const base64 = compressed.toString('base64');
|
||||
|
||||
console.log(`*** Compressed WASM: in=${data.length}, out=${compressed.length}, opt=${(100 * compressed.length / data.length).toFixed(2)}%, base64=${base64.length}`);
|
||||
|
||||
fs.mkdirSync(DIR_CJS, { recursive: true });
|
||||
|
||||
// Both module systems, as upstream does for wasm-crypto-wasm. The CJS copy sits
|
||||
// under a directory carrying its own `{"type":"commonjs"}`, because the package
|
||||
// itself is `"type": "module"` and node would otherwise refuse to load an
|
||||
// `exports.`-style file from it.
|
||||
fs.writeFileSync(`${DIR_CJS}/bytes.js`, `${HDR}
|
||||
exports.lenIn = ${compressed.length};
|
||||
|
||||
exports.lenOut = ${data.length};
|
||||
|
||||
exports.bytes = '${base64}';
|
||||
`);
|
||||
|
||||
fs.writeFileSync(`${DIR_ESM}/bytes.js`, `${HDR}
|
||||
export const lenIn = ${compressed.length};
|
||||
|
||||
export const lenOut = ${data.length};
|
||||
|
||||
export const bytes = '${base64}';
|
||||
`);
|
||||
Reference in New Issue
Block a user