Compare commits

..
Author SHA1 Message Date
grenadeandClaude Opus 5 0480bfcbc5 build: packageInfo at 14.0.3-quantus.3, as published
Lock Threads / lock (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 22:13:53 +03:00
grenadeandClaude Opus 5 a5157ef592 build: version the forks 14.0.3-quantus.3, on @quantus/crypto 0.3.0
The post-quantum-only change is a breaking one for every consumer. The keyring
and util-crypto surfaces both shrink, so it gets its own version rather than a
republish of quantus.2. @quantus/crypto moves to ^0.3.0, the version the
extension already resolves (wormhole addresses and nullifiers), so the two
share one copy of the WASM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 21:10:31 +03:00
grenadeandClaude Opus 5 e1d88c6f63 feat!: post-quantum only: remove ed25519, sr25519, ecdsa and ethereum keys
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
2026-09-16 21:05:52 +03:00
109 changed files with 372 additions and 4320 deletions
+1 -2
View File
@@ -31,8 +31,7 @@
"deno": "yarn polkadot-dev-deno-map && yarn build && deno check --import-map=import_map.json mod.ts",
"lint": "polkadot-dev-run-lint",
"postinstall": "polkadot-dev-yarn-only",
"test": "polkadot-dev-run-test --env browser ^mnemonic/toMiniSecretCmp",
"test:mnemonicCmp": "polkadot-dev-run-test --env browser mnemonic/toMiniSecretCmp",
"test": "polkadot-dev-run-test --env browser",
"test:node": "polkadot-dev-run-test --env browser ^mnemonic/toMiniSecretCmp",
"test:one": "polkadot-dev-run-test --env browser"
},
+3 -3
View File
@@ -18,12 +18,12 @@
"./packageDetect.cjs"
],
"type": "module",
"version": "14.0.3-quantus.2",
"version": "14.0.3-quantus.3",
"main": "index.js",
"dependencies": {
"@polkadot/util": "14.0.3",
"@polkadot/util-crypto": "14.0.3-quantus.2",
"@quantus/crypto": "^0.1.0",
"@polkadot/util-crypto": "14.0.3-quantus.3",
"@quantus/crypto": "^0.3.0",
"tslib": "^2.8.0"
},
"peerDependencies": {
-609
View File
@@ -1,609 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { KeyringPair$Json } from './types.js';
import { hexToU8a, stringToU8a } from '@polkadot/util';
import { base64Decode, cryptoWaitReady, encodeAddress, mnemonicGenerate, randomAsU8a, setSS58Format } from '@polkadot/util-crypto';
import * as languages from '@polkadot/util-crypto/mnemonic/wordlists/index';
import { decodePair } from './pair/decode.js';
import Keyring from './index.js';
await cryptoWaitReady();
describe('keypair', (): void => {
describe('ed25519', (): void => {
const publicKeyOne = new Uint8Array([47, 140, 97, 41, 216, 22, 207, 81, 195, 116, 188, 127, 8, 195, 230, 62, 209, 86, 207, 120, 174, 251, 74, 101, 80, 217, 123, 135, 153, 121, 119, 238]);
const publicKeyTwo = new Uint8Array([215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114, 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26]);
const seedOne = stringToU8a('12345678901234567890123456789012');
const seedTwo = hexToU8a('0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'ed25519' });
keyring.addFromSeed(seedOne, {});
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('creates via a dev seed', (): void => {
expect(
keyring.addFromUri('//Alice').address
).toEqual('5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu');
});
it('creates a ed25519 pair via mnemonicToSeed', (): void => {
expect(
keyring.addFromUri(
'seed sock milk update focus rotate barely fade car face mechanic mercy'
).address
).toEqual('5DkQP32jP4DVJLWWBRBoZF2tpWjqFrcrTBo6H5NcSk7MxKCC');
});
it('adds from a mnemonic, with correct ss58', (): void => {
// eslint-disable-next-line deprecation/deprecation
setSS58Format(20); // this would not be used
keyring.setSS58Format(2); // this would be used
const pair = keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card', {});
expect(pair.address).toEqual('HSLu2eci2GCfWkRimjjdTXKoFSDL3rBv5Ey2JWCBj68cVZj');
expect(encodeAddress(pair.publicKey)).toEqual('35cDYtPsdG1HUa2n2MaARgJyRz1WKMBZK1DL6c5cX7nugQh1');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(publicKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua","encoded":"0xb4a14995d25ab609f3686e9fa45f1fb237cd833f33f00d4b12c51858ca070d96972e47d73aae5eeb0fc06f923826cf0943fdb02c2c2ee30ef52a7912663053940d1da4da66b3a3f520ae07422c1c94b2d95690fca9d1f4a997623bb2923a8833280e19e7f72c3c5cfa343974e60e2b3dc53b404fdaf330756daad5e4e3","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"0"},"meta":{"isTesting":true,"name":"alice"}}') as KeyringPair$Json
).publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('sr25519', (): void => {
const publicKeyOne = new Uint8Array([116, 28, 8, 160, 111, 65, 197, 150, 96, 143, 103, 116, 37, 155, 217, 4, 51, 4, 173, 250, 93, 62, 234, 98, 118, 11, 217, 190, 151, 99, 77, 99]);
const publicKeyTwo = hexToU8a('0x44a996beb1eef7bdcab976ab6d2ca26104834164ecf28fb375600576fcc6eb0f');
const seedOne = stringToU8a('12345678901234567890123456789012');
const seedTwo = hexToU8a('0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'sr25519' });
keyring.addFromSeed(seedOne, {});
});
it('creates with dev phrase when only path specified', (): void => {
expect(
keyring.createFromUri('//Alice').address
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
it('creates with integer derivations', (): void => {
// MAX_SAFE_INTEGER
expect(
keyring.createFromUri('//9007199254740991').address
).toEqual('5CDsyNZyqxLpHnTvknr68anUcYoBFjZbFKiEJJf4prB75Uog');
// MAX_SAFE_INTEGER + extra digits
expect(
keyring.createFromUri('//900719925474099999').address
).toEqual('5GHj2D7RG2m2DXYwGSDpXwuuxn53G987i7p2EQVDqP4NYu4q');
});
it('creates via dev seed (2-byte encoding)', (): void => {
keyring.setSS58Format(252);
expect(
keyring.addFromUri('//Alice').address
).toEqual('xw8P6urbSAronL3zZFB7dg8p7LLSgKCUFDUgjohnf1iP434ic');
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('adds from a mnemonic', (): void => {
keyring.setSS58Format(2);
expect(
keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card', {}).address
).toEqual('FSjXNRT2K1R5caeHLPD6WMrqYUpfGZB7ua8W89JFctZ1YqV');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(publicKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua","encoded":"0xb4a14995d25ab609f3686e9fa45f1fb237cd833f33f00d4b12c51858ca070d96972e47d73aae5eeb0fc06f923826cf0943fdb02c2c2ee30ef52a7912663053940d1da4da66b3a3f520ae07422c1c94b2d95690fca9d1f4a997623bb2923a8833280e19e7f72c3c5cfa343974e60e2b3dc53b404fdaf330756daad5e4e3","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"0"},"meta":{"isTesting":true,"name":"alice"}}') as KeyringPair$Json
).publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('ecdsa', (): void => {
const seedOne = 'potato act energy ahead stone taxi receive fame gossip equip chest round';
const seedTwo = hexToU8a('0x3c74be003bd9a876be439949ccf2b292bd966c94959a689173b295b326cd6da7');
const publicKeyOne = hexToU8a('0x02c6b6c664db5ef505477bba1cf2f1789c98796b9bb5fa21abd0ac4589bed980e7');
const publicKeyTwo = hexToU8a('0x021da683b913fb28c979ba3e5f1881415cef4b1f58a5d05ed3610a2995e7b4943c');
const addressKeyOne = hexToU8a('0x0cfd0dd2c59a9987b9848919163931b6a42283ffd3d91e92c98b522525a7038f');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'ecdsa' });
keyring.addFromMnemonic(seedOne, {});
});
it('creates with dev phrase when only path specified', (): void => {
expect(
keyring.createFromUri('//Alice').address
).toEqual('5C7C2Z5sWbytvHpuLTvzKunnnRwQxft1jiqrLD5rhucQ5S9X');
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('adds from a mnemonic', (): void => {
keyring.setSS58Format(2);
expect(
keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card').address
).toEqual('DrRE1KAcs4pCicX8yJPh7YxkLPQ2vXnCFSVRPQfx38KjEFe');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(addressKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5DzMsaYFhmpRdErWrP6K6PD7UXzYoeETToSBUrZSvxasqWRz","encoded":"0xa192d39b42bc1601bf61df31039a554228593fadf870bc837b658a5114627aca199fff596260c95fe8994c66a47636cf0270aa08f402ba5541038753960d00e6c3af5e239ec58fb1eef3db7d6bc266f4853bdfe4ed17122d9092d879014d53980d2ee57f6f55a88c38836447d8645008e8815379626addc8f81f80cd49a2","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"2"},"meta":{}}') as KeyringPair$Json
).address
).toEqual('5DzMsaYFhmpRdErWrP6K6PD7UXzYoeETToSBUrZSvxasqWRz');
});
it('allows creation from JSON', (): void => {
keyring.setSS58Format(2);
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(pair.address).toEqual('DHL8HKFuTTR55JzzLmkJRCAfPBbuevKaT9cXikxbEV97Ko8');
expect(pair.publicKey).toEqual(hexToU8a('0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad'));
});
it('fails toJson() when password is incorrect', (): void => {
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(
() => pair.toJson('invalid')
).toThrow(/Unable to decode using the supplied passphrase/);
});
it('pass toJson() when password is correct', (): void => {
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(
() => pair.toJson('testing')
).not.toThrow();
});
it('encodes a pair toJSON (and decodes)', (): void => {
const pair = keyring.createFromUri('moral movie very draw assault whisper awful rebuild speed purity repeat card');
const json = pair.toJson('password');
expect(json.address).toEqual('0x03ddca309bd5fedd01f914d6fb76f23aa848a2a520802159215dba5085d7863619');
expect(json.encoding).toEqual({
content: ['pkcs8', 'ecdsa'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
const newPair = keyring.createFromJson(json);
expect(newPair.publicKey).toEqual(pair.publicKey);
expect(
() => newPair.unlock('password')
).not.toThrow();
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(addressKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(addressKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('ethereum', (): void => {
// combine mnemonic with derivation path
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy' + '/m/44\'/60\'/0\'/0/0';
const PRIV_KEY_ONE = '0x070dc3117300011918e26b02176945cc15c3d548cf49fd8418d97f93af699e46';
const ETH_ADDRESS_ONE = '0x31ea8795EE32D782C8ff41a5C68Dcbf0F5B27f6d';
const ETH_ADDRESS_TWO = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ type: 'ethereum' });
});
it('creates with dev phrase from the private key', (): void => {
const pair = keyring.addFromSeed(hexToU8a(PRIV_KEY_ONE));
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase from the private key in createFromUri', (): void => {
const pair = keyring.createFromUri(PRIV_KEY_ONE);
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified', (): void => {
const pair = keyring.createFromUri(PHRASE);
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified - addFromUri', (): void => {
expect(
keyring.addFromUri(PHRASE).address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified - addFromUri with type', (): void => {
const keyringUntyped = new Keyring();
expect(
keyringUntyped.addFromUri(PHRASE, {}, 'ethereum').address
).toEqual(ETH_ADDRESS_ONE);
});
it('encodes a pair toJSON (and decodes)', (): void => {
const pair = keyring.createFromUri(PHRASE);
const json = pair.toJson('password');
expect(json.address).toEqual('0x0381351b1b46d2602b0992bb5d5531f9c1696b0812feb2534b6884adc47e2e1d8b'); // this is the public key (different from address for ethereum)
expect(json.encoding).toEqual({
content: ['pkcs8', 'ethereum'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
const newPair = keyring.createFromJson(json);
expect(newPair.publicKey).toEqual(pair.publicKey);
expect(
() => newPair.unlock('password')
).not.toThrow();
});
it('encodes a pair toJSON and back', (): void => {
const pairOriginal = keyring.createFromUri(PHRASE);
const json = pairOriginal.toJson('password');
const pair = keyring.addFromJson(
json
);
expect(pair.address).toEqual(ETH_ADDRESS_ONE);
pair.decodePkcs8('password');
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe(ETH_ADDRESS_ONE);
});
it('allows adding from JSON', (): void => {
const pair = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'));
expect(pair.address).toEqual(ETH_ADDRESS_TWO);
pair.decodePkcs8('password');
expect(pair.isLocked).toBe(false);
expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'));
expect(pair.address).toBe(ETH_ADDRESS_TWO);
});
it('allows for signing/verification', (): void => {
const MESSAGE = stringToU8a('just some test message');
const signer = keyring.createFromUri(PHRASE);
const verifier = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
const signature = signer.sign(MESSAGE);
const dummyPublic = verifier.publicKey.slice();
dummyPublic[dummyPublic.length - 1] = 0;
expect(verifier.verify(MESSAGE, signature, signer.publicKey)).toBe(true);
expect(verifier.verify(MESSAGE, signature, dummyPublic)).toBe(false);
expect(verifier.verify(new Uint8Array(), signature, signer.publicKey)).toBe(false);
});
it('allows for signing/verification (withType)', (): void => {
const MESSAGE = stringToU8a('just some test message');
const signer = keyring.createFromUri(PHRASE);
const verifier = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
const signature = signer.sign(MESSAGE, { withType: true });
const dummyPublic = verifier.publicKey.slice();
dummyPublic[dummyPublic.length - 1] = 0;
expect(verifier.verify(MESSAGE, signature, signer.publicKey)).toBe(true);
expect(verifier.verify(MESSAGE, signature, dummyPublic)).toBe(false);
expect(verifier.verify(new Uint8Array(), signature, signer.publicKey)).toBe(false);
});
});
describe('raw pair add/create', (): void => {
const json = JSON.parse('{"address":"5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H","encoded":"JQUl8ZpoXv2OMkL9TPylLmcIye2cYhaS9INICbFgZTsAgAAAAQAAAAgAAAAr/0hJOOzokIdBG71TstigLABX9D5xGD7L37ySxtjDrVRg26LL90jLQ47quT9o3bq6ppXMVL6USk7Q4p3WU66bojTFuCDyhpYRhNbUqU6s0rD3S4bhv9lG+pG9vQ4eD5PVQUvxdANmJpYuDg45nrTmsMC5AHGdFGkHW/LHnkmbFid1cvPYkdiBoef5CIEdoly512pxMupVxnJWF1NT","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"name":"hello"}}') as KeyringPair$Json;
const decoded = decodePair('1', base64Decode(json.encoded), json.encoding.type);
const keyring = new Keyring({ ss58Format: 44 });
it('creates a pair from a private/public combo', (): void => {
const pair = keyring.createFromPair(decoded, json.meta, 'sr25519');
expect(pair.address).toEqual('5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H');
expect(pair.isLocked).toEqual(false);
expect(pair.meta.name).toEqual('hello');
});
it('adds a pair from a private/public combo', (): void => {
keyring.addFromPair(decoded, json.meta, 'sr25519');
const pair = keyring.getPairs()[0];
expect(pair.address).toEqual('5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H');
expect(pair.isLocked).toEqual(false);
expect(pair.meta.name).toEqual('hello');
});
});
describe('util', (): void => {
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42 });
});
it('can re-encode an address to Polkadot live', (): void => {
expect(
keyring.encodeAddress('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 0)
).toEqual('15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5');
});
it('can re-encode an address to keyring default', (): void => {
expect(
keyring.encodeAddress('15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5')
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
});
describe('version 2 JSON', (): void => {
const PAIR = '{"address":"5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu","encoded":"0xee8f236e2ac3217ce689692a4afc612220dc77fddaed0482f8f95136a7c3e034cccfbc495410a6e9b2439904974ed1d207abeca536ff6985ceb78edeeb3dc343e561c184c488101af8811d1331430b4ccf0e96ef507132e5132964e8564232e7100d973c5bee7b231dd0c8ad5273f3501515a422c8d7ed9d20a73c0ed17c98ee4588e54844bb73052dcad81f7a1094613d63c162fec7446c88b1fae70e","encoding":{"content":["pkcs8","sr25519"],"type":"xsalsa20-poly1305","version":"2"},"meta":{"genesisHash":"0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e","name":"json v2","tags":[],"whenCreated":1595243159596}}';
const PASS2 = 'versionTwo';
const PASS3 = 'versionThree';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42 });
});
it('can decode from a version 2 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS2);
const json = pair.toJson(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu');
expect(json.encoding).toEqual({
content: ['pkcs8', 'sr25519'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
pair.decodePkcs8(PASS3);
expect(pair.address).toEqual('5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu');
});
});
describe('version 3 JSON (hex)', (): void => {
const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"0xcd238963070cc4d6806053ee1ac500c7add9c28732bb5d434a332f84a91d9be0008000000100000008000000cf630a1113941b350ddd06697e50399183162e5e9a0e893eafc7f5f4893a223dca5055706b9925b56fdb4304192143843da718e11717daf89cf4f4781f94fb443f61432f782d54280af9eec90bd3069c3cc2d957a42b7c18dc2e9497f623735518e0e49b58f8e4db2c09da3a45dbb935659d015fc94b946cba75b606a6ff7f4e823f6b049e2e6892026b49de02d6dbbd64646fe0933f537d9ea53a70be","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639}}';
const PASS3 = 'version3';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 2 });
});
it('can decode from a version 3 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
});
});
describe('version 3 JSON (base64)', (): void => {
const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639,"whenEdited":1595278378596}}';
const PASS3 = 'version3';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 2 });
});
it('can decode from a version 3 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
});
});
describe('wordlist', (): void => {
it('creates keypair from different wordlists mnemonics', (): void => {
Object.keys(languages).forEach((language) => {
const mnemonic = mnemonicGenerate(12, languages[language as keyof typeof languages]);
const keyring = new Keyring({
type: 'ed25519'
});
expect(keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages[language as keyof typeof languages]
)).toBeDefined();
});
});
it('cannot create from invalid wordlist', (): void => {
const mnemonic = mnemonicGenerate(12, languages.japanese);
const keyring = new Keyring({
type: 'ed25519'
});
expect(() => keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages.english
)).toThrow('Invalid bip39 mnemonic specified');
});
});
});
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { KeypairType } from '@polkadot/util-crypto/types';
import { contextForSpec } from '@quantus/crypto';
import { u8aConcat } from '@polkadot/util';
import { cryptoWaitReady, decodeAddress, dilithiumSizes } from '@polkadot/util-crypto';
import { Keyring } from './index.js';
import { createTestPairs } from './testingPairs.js';
await cryptoWaitReady();
// Addresses `quantus developer create-test-wallets` prints for the dev accounts.
const ALICE = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
const BOB = 'qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y';
const CHARLIE = 'qzntBpmqHZF1jxC8KJKpuxcYuHST892jyXBqRctpAxd1WQ9BL';
const MESSAGE = new Uint8Array([1, 2, 3, 4]);
const PASSWORD = 'correct horse';
// Upstream's keyring specs exercised ed25519, sr25519, ecdsa and ethereum pairs.
// Those types, and their primitives, are gone (quantus/common#6). What replaces
// them is what a post-quantum-only keyring has to guarantee instead.
describe('Keyring (post-quantum only)', (): void => {
describe('refuses quantum-unsafe keys, and says why', (): void => {
const unsafe = ['ed25519', 'sr25519', 'ecdsa', 'ethereum'] as unknown as KeypairType[];
for (const type of unsafe) {
it(`at construction: ${type}`, (): void => {
expect(() => new Keyring({ type })).toThrow(/not quantum-safe and cannot be held here/);
});
it(`from a uri: ${type}`, (): void => {
expect(() => new Keyring().createFromUri('//Alice', {}, type)).toThrow(/not quantum-safe/);
});
}
// The entry point that matters most: a backup file the user chose. This is
// the shape of a real polkadot{.js} export of an ed25519 account.
it('from a polkadot{.js} JSON backup', (): void => {
const json = {
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
encoded: '0x00',
encoding: { content: ['pkcs8', 'ed25519'], type: ['scrypt', 'xsalsa20-poly1305'], version: '3' },
meta: { name: 'polkadot account' }
};
expect(() => new Keyring().createFromJson(json as never)).toThrow(/ed25519 keys are not quantum-safe and cannot be held here/);
});
it('and still refuses an unknown type as unknown', (): void => {
expect(() => new Keyring({ type: 'dilithium44' as KeypairType })).toThrow(/found 'dilithium44'/);
});
});
it('defaults to ML-DSA-65', (): void => {
expect(new Keyring().type).toEqual('dilithium65');
});
describe('test pairs are the Quantus dev accounts', (): void => {
const pairs = createTestPairs({ ss58Format: 189 });
it('with the addresses the chain endows', (): void => {
expect(pairs.crystal_alice.address).toEqual(ALICE);
expect(pairs.dilithium_bob.address).toEqual(BOB);
expect(pairs.crystal_charlie.address).toEqual(CHARLIE);
expect(pairs.crystal_alice.type).toEqual('dilithium87');
});
it('and a nobody pair of a type the keyring can hold', (): void => {
expect(pairs.nobody.type).toEqual('dilithium65');
});
});
// Upstream decoded the address and passed it as the public key, which for
// Substrate is the same bytes. Here the address is a hash of the key, so every
// watch-only account reported the hash of its own address.
it('adds a watch-only account at the address it was given', (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const watched = keyring.addFromAddress(ALICE, { name: 'watched' });
expect(watched.address).toEqual(ALICE);
expect(watched.addressRaw).toEqual(decodeAddress(ALICE));
expect(keyring.getPair(ALICE).meta.name).toEqual('watched');
});
describe('JSON round trip', (): void => {
for (const type of ['dilithium65', 'dilithium87'] as const) {
it(`${type}: encrypts, restores locked at the right address, and unlocks`, (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const pair = keyring.addFromSeed(new Uint8Array(32).fill(7), { name: 'roundtrip' }, type);
const restored = keyring.createFromJson(pair.toJson(PASSWORD));
expect(restored.isLocked).toBe(true);
expect(restored.address).toEqual(pair.address);
expect(() => restored.decodePkcs8('wrong')).toThrow();
restored.decodePkcs8(PASSWORD);
expect(restored.publicKey).toEqual(pair.publicKey);
expect(restored.sign(MESSAGE, { context: contextForSpec(148) })).toEqual(pair.sign(MESSAGE, { context: contextForSpec(148) }));
});
}
});
describe('verify', (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const alice = keyring.addFromSeed(new Uint8Array(32), {}, 'dilithium87');
const bob = keyring.addFromSeed(new Uint8Array(32).fill(1), {}, 'dilithium87');
const context = contextForSpec(148);
// what pair.sign returns: the chain's wire form, signature ‖ publicKey
const signature = alice.sign(MESSAGE, { context });
const bare = signature.slice(0, dilithiumSizes('dilithium87').signature);
it('accepts the signature ‖ publicKey form and a bare signature', (): void => {
expect(signature).toEqual(u8aConcat(bare, alice.publicKey));
expect(alice.verify(MESSAGE, signature, alice.publicKey, context)).toBe(true);
expect(alice.verify(MESSAGE, bare, alice.publicKey, context)).toBe(true);
});
it('fails under the wrong context, for the wrong signer, or for another message', (): void => {
expect(alice.verify(MESSAGE, signature, alice.publicKey)).toBe(false);
expect(alice.verify(MESSAGE, signature, bob.publicKey, context)).toBe(false);
expect(alice.verify(new Uint8Array([9]), signature, alice.publicKey, context)).toBe(false);
});
});
it('has no derivation and no VRF', (): void => {
const pair = new Keyring().addFromSeed(new Uint8Array(32));
expect(() => pair.derive('//1')).toThrow(/derive from the mnemonic/);
expect(() => pair.vrfSign(MESSAGE)).toThrow(/VRF signing is not available/);
expect(() => pair.vrfVerify(MESSAGE, new Uint8Array(96), pair.publicKey)).toThrow(/VRF verification is not available/);
});
});
+68 -102
View File
@@ -4,8 +4,8 @@
import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringInstance, KeyringOptions, KeyringPair, KeyringPair$Json, KeyringPair$Meta } from './types.js';
import { hexToU8a, isHex, stringToU8a } from '@polkadot/util';
import { base64Decode, decodeAddress, dilithiumPairFromMnemonic, dilithiumPairFromSeed, dilithiumPathFromSuri, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, ethereumEncode, hdEthereum, isDilithium, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto';
import { hexToU8a, isHex } from '@polkadot/util';
import { base64Decode, decodeAddress, dilithiumPairFromMnemonic, dilithiumPairFromSeed, dilithiumPathFromSuri, encodeAddress, keyExtractSuri } from '@polkadot/util-crypto';
import { createPair } from './pair/index.js';
import { DEV_PHRASE } from './defaults.js';
@@ -19,16 +19,25 @@ import { Pairs } from './pairs.js';
* the next change to the union has one place to look, and so the error message
* cannot drift from the check.
*/
const KEYPAIR_TYPES: KeypairType[] = ['dilithium65', 'dilithium87', 'ecdsa', 'ed25519', 'ethereum', 'sr25519'];
const KEYPAIR_TYPES: KeypairType[] = ['dilithium65', 'dilithium87'];
const PairFromSeed = {
dilithium65: (seed: Uint8Array): Keypair => dilithiumPairFromSeed(seed, 'dilithium65'),
dilithium87: (seed: Uint8Array): Keypair => dilithiumPairFromSeed(seed, 'dilithium87'),
ecdsa: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
ed25519: (seed: Uint8Array): Keypair => ed25519FromSeed(seed),
ethereum: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
sr25519: (seed: Uint8Array): Keypair => sr25519FromSeed(seed)
};
/** Upstream's types, refused by name so the refusal can say why. */
const QUANTUM_UNSAFE = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
/**
* Refuse anything that is not a post-quantum keypair type.
*
* A key of upstream's types arriving here (a JSON backup from polkadot{.js}, a
* caller asking for sr25519) is not a bug in this software, and "unknown crypto
* type" would read as one. Say what it is instead. quantus/common#6
*/
function assertKeypairType (type: string | undefined): asserts type is KeypairType {
if (type && QUANTUM_UNSAFE.includes(type)) {
throw new Error(`${type} keys are not quantum-safe and cannot be held here; only ML-DSA (dilithium65, dilithium87) keys are supported`);
} else if (!KEYPAIR_TYPES.includes(type as KeypairType)) {
throw new Error(`Expected a keypair type of one of ${KEYPAIR_TYPES.map((t) => `'${t}'`).join(', ')}, found '${type || 'unknown'}'`);
}
}
function pairToPublic ({ publicKey }: KeyringPair): Uint8Array {
return publicKey;
@@ -60,14 +69,12 @@ export class Keyring implements KeyringInstance {
public decodeAddress = decodeAddress;
constructor (options: KeyringOptions = {}) {
options.type = options.type || 'ed25519';
// ML-DSA-65 is the scheme new Quantus accounts use.
options.type = options.type || 'dilithium65';
// A runtime whitelist behind the KeypairType union, so widening the type
// alone was not enough — the compiler cannot see this one, and it fails at
// construction rather than at use.
if (!KEYPAIR_TYPES.includes(options.type || ('undefined' as KeypairType))) {
throw new Error(`Expected a keyring type of one of ${KEYPAIR_TYPES.map((t) => `'${t}'`).join(', ')}, found '${options.type || 'unknown'}'`);
}
// Behind the union type: a JavaScript caller, or one casting, can still pass
// anything, and this fails at construction rather than at use.
assertKeypairType(options.type);
this.#pairs = new Pairs();
this.#ss58 = options.ss58Format;
@@ -89,7 +96,7 @@ export class Keyring implements KeyringInstance {
}
/**
* @description Returns the type of the keyring, ed25519, sr25519 or ecdsa
* @description Returns the type of the keyring: dilithium65 or dilithium87
*/
public get type (): KeypairType {
return this.#type;
@@ -112,9 +119,14 @@ export class Keyring implements KeyringInstance {
* `addPair` to stores in a keyring pair dictionary the public key of the generated pair as a key and the pair as the associated value.
*/
public addFromAddress (address: string | Uint8Array, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, type: KeypairType = this.type, ignoreChecksum?: boolean, encType?: EncryptedJsonEncoding[]): KeyringPair {
const publicKey = this.decodeAddress(address, ignoreChecksum);
assertKeypairType(type);
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, { publicKey, secretKey: new Uint8Array() }, meta, encoded, encType));
// An address is a Poseidon2 hash of an ML-DSA public key, not the key, so it
// is carried as an account id. Upstream passed it as `publicKey`, which for
// these types made every watch-only account report the hash of its address.
const accountId = this.decodeAddress(address, ignoreChecksum);
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, { accountId, publicKey: new Uint8Array(), secretKey: new Uint8Array() }, meta, encoded, encType));
}
/**
@@ -159,7 +171,7 @@ export class Keyring implements KeyringInstance {
*/
public addFromSeed (seed: Uint8Array, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
return this.addPair(
createPair({ toSS58: this.encodeAddress, type }, PairFromSeed[type](seed), meta, null)
this.createFromPair(dilithiumPairFromSeed(seed, type), meta, type)
);
}
@@ -190,17 +202,11 @@ export class Keyring implements KeyringInstance {
? [type]
: type;
// Same list, second copy — this one guards JSON coming in from a file the
// user chose, so it is the guard that will matter most once quantus/common#6
// narrows the union and imports of quantum-unsafe keys have to be refused
// with a message explaining why rather than "unknown crypto type".
if (!KEYPAIR_TYPES.includes(cryptoType as KeypairType)) {
throw new Error(`Unknown crypto type ${cryptoType}`);
}
// The guard that matters most: JSON arrives from a file the user chose, and a
// polkadot{.js} backup of an sr25519 account must be refused with a reason,
// not constructed and not reported as an unknown type.
assertKeypairType(cryptoType);
// For the curve schemes the address and publicKey are 32 bytes and
// isomorphic, which is why the address field holds the public key for
// ethereum pairs and why this works at all.
const raw = isHex(address)
? hexToU8a(address)
: this.decodeAddress(address, ignoreChecksum);
@@ -208,17 +214,12 @@ export class Keyring implements KeyringInstance {
? hexToU8a(encoded)
: base64Decode(encoded);
// ML-DSA breaks the isomorphism: the account id is a one-way Poseidon2 hash
// and the 1952/2592-byte public key is inside `decoded`, which is encrypted
// and stays that way until someone supplies a password. So pass the account
// id as an account id rather than pretending it is a key — createPair uses
// it for the address while locked, and checks it against the real public key
// the moment `decodePkcs8` produces one.
const info = isDilithium(cryptoType as KeypairType)
? { accountId: raw, publicKey: new Uint8Array(), secretKey: new Uint8Array() }
: { publicKey: raw, secretKey: new Uint8Array() };
return createPair({ toSS58: this.encodeAddress, type: cryptoType as KeypairType }, info, meta, decoded, encType);
// The account id is a one-way Poseidon2 hash, and the 1952/2592-byte public
// key is inside `decoded`, encrypted until someone supplies a password. So
// carry the account id as an account id: createPair uses it for the address
// while locked, and checks it against the real public key the moment
// `decodePkcs8` produces one.
return createPair({ toSS58: this.encodeAddress, type: cryptoType }, { accountId: raw, publicKey: new Uint8Array(), secretKey: new Uint8Array() }, meta, decoded, encType);
}
/**
@@ -226,6 +227,8 @@ export class Keyring implements KeyringInstance {
* @summary Creates a pair from an explicit publicKey/secreteKey combination
*/
public createFromPair (pair: Keypair, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
assertKeypairType(type);
return createPair({ toSS58: this.encodeAddress, type }, pair, meta, null);
}
@@ -234,77 +237,40 @@ export class Keyring implements KeyringInstance {
* @summary Creates a Keypair from an suri
* @description This creates a pair from the suri, but does not add it to the keyring
*/
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
// here we only aut-add the dev phrase if we have a hard-derived path
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, _wordlist?: string[]): KeyringPair {
assertKeypairType(type);
// here we only auto-add the dev phrase if we have a hard-derived path
const suri = _suri.startsWith('//')
? `${DEV_PHRASE}${_suri}`
: _suri;
const { derivePath, password, path, phrase } = keyExtractSuri(suri);
let seed: Uint8Array;
const isPhraseHex = isHex(phrase, 256);
const { derivePath, password, phrase } = keyExtractSuri(suri);
// ML-DSA derives from the mnemonic itself along a hardened BIP44 path, not
// 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. Handled before the seeding below
// because that seeding (mnemonicToMiniSecret) is the wrong one for us and
// would produce a valid key for an account nobody owns.
if (isDilithium(type)) {
if (isPhraseHex) {
// A raw 32-byte seed goes straight into keygen, which is how the
// dev-genesis accounts are defined. Combining one with a derivation path
// is ambiguous — is the seed the master, or already derived? — so refuse
// rather than pick.
if (derivePath) {
throw new Error('A derivation path cannot be combined with a raw seed for post-quantum pairs');
}
return createPair({ toSS58: this.encodeAddress, type }, PairFromSeed[type](hexToU8a(phrase)), meta, null);
// 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.
if (isHex(phrase, 256)) {
// A raw 32-byte seed goes straight into keygen, which is how the
// dev-genesis accounts are defined. Combining one with a derivation path
// is ambiguous (is the seed the master, or already derived?), so refuse
// rather than pick.
if (derivePath) {
throw new Error('A derivation path cannot be combined with a raw seed for post-quantum pairs');
}
return createPair(
{ toSS58: this.encodeAddress, type },
dilithiumPairFromMnemonic(phrase, password || '', dilithiumPathFromSuri(type, derivePath), type),
meta,
null
);
return this.createFromPair(dilithiumPairFromSeed(hexToU8a(phrase), type), meta, type);
}
if (isPhraseHex) {
seed = hexToU8a(phrase);
} else {
const parts = phrase.split(' ');
if ([12, 15, 18, 21, 24].includes(parts.length)) {
seed = type === 'ethereum'
? mnemonicToLegacySeed(phrase, '', false, 64)
: mnemonicToMiniSecret(phrase, password, wordlist);
} else {
if (phrase.length > 32) {
throw new Error('specified phrase is not a valid mnemonic and is invalid as a raw seed at > 32 bytes');
}
seed = stringToU8a(phrase.padEnd(32));
}
}
const derived = type === 'ethereum'
? isPhraseHex
? PairFromSeed[type](seed) // for eth, if the private key is provided as suri, it must be derived only once
: hdEthereum(seed, derivePath.substring(1))
: keyFromPath(PairFromSeed[type](seed), path, type);
return createPair({ toSS58: this.encodeAddress, type }, derived, meta, null);
return this.createFromPair(
dilithiumPairFromMnemonic(phrase, password || '', dilithiumPathFromSuri(type, derivePath), type),
meta,
type
);
}
/**
* @name encodeAddress
* @description Encodes the input into an ss58 representation
*/
public encodeAddress = (address: Uint8Array | string, ss58Format?: number): string => {
return this.type === 'ethereum'
? ethereumEncode(address)
: encodeAddress(address, ss58Format ?? this.#ss58);
return encodeAddress(address, ss58Format ?? this.#ss58);
};
/**
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '14.0.3-quantus.2' };
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { createTestPairs } from '../testingPairs.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('decode', (): void => {
it('fails when no data provided', (): void => {
expect(
(): void => keyring.alice.decodePkcs8()
).toThrow(/No encrypted data available/);
});
it('returns correct publicKey from encoded', (): void => {
const PASS = 'testing';
expect(
(): void => keyring.alice.decodePkcs8(
PASS, keyring.alice.encodePkcs8(PASS)
)
).not.toThrow();
});
});
@@ -88,13 +88,4 @@ describe('dilithium pairs', (): void => {
it('refuses to VRF sign', (): void => {
expect(() => alice.vrfSign(MESSAGE)).toThrow(/not available for dilithium87/);
});
// The classical paths must be untouched — this is what a careless rebase
// breaks, and it would break silently.
it('leaves sr25519 unchanged', (): void => {
const sr = new Keyring({ type: 'sr25519' }).addFromUri('//Alice');
expect(sr.address).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
expect(sr.sign(MESSAGE).length).toEqual(64);
});
});
@@ -94,11 +94,4 @@ describe('dilithium derivation', (): void => {
expect(() => pair.derive('//1')).toThrow(/derive from the mnemonic with createFromUri/);
});
it('leaves sr25519 junction derivation unchanged', (): void => {
const sr = new Keyring({ type: 'sr25519' });
expect(sr.createFromUri('//Alice').address).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
expect(sr.createFromUri('//Bob').address).toEqual('5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty');
});
});
@@ -100,15 +100,4 @@ describe('dilithium account JSON', (): void => {
expect(restored.address).toEqual(other.address);
expect(() => restored.decodePkcs8(PASSWORD)).toThrow(/does not match the address/);
});
it('leaves sr25519 JSON round-tripping unchanged', (): void => {
const sr = new Keyring({ type: 'sr25519' });
const pair = sr.addFromUri('//Alice');
const restored = sr.createFromJson(pair.toJson(PASSWORD));
restored.decodePkcs8(PASSWORD);
expect(restored.address).toEqual(pair.address);
expect(restored.publicKey).toEqual(pair.publicKey);
});
});
-28
View File
@@ -1,28 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { NONCE_LENGTH, SCRYPT_LENGTH } from '@polkadot/util-crypto/json/constants';
import { createTestPairs } from '../testingPairs.js';
import { PAIR_DIV, PAIR_HDR, PUB_LENGTH, SEC_LENGTH } from './defaults.js';
const DECODED_LENGTH = PAIR_DIV.length + PAIR_HDR.length + PUB_LENGTH + SEC_LENGTH;
const ENCODED_LENGTH = 16 + DECODED_LENGTH + NONCE_LENGTH + SCRYPT_LENGTH;
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('encode', (): void => {
it('returns PKCS8 when no passphrase supplied', (): void => {
expect(
keyring.alice.encodePkcs8()
).toHaveLength(DECODED_LENGTH);
});
it('returns encoded PKCS8 when passphrase supplied', (): void => {
expect(
keyring.alice.encodePkcs8('testing')
).toHaveLength(ENCODED_LENGTH);
});
});
-189
View File
@@ -1,189 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a, u8aToHex } from '@polkadot/util';
import { cryptoWaitReady, encodeAddress as toSS58, setSS58Format } from '@polkadot/util-crypto';
import { PAIRSSR25519 } from '../testing.js';
import { createTestPairs } from '../testingPairs.js';
import { createPair } from './index.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
const TEST_ADDRESS = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
await cryptoWaitReady();
describe('pair', (): void => {
const SIGNATURE = new Uint8Array([80, 191, 198, 147, 225, 207, 75, 88, 126, 39, 129, 109, 191, 38, 72, 181, 75, 254, 81, 143, 244, 79, 237, 38, 236, 141, 28, 252, 134, 26, 169, 234, 79, 33, 153, 158, 151, 34, 175, 188, 235, 20, 35, 135, 83, 120, 139, 211, 233, 130, 1, 208, 201, 215, 73, 80, 56, 98, 185, 196, 11, 8, 193, 14]);
it('has a publicKey', (): void => {
expect(
keyring.alice.publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
expect(
keyring.alice.addressRaw
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('allows signing', (): void => {
expect(
keyring.alice.sign(
new Uint8Array([0x61, 0x62, 0x63, 0x64])
)
).toEqual(SIGNATURE);
});
it('validates a correctly signed message', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
SIGNATURE,
keyring.alice.publicKey
)
).toEqual(true);
});
it('fails a correctly signed message (signer changed)', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
SIGNATURE,
keyring.bob.publicKey
)
).toEqual(false);
});
it('fails a correctly signed message (message changed)', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]),
SIGNATURE,
keyring.alice.publicKey
)
).toEqual(false);
});
it('allows vrf sign and verify', (): void => {
const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]);
expect(
keyring.alice.vrfVerify(
message,
keyring.alice.vrfSign(message),
keyring.alice.publicKey
)
).toBe(true);
});
it('fails vrf sign and verify (publicKey changed)', (): void => {
const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]);
expect(
keyring.alice.vrfVerify(
message,
keyring.alice.vrfSign(message),
keyring.bob.publicKey
)
).toBe(false);
});
it('allows setting/getting of meta', (): void => {
keyring.bob.setMeta({ foo: 'bar', something: 'else' });
expect(keyring.bob.meta).toMatchObject({ foo: 'bar', something: 'else' });
keyring.bob.setMeta({ something: 'thing' });
expect(keyring.bob.meta).toMatchObject({ foo: 'bar', something: 'thing' });
});
it('allows encoding of address with different prefixes', (): void => {
expect(keyring.alice.address).toEqual('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua');
// eslint-disable-next-line deprecation/deprecation
setSS58Format(255);
expect(keyring.alice.address).toEqual('yGHU8YKprxHbHdEv7oUK4rzMZXtsdhcXVG2CAMyC9WhzhjH2k');
// eslint-disable-next-line deprecation/deprecation
setSS58Format(42);
});
it('allows getting public key after decoding', (): void => {
const PASS = 'testing';
const encoded = keyring.alice.encodePkcs8(PASS);
const pair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey });
pair.decodePkcs8(PASS, encoded);
expect(pair.isLocked).toEqual(false);
});
it('allows derivation on the pair', (): void => {
const alice = createPair({ toSS58, type: 'sr25519' }, { publicKey: hexToU8a(PAIRSSR25519[0].p), secretKey: hexToU8a(PAIRSSR25519[0].s) }, {});
const stash = alice.derive('//stash');
const soft = alice.derive('//funding/0');
expect(stash.publicKey).toEqual(hexToU8a(PAIRSSR25519[1].p));
expect(soft.address).toEqual('5ECQNn7UueWHPFda5qUi4fTmTtyCnPvGnuoyVVSj5CboJh9J');
});
it('fails to sign when locked', (): void => {
const pair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey });
expect(pair.isLocked).toEqual(true);
expect((): Uint8Array =>
pair.sign(new Uint8Array([0]))
).toThrow('Cannot sign with a locked key pair');
});
describe('ethereum', (): void => {
const PUBLICDERIVED = new Uint8Array([
3, 129, 53, 27, 27, 70, 210, 96,
43, 9, 146, 187, 93, 85, 49, 249,
193, 105, 107, 8, 18, 254, 178, 83,
75, 104, 132, 173, 196, 126, 46, 29,
139
]);
const SECRETDERIVED = new Uint8Array([
7, 13, 195, 17, 115, 0, 1, 25,
24, 226, 107, 2, 23, 105, 69, 204,
21, 195, 213, 72, 207, 73, 253, 132,
24, 217, 127, 147, 175, 105, 158, 70
]);
it('has a valid address from a known public', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') });
expect(pair.address).toEqual(TEST_ADDRESS);
expect(pair.addressRaw).toEqual(hexToU8a(TEST_ADDRESS));
});
it('has a valid address from a known ethereum address (20 length)', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: new Uint8Array([75, 32, 205, 127, 248, 119, 52, 31, 46, 171, 170, 23, 158, 23, 46, 108, 95, 180, 186, 168]), secretKey: new Uint8Array([]) });
expect(pair.address.toLowerCase()).toEqual('0x4b20cd7ff877341f2eabaa179e172e6c5fb4baa8');
expect(pair.addressRaw).toEqual(hexToU8a('0x4b20cd7ff877341f2eabaa179e172e6c5fb4baa8'));
});
it('converts to json', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: PUBLICDERIVED, secretKey: SECRETDERIVED });
const json = pair.toJson('password');
expect(json.encoding).toEqual({
content: ['pkcs8', 'ethereum'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
expect(json.address).toEqual(u8aToHex(PUBLICDERIVED));
});
});
});
+57 -166
View File
@@ -1,12 +1,12 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util-crypto/types';
import type { EncryptedJsonEncoding, KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta, SignOptions } from '../types.js';
import type { PairInfo } from './types.js';
import { objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util';
import { blake2AsU8a, dilithiumAccountFromPublic, dilithiumPairFromSeed, dilithiumSign, dilithiumSizes, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, ethereumEncode, isDilithium, keccakAsU8a, keyExtractPath, keyFromPath, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto';
import { objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToU8a } from '@polkadot/util';
import { dilithiumAccountFromPublic, dilithiumSign, dilithiumSizes, dilithiumVerify } from '@polkadot/util-crypto';
import { decodePair } from './decode.js';
import { encodePair } from './encode.js';
@@ -19,44 +19,17 @@ interface Setup {
const SIG_TYPE_NONE = new Uint8Array();
const TYPE_FROM_SEED = {
dilithium65: (seed: Uint8Array) => dilithiumPairFromSeed(seed, 'dilithium65'),
dilithium87: (seed: Uint8Array) => dilithiumPairFromSeed(seed, 'dilithium87'),
ecdsa: secp256k1FromSeed,
ed25519: ed25519FromSeed,
ethereum: secp256k1FromSeed,
sr25519: sr25519FromSeed
};
// For the curve types these index Substrate's `MultiSignature`. For the ML-DSA
// types they index the Quantus runtime's `DilithiumSignatureScheme`, where
// `Dilithium87` is variant 0 and `Dilithium65` is variant 1 — a different enum
// that happens to be reached by the same `withType` mechanism, so no special
// casing is needed anywhere upstream of here.
const TYPE_PREFIX = {
// The Quantus runtime's `DilithiumSignatureScheme`: `Dilithium87` is variant 0
// and `Dilithium65` variant 1. Reached through the same `withType` mechanism
// upstream used for Substrate's `MultiSignature`, so nothing above this file
// needs to know which enum it is.
//
// There are no other arms. Upstream's ed25519, sr25519, ecdsa and ethereum were
// removed with their primitives: each falls to Shor's algorithm, and this keyring
// exists to hold keys that do not. quantus/common#6
const TYPE_PREFIX: Record<KeypairType, Uint8Array> = {
dilithium65: new Uint8Array([1]),
dilithium87: new Uint8Array([0]),
ecdsa: new Uint8Array([2]),
ed25519: new Uint8Array([0]),
ethereum: new Uint8Array([2]),
sr25519: new Uint8Array([1])
};
// The ML-DSA arms take a context; the curve arms ignore the extra argument. The
// signature is written out because TypeScript would otherwise infer the narrowest
// common shape — the two-argument curve arms — and reject passing a context at all.
type SignFn = (message: Uint8Array, pair: Partial<Keypair>, context?: Uint8Array) => Uint8Array;
const TYPE_SIGNATURE: Record<KeypairType, SignFn> = {
dilithium65: (m, p, ctx) => dilithiumSign(m, p, 'dilithium65', requireContext(ctx)),
dilithium87: (m, p, ctx) => dilithiumSign(m, p, 'dilithium87', requireContext(ctx)),
ecdsa: (m, p) => secp256k1Sign(m, p, 'blake2'),
// Wrapped rather than passed directly: 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.
ed25519: (m, p) => ed25519Sign(m, p),
ethereum: (m, p) => secp256k1Sign(m, p, 'keccak'),
sr25519: (m, p) => sr25519Sign(m, p)
dilithium87: new Uint8Array([0])
};
/**
@@ -74,30 +47,10 @@ function requireContext (context?: Uint8Array): Uint8Array {
return context;
}
// Every curve arm here is identity or a cheap re-encoding, because on Substrate
// the address *is* the public key. The ML-DSA arms are the exception that the
// rest of this file has to be read in light of: 1952 or 2592 bytes in, 32 out,
// and no way back. `ecdsa` already hints at the shape — it hashes when given an
// uncompressed key — but it stays reversible in the sense that matters, since
// the signature carries a recoverable key. ML-DSA does not, which is why its
// signatures carry the public key explicitly.
const TYPE_ADDRESS = {
dilithium65: dilithiumAccountFromPublic,
dilithium87: dilithiumAccountFromPublic,
ecdsa: (p: Uint8Array) => p.length > 32 ? blake2AsU8a(p) : p,
ed25519: (p: Uint8Array) => p,
ethereum: (p: Uint8Array) => p.length === 20 ? p : keccakAsU8a(secp256k1Expand(p)),
sr25519: (p: Uint8Array) => p
};
function isLocked (secretKey?: Uint8Array): secretKey is undefined {
return !secretKey || u8aEmpty(secretKey);
}
function vrfHash (proof: Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array {
return blake2AsU8a(u8aConcat(context || '', extra || '', proof));
}
/**
* @name createPair
* @summary Creates a keyring pair object
@@ -132,40 +85,22 @@ function vrfHash (proof: Uint8Array, context?: string | Uint8Array, extra?: stri
export function createPair ({ toSS58, type }: Setup, { accountId, publicKey, secretKey }: PairInfo, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, encTypes?: EncryptedJsonEncoding[]): KeyringPair {
const decodePkcs8 = (passphrase?: string, userEncoded?: Uint8Array | null): void => {
// ML-DSA secrets are 4032/4896 bytes, which is neither of the two lengths
// decodePair would otherwise try. It has to be told.
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes, isDilithium(type)
? dilithiumSizes(type).secretKey
: undefined
);
// decodePair would otherwise try, so it has to be told.
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes, dilithiumSizes(type).secretKey);
// Upstream decides "is this a secret key or a seed?" by length, because for
// the curve schemes a 64-byte blob is a secret and a 32-byte one is a seed.
// An ML-DSA secret is neither, and would take the seed branch and be fed to
// keygen as entropy — producing a valid, wrong key in silence. The type
// knows the answer, so ask it.
if (isDilithium(type) || decoded.secretKey.length === 64) {
publicKey = decoded.publicKey;
secretKey = decoded.secretKey;
} else {
const pair = TYPE_FROM_SEED[type](decoded.secretKey);
publicKey = pair.publicKey;
secretKey = pair.secretKey;
}
publicKey = decoded.publicKey;
secretKey = decoded.secretKey;
// The public key has only now arrived. If the pair was constructed from JSON
// it has been reporting an address carried as data since then, so check the
// two agree.
//
// This is not a paranoid check. For every other scheme the address *is* the
// public key, so a JSON file whose `address` field has been edited cannot
// decode at all. Here it decodes perfectly and yields a pair that reports an
// address its key does not control — a user would see the attacker's address
// in their own wallet and believe they held it.
// This is not a paranoid check. A JSON file whose `address` field has been
// edited decodes perfectly and would yield a pair reporting an address its
// key does not control: a user would see an attacker's address in their own
// wallet and believe they held it.
if (accountId) {
const derived = TYPE_ADDRESS[type](publicKey);
if (!u8aEq(derived, accountId)) {
if (!u8aEq(dilithiumAccountFromPublic(publicKey), accountId)) {
throw new Error('Decoded public key does not match the address in the account JSON');
}
@@ -182,31 +117,19 @@ export function createPair ({ toSS58, type }: Setup, { accountId, publicKey, sec
return encoded;
};
// While an ML-DSA pair restored from JSON is locked, its public key is still
// inside the encrypted blob and the account id cannot be computed — so use the
// one carried alongside. `decodePkcs8` clears it once the real key arrives and
// has been checked against it.
// While a pair restored from JSON, or added by address, has no public key,
// its account id cannot be computed (it is a one-way Poseidon2 hash of that
// key), so use the one carried alongside. `decodePkcs8` clears it once the
// real key arrives and has been checked against it.
const addressRawOf = (): Uint8Array =>
accountId ?? TYPE_ADDRESS[type](publicKey);
const encodeAddress = (): string => {
const raw = addressRawOf();
return type === 'ethereum'
? ethereumEncode(raw)
: toSS58(raw);
};
accountId ?? dilithiumAccountFromPublic(publicKey);
return {
get address (): string {
return encodeAddress();
return toSS58(addressRawOf());
},
get addressRaw (): Uint8Array {
const raw = addressRawOf();
return type === 'ethereum'
? raw.slice(-20)
: raw;
return addressRawOf();
},
get isLocked (): boolean {
return isLocked(secretKey);
@@ -222,24 +145,12 @@ export function createPair ({ toSS58, type }: Setup, { accountId, publicKey, sec
},
// eslint-disable-next-line sort-keys
decodePkcs8,
derive: (suri: string, meta?: KeyringPair$Meta): KeyringPair => {
if (type === 'ethereum') {
throw new Error('Unable to derive on this keypair');
} else if (isDilithium(type)) {
// Not "not implemented" — not possible. A child here would have to come
// from this pair's key material, and ML-DSA keys are not derivable from
// one another at all; the Quantus tree derives every account from the
// mnemonic independently. So the caller needs the mnemonic, not this
// pair, and saying that is more use than a generic refusal.
throw new Error(`Unable to derive from an existing ${type} pair; derive from the mnemonic with createFromUri instead`);
} else if (isLocked(secretKey)) {
throw new Error('Cannot derive on a locked keypair');
}
const { path } = keyExtractPath(suri);
const derived = keyFromPath({ publicKey, secretKey }, path, type);
return createPair({ toSS58, type }, derived, meta, null);
derive: (_suri: string, _meta?: KeyringPair$Meta): KeyringPair => {
// Not "not implemented" — not possible. A child here would have to come
// from this pair's key material, and ML-DSA keys are not derivable from
// one another at all; the Quantus tree derives every account from the
// mnemonic independently. So the caller needs the mnemonic, not this pair.
throw new Error(`Unable to derive from an existing ${type} pair; derive from the mnemonic with createFromUri instead`);
},
encodePkcs8: (passphrase?: string): Uint8Array => {
return recode(passphrase);
@@ -259,57 +170,37 @@ export function createPair ({ toSS58, type }: Setup, { accountId, publicKey, sec
options.withType
? TYPE_PREFIX[type]
: SIG_TYPE_NONE,
TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey }, options.context)
dilithiumSign(u8aToU8a(message), { publicKey, secretKey }, type, requireContext(options.context))
);
},
toJson: (passphrase?: string): KeyringPair$Json => {
// NOTE: For ecdsa and ethereum, the publicKey cannot be extracted from the address. For these
// pass the hex-encoded publicKey through to the address portion of the JSON (before decoding)
// unless the publicKey is already an address
const address = ['ecdsa', 'ethereum'].includes(type)
? publicKey.length === 20
? u8aToHex(publicKey)
: u8aToHex(secp256k1Compress(publicKey))
: encodeAddress();
return pairToJson(type, { address, meta }, recode(passphrase), !!passphrase);
return pairToJson(type, { address: toSS58(addressRawOf()), meta }, recode(passphrase), !!passphrase);
},
unlock: (passphrase?: string): void => {
return decodePkcs8(passphrase);
},
verify: (message: string | Uint8Array, signature: string | Uint8Array, signerPublic: string | Uint8Array): boolean => {
return signatureVerify(message, signature, TYPE_ADDRESS[type](u8aToU8a(signerPublic))).isValid;
verify: (message: string | Uint8Array, signature: string | Uint8Array, signerPublic: string | Uint8Array, context: Uint8Array = new Uint8Array()): boolean => {
const pk = u8aToU8a(signerPublic);
const sig = u8aToU8a(signature);
const { signature: sigLength } = dilithiumSizes(type);
// Accept a bare signature or the chain's `signature ‖ publicKey` form; the
// verifier needs the latter, and the public key is at hand either way.
const sigWithPublic = sig.length === sigLength
? u8aConcat(sig, pk)
: sig;
// Unlike signing, a wrong context here cannot produce anything harmful —
// only `false` — so the empty context used for raw bytes is the default.
return dilithiumVerify(u8aToU8a(message), sigWithPublic, dilithiumAccountFromPublic(pk), type, context);
},
vrfSign: (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array => {
if (isLocked(secretKey)) {
throw new Error('Cannot sign with a locked key pair');
}
if (type === 'sr25519') {
return sr25519VrfSign(message, { secretKey }, context, extra);
}
// There is no ML-DSA VRF. The construction below fakes one out of an
// ordinary signature, which is sound only because the underlying schemes
// are deterministic and unique — properties ML-DSA's signature does not
// have in the form this needs. Refuse rather than produce something that
// looks like a VRF output and is not verifiable as one.
if (isDilithium(type)) {
throw new Error(`VRF signing is not available for ${type}`);
}
const proof = TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey });
return u8aConcat(vrfHash(proof, context, extra), proof);
vrfSign: (): Uint8Array => {
// There is no ML-DSA VRF, and faking one out of an ordinary signature needs
// uniqueness ML-DSA does not have in the form required.
throw new Error(`VRF signing is not available for ${type}`);
},
vrfVerify: (message: string | Uint8Array, vrfResult: Uint8Array, signerPublic: Uint8Array | string, context?: string | Uint8Array, extra?: string | Uint8Array): boolean => {
if (type === 'sr25519') {
return sr25519VrfVerify(message, vrfResult, publicKey, context, extra);
}
const result = signatureVerify(message, u8aConcat(TYPE_PREFIX[type], vrfResult.subarray(32)), TYPE_ADDRESS[type](u8aToU8a(signerPublic)));
return result.isValid && u8aEq(vrfResult.subarray(0, 32), vrfHash(vrfResult.subarray(32), context, extra));
vrfVerify: (): boolean => {
throw new Error(`VRF verification is not available for ${type}`);
}
};
}
+2 -2
View File
@@ -18,7 +18,7 @@ const json: KeyringPair$Json = {
address,
encoded: '',
encoding: {
content: ['pkcs8', 'ed25519'],
content: ['pkcs8', 'dilithium65'],
type: 'none',
version: '0'
},
@@ -46,7 +46,7 @@ const pair: KeyringPair = {
new Uint8Array(64),
toJson: (_passphrase?: string): KeyringPair$Json =>
json,
type: 'ed25519',
type: 'dilithium65',
unlock: (_passphrase?: string): void =>
undefined,
verify: (_message: Uint8Array, _signature: Uint8Array): boolean =>
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { createTestPairs } from '../testingPairs.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('toJson', (): void => {
it('creates an unencoded output with no passphrase', (): void => {
expect(
keyring.alice.toJson()
).toMatchObject({
address: '5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua',
encoded: 'MFMCAQEwBQYDK2VwBCIEIEFsaWNlICAgICAgICAgICAgICAgICAgICAgICAgICAg0XKnTNpMhlkSwyugqApXrmmrrkEOXMtZ3uhOL0Qy20+hIwMhANFyp0zaTIZZEsMroKgKV65pq65BDlzLWd7oTi9EMttP',
encoding: {
content: ['pkcs8', 'ed25519'],
type: ['none'],
version: '3'
},
meta: {
isTesting: true,
name: 'alice'
}
});
});
it('creates an encoded output with passphrase', (): void => {
const json = keyring.alice.toJson('testing');
expect(json.encoded).toHaveLength(268);
expect(json).toMatchObject({
address: '5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua',
encoding: {
content: ['pkcs8', 'ed25519'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
}
});
});
});
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady, ed25519PairFromSeed, encodeAddress as toSS58, randomAsU8a, secp256k1PairFromSeed, sr25519PairFromSeed } from '@polkadot/util-crypto';
import { createPair } from './index.js';
const MESSAGE = 'this is a test message';
const CONTEXT = 'some context';
await cryptoWaitReady();
const ecdsa = createPair({ toSS58, type: 'ecdsa' }, secp256k1PairFromSeed(randomAsU8a()));
const ed25519 = createPair({ toSS58, type: 'ed25519' }, ed25519PairFromSeed(randomAsU8a()));
const sr25519 = createPair({ toSS58, type: 'sr25519' }, sr25519PairFromSeed(randomAsU8a()));
describe('vrf', (): void => {
it('has deterministic signature values for ecdsa', (): void => {
const sig1 = ecdsa.vrfSign(MESSAGE, CONTEXT);
const sig2 = ecdsa.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1)).toEqual(u8aToHex(sig2));
expect(ecdsa.vrfVerify(MESSAGE, sig1, ecdsa.publicKey, CONTEXT)).toEqual(true);
expect(ecdsa.vrfVerify(MESSAGE, sig2, ecdsa.publicKey, CONTEXT)).toEqual(true);
});
it('has deterministic signature values for ed25519', (): void => {
const sig1 = ed25519.vrfSign(MESSAGE, CONTEXT);
const sig2 = ed25519.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1)).toEqual(u8aToHex(sig2));
expect(ed25519.vrfVerify(MESSAGE, sig1, ed25519.publicKey, CONTEXT)).toEqual(true);
expect(ed25519.vrfVerify(MESSAGE, sig2, ed25519.publicKey, CONTEXT)).toEqual(true);
});
it('has deterministic signature values for sr25519', (): void => {
const sig1 = sr25519.vrfSign(MESSAGE, CONTEXT);
const sig2 = sr25519.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1.slice(0, 32))).toEqual(u8aToHex(sig2.slice(0, 32)));
expect(sr25519.vrfVerify(MESSAGE, sig1, sr25519.publicKey, CONTEXT)).toEqual(true);
expect(sr25519.vrfVerify(MESSAGE, sig2, sr25519.publicKey, CONTEXT)).toEqual(true);
});
});
-109
View File
@@ -1,109 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
// From https://github.com/paritytech/substrate/wiki/Secret-URI-Test-Vectors
import type { KeypairType } from '@polkadot/util-crypto/types';
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import Keyring from './index.js';
const PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
const ETHEREUM_PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const TESTS = {
ecdsa: [
{
pk: '0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1',
ss: '5C7C2Z5sWbytvHpuLTvzKunnnRwQxft1jiqrLD5rhucQ5S9X',
uri: `${PHRASE}//Alice`
}
],
ethereum: [
{
pk: '0x0381351b1b46d2602b0992bb5d5531f9c1696b0812feb2534b6884adc47e2e1d8b',
ss: '0x31ea8795EE32D782C8ff41a5C68Dcbf0F5B27f6d',
uri: `${ETHEREUM_PHRASE}/m/44'/60'/0'/0/0`
},
{
pk: '0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f',
ss: '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac',
uri: `${PHRASE}/m/44'/60'/0'/0/0`
},
{
pk: '0x033bc19e36ff1673910575b6727a974a9abd80c9a875d41ab3e2648dbfb9e4b518',
ss: '0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0',
uri: `${PHRASE}/m/44'/60'/0'/0/1`
}
],
sr25519: [
{
pk: '0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a',
ss: '5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV',
uri: PHRASE
},
{
pk: '0xb69355deefa7a8f33e9297f5af22e680f03597a99d4f4b1c44be47e7a2275802',
ss: '5GC6LfpV352HtJPySfAecb5JdePtf4R9Vq49NUU8RhzgBqgq',
uri: `${PHRASE}///password`
},
{
pk: '0x40b9675df90efa6069ff623b0fdfcf706cd47ca7452a5056c7ad58194d23440a',
ss: '5DXZzrDxHbkQov4QBAY4TjpwnHCMrKXkomTnKSw8UArBEY5v',
uri: `${PHRASE}/foo`
},
{
pk: '0x547d4a55642ec7ebadc0bd29b6e570b8c926059b3c0655d4948075e9a7e6f31e',
ss: '5DyV6fZuvPemWrUqBgWwTSgoV86w6xms3KhkFU6cQcWxU8eP',
uri: `${PHRASE}//foo`
},
{
pk: '0x3841947ffcde6f5fef26fb68b59bb8665637e30e32ec2051f99cf6b9c674fe09',
ss: '5DLU27is5iViNopQb2KxsTyPx6j4vCu8X3sk3j3NNLkPCqKM',
uri: `${PHRASE}//foo/bar`
},
{
pk: '0xdc142f7476a7b0aa262aeccf207f1d18daa90762db393006741e8a31f39dbc53',
ss: '5H3GPTqDSpjkfDwbHy12PD6BWm8jvGSX4xYC8UMprHpTPcRg',
uri: `${PHRASE}/foo//bar`
},
{
pk: '0xa2e56b06407a6d1e819d2fc33fa0ec604b29c2e868b70b3696bb049b8725934b',
ss: '5FkHmNgbg64MwStgCyDi2Uw3ufFu11mqQgmWT9uwK4Lghvpv',
uri: `${PHRASE}//foo/bar//42/69`
},
{
pk: '0x0e0d24e3e1ff2c07f269c99e2e0df8681fda1851ac42fc846ca2daaa90cd8f14',
ss: '5CP8S23JBNXYNpJsL7ESPJBNnUZE6itcfM4EnDxEhaVEU6dT',
uri: `${PHRASE}//foo/bar//42/69///password`
},
{
pk: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d',
ss: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
uri: `${PHRASE}//Alice`
}
]
};
await cryptoWaitReady();
describe('keyring.addFromUri', (): void => {
for (const [type, tests] of Object.entries(TESTS)) {
const keyring = new Keyring({ type: type as KeypairType });
describe(`${type}`, (): void => {
tests.forEach(({ pk, ss, uri }): void => {
it(`creates ${uri}`, (): void => {
const pair = keyring.addFromUri(uri, {}, type as KeypairType);
expect(u8aToHex(pair.publicKey)).toEqual(pk);
expect(pair.address).toEqual(ss);
});
});
});
}
});
+33 -136
View File
@@ -1,151 +1,48 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringInstance, KeyringOptions } from './types.js';
import { hexToU8a } from '@polkadot/util';
import { createPair } from './pair/index.js';
import { Keyring } from './keyring.js';
interface PairDef {
name?: string;
p: HexString;
s: HexString;
seed?: string;
type: KeypairType
}
// NOTE This is not great since we have the secretKey here explicitly, but a testing
// keyring is for testing - what happens is that in most cases the keyring is initialises
// before anything else. Since the sr25519 crypto is async, this creates problems with
// adding the keys when only the keyring is used.
export const PAIRSSR25519: PairDef[] = [
{
p: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d',
s: '0x98319d4ff8a9508c4bb0cf0b5a78d760a0b2082c02775e6e82370816fedfff48925a225d97aa00682d6a59b95b18780c10d7032336e88f3442b42361f4a66011', // nosemgrep
seed: 'Alice',
type: 'sr25519'
},
{
p: '0xbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f',
s: '0xe8da6c9d810e020f5e3c7f5af2dea314cbeaa0d72bc6421e92c0808a0c584a6046ab28e97c3ffc77fe12b5a4d37e8cd4afbfebbf2391ffc7cb07c0f38c023efd', // nosemgrep
seed: 'Alice//stash',
type: 'sr25519'
},
{
p: '0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48',
s: '0x081ff694633e255136bdb456c20a5fc8fed21f8b964c11bb17ff534ce80ebd5941ae88f85d0c1bfc37be41c904e1dfc01de8c8067b0d6d5df25dd1ac0894a325', // nosemgrep
seed: 'Bob',
type: 'sr25519'
},
{
p: '0xfe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e',
s: '0xc006507cdfc267a21532394c49ca9b754ca71de21e15a1cdf807c7ceab6d0b6c3ed408d9d35311540dcd54931933e67cf1ea10d46f75408f82b789d9bd212fde', // nosemgrep
seed: 'Bob//stash',
type: 'sr25519'
},
{
p: '0x90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22',
s: '0xa8f2d83016052e5d6d77b2f6fd5d59418922a09024cda701b3c34369ec43a7668faf12ff39cd4e5d92bb773972f41a7a5279ebc2ed92264bed8f47d344f8f18c', // nosemgrep
seed: 'Charlie',
type: 'sr25519'
},
{
p: '0x306721211d5404bd9da88e0204360a1a9ab8b87c66c1bc2fcdd37f3c2222cc20',
s: '0x20e05482ca4677e0edbc58ae9a3a59f6ed3b1a9484ba17e64d6fe8688b2b7b5d108c4487b9323b98b11fe36cb301b084e920f7b7895536809a6d62a451b25568', // nosemgrep
seed: 'Dave',
type: 'sr25519'
},
{
p: '0xe659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e',
s: '0x683576abfd5dc35273e4264c23095a1bf21c14517bece57c7f0cc5c0ed4ce06a3dbf386b7828f348abe15d76973a72009e6ef86a5c91db2990cb36bb657c6587', // nosemgrep
seed: 'Eve',
type: 'sr25519'
},
{
p: '0x1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c',
s: '0xb835c20f450079cf4f513900ae9faf8df06ad86c681884122c752a4b2bf74d4303e4f21bc6cc62bb4eeed5a9cce642c25e2d2ac1464093b50f6196d78e3a7426', // nosemgrep
seed: 'Ferdie',
type: 'sr25519'
}
];
export const PAIRSETHEREUM: PairDef[] = [
{
name: 'Alith',
p: '0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f',
s: '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133', // nosemgrep
type: 'ethereum'
},
{
name: 'Baltathar',
p: '0x033bc19e36ff1673910575b6727a974a9abd80c9a875d41ab3e2648dbfb9e4b518',
s: '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b', // nosemgrep
type: 'ethereum'
},
{
name: 'Charleth',
p: '0x0234637bdc0e89b5d46543bcbf8edff329d2702bc995e27e9af4b1ba009a3c2a5e',
s: '0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b', // nosemgrep
type: 'ethereum'
},
{
name: 'Dorothy',
p: '0x02a00d60b2b408c2a14c5d70cdd2c205db8985ef737a7e55ad20ea32cc9e7c417c',
s: '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68', // nosemgrep
type: 'ethereum'
},
{
name: 'Ethan',
p: '0x025cdc005b752651cd3f728fb9192182acb3a9c89e19072cbd5b03f3ee1f1b3ffa',
s: '0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4', // nosemgrep
type: 'ethereum'
},
{
name: 'Faith',
p: '0x037964b6c9d546da4646ada28a99e34acaa1d14e7aba861a9055f9bd200c8abf74',
s: '0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df', // nosemgrep
type: 'ethereum'
}
];
function createMeta (name?: string, seed?: string) {
if (!name && !seed) {
throw new Error('Testing pair should have either a name or a seed');
}
return {
isTesting: true,
name: name || seed?.replace('//', '_').toLowerCase()
};
interface DevAccount {
name: string;
/** The byte the 32-byte seed is filled with. */
seed: number;
}
/**
* @name testKeyring
* @summary Create an instance of Keyring pre-populated with locked test accounts
* @description The test accounts (i.e. alice, bob, dave, eve, ferdie)
* are available on the dev chain and each test account is initialized with DOT funds.
* The accounts a Quantus dev chain endows at genesis.
*
* From `chain:primitives/dilithium-crypto/src/pair.rs`: ML-DSA-87 keys generated
* directly from a 32-byte seed filled with one byte, with no HD derivation. Their
* account ids are pinned against `quantus-cli` in `@quantus/crypto`'s tests
* (`dev_account_ids_match_the_cli`), e.g. crystal_alice is
* `qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7`.
*
* They replace upstream's sr25519 Alice…Ferdie and Ethereum Alith…Faith, which
* this keyring can no longer hold. quantus/common#6
*/
export function createTestKeyring (options: KeyringOptions = {}, isDerived = true): KeyringInstance {
const keyring = new Keyring(options);
const pairs = options.type === 'ethereum'
? PAIRSETHEREUM
: PAIRSSR25519;
export const DEV_ACCOUNTS: DevAccount[] = [
{ name: 'crystal_alice', seed: 0 },
{ name: 'dilithium_bob', seed: 1 },
{ name: 'crystal_charlie', seed: 2 }
];
for (const { name, p, s, seed, type } of pairs) {
const meta = createMeta(name, seed);
const pair = !isDerived && !name && seed
? keyring.addFromUri(seed, meta, options.type)
: keyring.addPair(
createPair(
{ toSS58: keyring.encodeAddress, type },
{ publicKey: hexToU8a(p), secretKey: hexToU8a(s) },
meta
)
);
/**
* @name testKeyring
* @summary Create an instance of Keyring pre-populated with the dev accounts
* @description crystal_alice, dilithium_bob and crystal_charlie, as a Quantus
* dev chain endows them. They are always ML-DSA-87, whatever `options.type` says,
* because that is what the chain made them; `options` sets the keyring's default
* type and SS58 format for anything added afterwards. `_isDerived` is accepted for
* upstream's signature and ignored: these accounts have no derivation.
*/
export function createTestKeyring (options: KeyringOptions = {}, _isDerived = true): KeyringInstance {
const keyring = new Keyring(options);
for (const { name, seed } of DEV_ACCOUNTS) {
const pair = keyring.addFromSeed(new Uint8Array(32).fill(seed), { isTesting: true, name }, 'dilithium87');
pair.lock = (): void => {
// we don't have lock/unlock functionality here
-79
View File
@@ -1,79 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import Keyring from './index.js';
import { createTestPairs } from './testingPairs.js';
const TEST_ADD = '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac';
await cryptoWaitReady();
describe('testingPairs', (): void => {
it('creates without failing', (): void => {
expect(
Object.keys(createTestPairs())
).toHaveLength(2 + 0 + 7); // stash, session, pairs
});
it('has the correct address for Alice (non-HDKD)', (): void => {
expect(
createTestPairs({ type: 'ed25519' }, false).alice.address
).toEqual('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua');
});
it('has the correct address for Alice (HDKD)', (): void => {
expect(
createTestPairs({ type: 'ed25519' }).alice.address
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
it('has the correct address for Alith (Eth)', (): void => {
expect(
createTestPairs({ type: 'ethereum' }).Alith.address
).toEqual(TEST_ADD);
});
it('has the correct address for Alith (Eth), same as obtained by createFromUri', (): void => {
const keyring = new Keyring({ type: 'ethereum' });
const pair = keyring.createFromUri('0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133');
expect(pair?.address).toEqual(TEST_ADD);
});
describe('checks eth test addresses', (): void => {
const ring = createTestPairs({ type: 'ethereum' });
const keyring = new Keyring({ type: 'ethereum' });
// priv keys generated by ganache-cli --mnemonic "bottom drive obey lake curtain smoke basket hold race lonely fit walk"
const privKeys: string[] = ['0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133',
'0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b',
'0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b',
'0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68',
'0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4',
'0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df',
'0x96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18',
'0x0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab',
'0x4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8',
'0x94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b'];
// @ts-expect-error We should not delete from the maps, however this is a test
delete ring.nobody;
Object
.keys(ring)
.filter((_, i) => i < 6)
.forEach((testKeyring, i) => {
it(`checks #${i}`, (): void => {
expect(
u8aToHex(ring[testKeyring].publicKey)
).toEqual(
u8aToHex(keyring.createFromUri(privKeys[i]).publicKey)
);
});
});
});
});
+6 -29
View File
@@ -1,7 +1,6 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringOptions, KeyringPair } from './types.js';
import { nobody } from './pair/nobody.js';
@@ -9,39 +8,17 @@ import { createTestKeyring } from './testing.js';
export interface TestKeyringMap {
nobody: KeyringPair;
[index: string]: KeyringPair;
}
export interface TestKeyringMapSubstrate extends TestKeyringMap {
alice: KeyringPair;
bob: KeyringPair;
charlie: KeyringPair;
dave: KeyringPair;
eve: KeyringPair;
ferdie: KeyringPair;
/** The Quantus dev accounts, by name. See `DEV_ACCOUNTS`. */
export interface TestKeyringMapQuantus extends TestKeyringMap {
crystal_alice: KeyringPair;
crystal_charlie: KeyringPair;
dilithium_bob: KeyringPair;
}
export interface TestKeyringMapEthereum extends TestKeyringMap {
Alith: KeyringPair;
Baltathar: KeyringPair;
Charleth: KeyringPair;
Dorothy: KeyringPair;
Ethan: KeyringPair;
Faith: KeyringPair;
}
export type DetectMap<O extends KeyringOptions | undefined> = DetectPairType<O> extends 'ethereum'
? TestKeyringMapEthereum
: TestKeyringMapSubstrate;
export type DetectPairType<O extends KeyringOptions | undefined> = O extends KeyringOptions
? O['type'] extends KeypairType
? O['type']
: 'sr25519'
: 'sr25519';
export function createTestPairs <O extends KeyringOptions, M = DetectMap<O>> (options?: O, isDerived = true): M {
export function createTestPairs <O extends KeyringOptions, M = TestKeyringMapQuantus> (options?: O, isDerived = true): M {
const keyring = createTestKeyring(options, isDerived);
const pairs = keyring.getPairs();
const map: TestKeyringMap = { nobody: nobody() };
+6 -2
View File
@@ -7,7 +7,7 @@ import type { EncryptedJson, Keypair, KeypairType, Prefix } from '@polkadot/util
export interface KeyringOptions {
/** The ss58Format to use for address encoding (defaults to 42) */
ss58Format?: Prefix;
/** The type of keyring to create (defaults to ed25519) */
/** The type of keyring to create (defaults to dilithium65, ML-DSA-65) */
type?: KeypairType;
}
@@ -103,7 +103,11 @@ export interface KeyringPair {
sign (message: string | Uint8Array, options?: SignOptions): Uint8Array;
toJson (passphrase?: string): KeyringPair$Json;
unlock (passphrase?: string): void;
verify (message: string | Uint8Array, signature: Uint8Array, signerPublic: string | Uint8Array): boolean;
/**
* Verify an ML-DSA signature, bare or as the chain's `signature ‖ publicKey`,
* under `context` (default: the empty context used for raw bytes).
*/
verify (message: string | Uint8Array, signature: Uint8Array, signerPublic: string | Uint8Array, context?: Uint8Array): boolean;
vrfSign (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array;
vrfVerify (message: string | Uint8Array, vrfResult: Uint8Array, signerPublic: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): boolean;
}
+1 -1
View File
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
"version": "14.0.3-quantus.2",
"version": "14.0.3-quantus.3",
"main": "index.js",
"dependencies": {
"@polkadot/util": "14.0.3",
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '14.0.3-quantus.2' };
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };
+3 -3
View File
@@ -20,7 +20,7 @@
"./packageDetect.cjs"
],
"type": "module",
"version": "14.0.3-quantus.2",
"version": "14.0.3-quantus.3",
"browser": {
"crypto": false,
"stream": false
@@ -29,13 +29,13 @@
"dependencies": {
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@polkadot/networks": "14.0.3-quantus.2",
"@polkadot/networks": "14.0.3-quantus.3",
"@polkadot/util": "14.0.3",
"@polkadot/wasm-crypto": "^7.5.3",
"@polkadot/wasm-util": "^7.5.3",
"@polkadot/x-bigint": "14.0.3",
"@polkadot/x-randomvalues": "14.0.3",
"@quantus/crypto": "^0.1.0",
"@quantus/crypto": "^0.3.0",
"@scure/base": "^1.1.7",
"@scure/sr25519": "^0.2.0",
"tslib": "^2.8.0"
@@ -1,26 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { waitReady } from '@polkadot/wasm-crypto';
import { deriveAddress } from './index.js';
describe('deriveAddress', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
it('derives a known path', (): void => {
expect(
deriveAddress('5CZtJLXtVzrBJq1fMWfywDa6XuRwXekGdShPR4b8i9GWSbzB', '/joe/polkadot/0')
).toEqual('5GZ4srnepXvdsuNVoxCGyVZd8ScDm4gkGLTKuaGARy9akjTa');
});
it('fails on hard paths', (): void => {
expect(
() => deriveAddress('5CZtJLXtVzrBJq1fMWfywDa6XuRwXekGdShPR4b8i9GWSbzB', '//bob')
).toThrow(/Expected suri to contain a combination of non-hard paths/);
});
});
@@ -1,36 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveJunction } from '../key/DeriveJunction.js';
import type { Prefix } from './types.js';
import { keyExtractPath } from '../key/index.js';
import { sr25519DerivePublic } from '../sr25519/index.js';
import { decodeAddress } from './decode.js';
import { encodeAddress } from './encode.js';
function filterHard ({ isHard }: DeriveJunction): boolean {
return isHard;
}
/**
* @name deriveAddress
* @summary Creates a sr25519 derived address from the supplied and path.
* @description
* Creates a sr25519 derived address based on the input address/publicKey and the uri supplied.
*/
export function deriveAddress (who: string | Uint8Array, suri: string, ss58Format?: Prefix): string {
const { path } = keyExtractPath(suri);
if (!path.length || path.every(filterHard)) {
throw new Error('Expected suri to contain a combination of non-hard paths');
}
let publicKey = decodeAddress(who);
for (const { chainCode } of path) {
publicKey = sr25519DerivePublic(publicKey, chainCode);
}
return encodeAddress(publicKey, ss58Format);
}
@@ -1,13 +1,13 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from '../secp256k1/types.js';
import type { HashType } from './hasher.js';
import type { Prefix } from './types.js';
import { u8aConcat } from '@polkadot/util';
import { hasher } from '../secp256k1/hasher.js';
import { encodeAddress } from './encode.js';
import { hasher } from './hasher.js';
/**
* @name evmToAddress
@@ -1,11 +1,12 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from './types.js';
/** Which hash an EVM ↔ Substrate address conversion uses. */
import { blake2AsU8a } from '../blake2/index.js';
import { keccakAsU8a } from '../keccak/index.js';
export type HashType = 'blake2' | 'keccak';
export function hasher (hashType: HashType, data: Uint8Array | string, onlyJs?: boolean): Uint8Array {
return hashType === 'keccak'
? keccakAsU8a(data, undefined, onlyJs)
@@ -5,7 +5,6 @@ export { addressToEvm } from './addressToEvm.js';
export { checkAddress } from './check.js';
export { checkAddressChecksum } from './checksum.js';
export { decodeAddress } from './decode.js';
export { deriveAddress } from './derive.js';
export { encodeAddress } from './encode.js';
export { encodeDerivedAddress } from './encodeDerived.js';
export { encodeMultiAddress } from './encodeMulti.js';
-4
View File
@@ -15,7 +15,6 @@ export * from './blake2/index.js';
export * from './crypto.js';
export * from './dilithium.js';
export * from './dilithium/index.js';
export * from './ed25519/index.js';
export * from './ethereum/index.js';
export * from './hd/index.js';
export * from './hmac/index.js';
@@ -28,8 +27,5 @@ export * from './networks.js';
export * from './pbkdf2/index.js';
export * from './random/index.js';
export * from './scrypt/index.js';
export * from './secp256k1/index.js';
export * from './sha/index.js';
export * from './signature/index.js';
export * from './sr25519/index.js';
export * from './xxhash/index.js';
@@ -1,18 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { compactAddLength, isU8a, stringToU8a, u8aConcat } from '@polkadot/util';
import { blake2AsU8a } from '../blake2/asU8a.js';
const HDKD = compactAddLength(stringToU8a('Ed25519HDKD'));
export function ed25519DeriveHard (seed: Uint8Array, chainCode: Uint8Array): Uint8Array {
if (!isU8a(chainCode) || chainCode.length !== 32) {
throw new Error('Invalid chainCode passed to derive');
}
return blake2AsU8a(
u8aConcat(HDKD, seed, chainCode)
);
}
-13
View File
@@ -1,13 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/**
* @summary Implements ed25519 operations
*/
export { ed25519DeriveHard } from './deriveHard.js';
export { ed25519PairFromRandom } from './pair/fromRandom.js';
export { ed25519PairFromSecret } from './pair/fromSecret.js';
export { ed25519PairFromSeed } from './pair/fromSeed.js';
export { ed25519PairFromString } from './pair/fromString.js';
export { ed25519Sign } from './sign.js';
export { ed25519Verify } from './verify.js';
@@ -1,28 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { Keypair } from '../../types.js';
import { ed25519PairFromRandom } from '../index.js';
describe('ed25519PairFromRandom', (): void => {
let keypair: Keypair;
beforeEach((): void => {
keypair = ed25519PairFromRandom();
});
it('generates a valid publicKey', (): void => {
expect(
keypair.publicKey
).toHaveLength(32);
});
it('generates a valid secretKey', (): void => {
expect(
keypair.secretKey
).toHaveLength(64);
});
});
@@ -1,25 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { randomAsU8a } from '../../random/index.js';
import { ed25519PairFromSeed } from './fromSeed.js';
/**
* @name ed25519PairFromRandom
* @summary Creates a new public/secret keypair.
* @description
* Returns a new generate object containing a `publicKey` & `secretKey`.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromRandom } from '@polkadot/util-crypto';
*
* ed25519PairFromRandom(); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromRandom (): Keypair {
return ed25519PairFromSeed(randomAsU8a());
}
@@ -1,33 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { ed25519PairFromSecret } from '../index.js';
describe('ed25519PairFromSecret', (): void => {
const secretKey = new Uint8Array([
18, 52, 86, 120, 144, 18, 52, 86,
120, 144, 18, 52, 86, 120, 144, 18,
18, 52, 86, 120, 144, 18, 52, 86,
120, 144, 18, 52, 86, 120, 144, 18,
180, 114, 93, 155, 165, 255, 217, 82,
16, 250, 209, 11, 193, 10, 88, 218,
190, 190, 41, 193, 236, 252, 1, 152,
216, 214, 0, 41, 45, 138, 13, 53
]);
it('generates a valid publicKey/secretKey pair', (): void => {
expect(
ed25519PairFromSecret(secretKey)
).toEqual({
publicKey: new Uint8Array([
180, 114, 93, 155, 165, 255, 217, 82,
16, 250, 209, 11, 193, 10, 88, 218,
190, 190, 41, 193, 236, 252, 1, 152,
216, 214, 0, 41, 45, 138, 13, 53
]),
secretKey
});
});
});
@@ -1,29 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
/**
* @name ed25519PairFromSecret
* @summary Creates a new public/secret keypair from a secret.
* @description
* Returns a object containing a `publicKey` & `secretKey` generated from the supplied secret.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromSecret } from '@polkadot/util-crypto';
*
* ed25519PairFromSecret(...); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromSecret (secretKey: Uint8Array): Keypair {
if (secretKey.length !== 64) {
throw new Error('Invalid secretKey provided');
}
return {
publicKey: secretKey.slice(32),
secretKey
};
}
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { stringToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { ed25519PairFromSeed } from '../index.js';
describe('ed25519PairFromSeed', (): void => {
// NOTE: Aligned with Rust test, b"12345678901234567890123456789012"
const TEST = stringToU8a('12345678901234567890123456789012');
const RESULT = {
publicKey: new Uint8Array([
0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51,
0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e,
0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65,
0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee
]),
secretKey: new Uint8Array([
49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50,
// public part
0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51,
0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e,
0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65,
0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee
])
};
beforeEach(async (): Promise<void> => {
await waitReady();
});
it('generates a valid publicKey/secretKey pair (u8a)', (): void => {
[true, false].forEach((onlyJs): void => {
expect(
ed25519PairFromSeed(TEST, onlyJs)
).toEqual(RESULT);
});
});
});
@@ -1,41 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { ed25519 } from '@noble/curves/ed25519';
import { hasBigInt, u8aConcatStrict } from '@polkadot/util';
import { ed25519KeypairFromSeed, isReady } from '@polkadot/wasm-crypto';
/**
* @name ed25519PairFromSeed
* @summary Creates a new public/secret keypair from a seed.
* @description
* Returns a object containing a `publicKey` & `secretKey` generated from the supplied seed.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromSeed } from '@polkadot/util-crypto';
*
* ed25519PairFromSeed(...); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromSeed (seed: Uint8Array, onlyJs?: boolean): Keypair {
if (!hasBigInt || (!onlyJs && isReady())) {
const full = ed25519KeypairFromSeed(seed);
return {
publicKey: full.slice(32),
secretKey: full.slice(0, 64)
};
}
const publicKey = ed25519.getPublicKey(seed);
return {
publicKey,
secretKey: u8aConcatStrict([seed, publicKey])
};
}
@@ -1,17 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { ed25519PairFromString } from '../index.js';
describe('ed25519PairFromSeed', (): void => {
it('generates a valid publicKey/secretKey pair', (): void => {
expect(
ed25519PairFromString('test')
).toEqual({
publicKey: new Uint8Array([188, 108, 179, 142, 36, 142, 76, 87, 77, 193, 147, 139, 254, 110, 196, 217, 117, 233, 167, 165, 250, 150, 247, 237, 198, 68, 129, 4, 211, 209, 136, 48]),
secretKey: new Uint8Array([146, 139, 32, 54, 105, 67, 226, 175, 209, 30, 188, 14, 174, 46, 83, 169, 59, 241, 119, 164, 252, 243, 91, 204, 100, 213, 3, 112, 78, 101, 226, 2, 188, 108, 179, 142, 36, 142, 76, 87, 77, 193, 147, 139, 254, 110, 196, 217, 117, 233, 167, 165, 250, 150, 247, 237, 198, 68, 129, 4, 211, 209, 136, 48])
});
});
});
@@ -1,31 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { stringToU8a } from '@polkadot/util';
import { blake2AsU8a } from '../../blake2/asU8a.js';
import { ed25519PairFromSeed } from './fromSeed.js';
/**
* @name ed25519PairFromString
* @summary Creates a new public/secret keypair from a string.
* @description
* Returns a object containing a `publicKey` & `secretKey` generated from the supplied string. The string is hashed and the value used as the input seed.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromString } from '@polkadot/util-crypto';
*
* ed25519PairFromString('test'); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromString (value: string): Keypair {
return ed25519PairFromSeed(
blake2AsU8a(
stringToU8a(value)
)
);
}
@@ -1,40 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { stringToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { ed25519PairFromSeed, ed25519Sign } from './index.js';
const PAIR = ed25519PairFromSeed(
stringToU8a('12345678901234567890123456789012')
);
describe('ed25519Sign', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('returns a valid signature for the message', (): void => {
expect(
ed25519Sign(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
PAIR,
onlyJs
)
).toEqual(
new Uint8Array([28, 58, 206, 239, 249, 70, 59, 191, 166, 40, 219, 218, 235, 170, 25, 79, 10, 94, 9, 197, 34, 126, 1, 150, 246, 68, 28, 238, 36, 26, 172, 163, 168, 90, 202, 211, 126, 246, 57, 212, 43, 24, 88, 197, 240, 113, 118, 76, 37, 81, 91, 110, 236, 50, 144, 134, 100, 223, 220, 238, 34, 185, 211, 7])
);
});
});
}
perfWasm('ed25519Sign', 250, (input, onlyJs) =>
ed25519Sign(input, PAIR, onlyJs)
);
});
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import { ed25519 } from '@noble/curves/ed25519';
import { hasBigInt, u8aToU8a } from '@polkadot/util';
import { ed25519Sign as wasmSign, isReady } from '@polkadot/wasm-crypto';
/**
* @name ed25519Sign
* @summary Signs a message using the supplied secretKey
* @description
* Returns message signature of `message`, using the `secretKey`.
* @example
* <BR>
*
* ```javascript
* import { ed25519Sign } from '@polkadot/util-crypto';
*
* ed25519Sign([...], [...]); // => [...]
* ```
*/
export function ed25519Sign (message: string | Uint8Array, { publicKey, secretKey }: Partial<Keypair>, onlyJs?: boolean): Uint8Array {
if (!secretKey) {
throw new Error('Expected a valid secretKey');
} else if (!publicKey) {
throw new Error('Expected a valid publicKey');
}
const messageU8a = u8aToU8a(message);
const privateU8a = secretKey.subarray(0, 32);
return !hasBigInt || (!onlyJs && isReady())
? wasmSign(publicKey, privateU8a, messageU8a)
: ed25519.sign(messageU8a, privateU8a);
}
@@ -1,84 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { stringToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { ed25519PairFromSeed, ed25519Verify } from './index.js';
describe('ed25519Verify', (): void => {
let publicKey: Uint8Array;
let signature: Uint8Array;
beforeEach(async (): Promise<void> => {
await waitReady();
publicKey = ed25519PairFromSeed(
stringToU8a('12345678901234567890123456789012')
).publicKey;
signature = new Uint8Array([28, 58, 206, 239, 249, 70, 59, 191, 166, 40, 219, 218, 235, 170, 25, 79, 10, 94, 9, 197, 34, 126, 1, 150, 246, 68, 28, 238, 36, 26, 172, 163, 168, 90, 202, 211, 126, 246, 57, 212, 43, 24, 88, 197, 240, 113, 118, 76, 37, 81, 91, 110, 236, 50, 144, 134, 100, 223, 220, 238, 34, 185, 211, 7]);
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('validates a correctly signed message', (): void => {
expect(
ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
signature,
publicKey,
onlyJs
)
).toEqual(true);
});
it('fails a correctly signed message (message changed)', (): void => {
expect(
ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]),
signature,
publicKey,
onlyJs
)
).toEqual(false);
});
it('fails a correctly signed message (signature changed)', (): void => {
signature[0] = 0xff;
expect(
ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
signature,
publicKey,
onlyJs
)
).toEqual(false);
});
it('throws error when publicKey lengths do not match', (): void => {
expect(
() => ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
signature,
new Uint8Array([1, 2]),
onlyJs
)
).toThrow(/Invalid publicKey/);
});
it('throws error when signature lengths do not match', (): void => {
expect(
() => ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
new Uint8Array([1, 2]),
publicKey,
onlyJs
)
).toThrow(/Invalid signature/);
});
});
}
});
@@ -1,41 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ed25519 } from '@noble/curves/ed25519';
import { hasBigInt, u8aToU8a } from '@polkadot/util';
import { ed25519Verify as wasmVerify, isReady } from '@polkadot/wasm-crypto';
/**
* @name ed25519Sign
* @summary Verifies the signature on the supplied message.
* @description
* Verifies the `signature` on `message` with the supplied `publicKey`. Returns `true` on sucess, `false` otherwise.
* @example
* <BR>
*
* ```javascript
* import { ed25519Verify } from '@polkadot/util-crypto';
*
* ed25519Verify([...], [...], [...]); // => true/false
* ```
*/
export function ed25519Verify (message: string | Uint8Array, signature: string | Uint8Array, publicKey: string | Uint8Array, onlyJs?: boolean): boolean {
const messageU8a = u8aToU8a(message);
const publicKeyU8a = u8aToU8a(publicKey);
const signatureU8a = u8aToU8a(signature);
if (publicKeyU8a.length !== 32) {
throw new Error(`Invalid publicKey, received ${publicKeyU8a.length}, expected 32`);
} else if (signatureU8a.length !== 64) {
throw new Error(`Invalid signature, received ${signatureU8a.length} bytes, expected 64`);
}
try {
return !hasBigInt || (!onlyJs && isReady())
? wasmVerify(signatureU8a, messageU8a, publicKeyU8a)
: ed25519.verify(signatureU8a, messageU8a, publicKeyU8a);
} catch {
return false;
}
}
@@ -17,7 +17,7 @@ describe('formatAddress', () => {
it('returns fails on invalid address', () => {
expect(
() => ethereumEncode('0xnotaddress')
).toThrow(/Invalid address or publicKey provided/);
).toThrow(/Invalid address provided/);
});
it('converts lowercase to the checksummed address', () => {
@@ -36,16 +36,15 @@ describe('formatAddress', () => {
describe('from publicKey', (): void => {
const ADDRESS = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
it('encodes a compressed publicKey', (): void => {
// Deriving an address from a secp256k1 key needs the curve, which is gone
// (quantus/common#6). Refused with the reason, not reported as a bad length.
it('refuses a compressed or expanded secp256k1 publicKey', (): void => {
expect(
ethereumEncode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
).toEqual(ADDRESS);
});
it('encodes an expanded publicKey', (): void => {
() => ethereumEncode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
).toThrow(/secp256k1 is quantum-unsafe and has been removed/);
expect(
ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
).toEqual(ADDRESS);
() => ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
).toThrow(/secp256k1 is quantum-unsafe and has been removed/);
});
it('encodes a pre-hashed key', (): void => {
+13 -7
View File
@@ -6,13 +6,17 @@ import type { HexString } from '@polkadot/util/types';
import { u8aToHex, u8aToU8a } from '@polkadot/util';
import { keccakAsU8a } from '../keccak/index.js';
import { secp256k1Expand } from '../secp256k1/index.js';
/**
* Formats an Ethereum address for display: EIP-55 checksum casing.
*
* Kept, though secp256k1 is gone, because it holds no key: `@polkadot/types`
* and the identicon renderer format 20-byte addresses with it, and a wallet has
* to be able to *show* an Ethereum address to recognise and refuse one. What is
* gone is deriving an address from a secp256k1 public key, which needed the
* curve. quantus/common#6
*/
function getH160 (u8a: Uint8Array): Uint8Array {
if ([33, 65].includes(u8a.length)) {
u8a = keccakAsU8a(secp256k1Expand(u8a));
}
return u8a.slice(-20);
}
@@ -23,8 +27,10 @@ export function ethereumEncode (addressOrPublic?: string | Uint8Array): HexStrin
const u8aAddress = u8aToU8a(addressOrPublic);
if (![20, 32, 33, 65].includes(u8aAddress.length)) {
throw new Error(`Invalid address or publicKey provided, received ${u8aAddress.length} bytes input`);
if ([33, 65].includes(u8aAddress.length)) {
throw new Error('Deriving an Ethereum address from a secp256k1 public key is not supported: secp256k1 is quantum-unsafe and has been removed');
} else if (![20, 32].includes(u8aAddress.length)) {
throw new Error(`Invalid address provided, received ${u8aAddress.length} bytes input`);
}
const address = u8aToHex(getH160(u8aAddress), -1, false);
@@ -1,54 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { mnemonicToLegacySeed } from '@polkadot/util-crypto';
import { hdEthereum } from './index.js';
describe('hdEthereum', (): void => {
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const derivationPath = 'm/44\'/60\'/0\'/0/0';
const PUBLIC = new Uint8Array([
3, 118, 64, 77, 247, 27, 4, 157,
236, 206, 251, 221, 230, 244, 154, 147,
189, 131, 249, 169, 102, 78, 3, 185,
153, 19, 89, 40, 24, 25, 139, 131,
93
]);
const SECRET = new Uint8Array([
166, 162, 203, 17, 2, 206, 110, 176,
18, 102, 230, 144, 90, 158, 25, 232,
43, 180, 176, 49, 189, 149, 3, 71,
243, 228, 223, 104, 125, 132, 58, 228
]
);
const PUBLICDERIVED = new Uint8Array([
3, 129, 53, 27, 27, 70, 210, 96,
43, 9, 146, 187, 93, 85, 49, 249,
193, 105, 107, 8, 18, 254, 178, 83,
75, 104, 132, 173, 196, 126, 46, 29,
139
]);
const SECRETDERIVED = new Uint8Array([
7, 13, 195, 17, 115, 0, 1, 25,
24, 226, 107, 2, 23, 105, 69, 204,
21, 195, 213, 72, 207, 73, 253, 132,
24, 217, 127, 147, 175, 105, 158, 70
]);
it('derives the right key pair from a mnemonic', (): void => {
const key = hdEthereum(mnemonicToLegacySeed(PHRASE, '', false, 64));
expect(key.publicKey).toEqual(PUBLIC);
expect(key.secretKey).toEqual(SECRET);
});
it('derives the right key pair from a mnemonic and a derivation path', (): void => {
const key = hdEthereum(mnemonicToLegacySeed(PHRASE, '', false, 64), derivationPath);
expect(key.publicKey).toEqual(PUBLICDERIVED);
expect(key.secretKey).toEqual(SECRETDERIVED);
});
});
@@ -1,69 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { bnToU8a, stringToU8a, u8aConcat } from '@polkadot/util';
import { BN_BE_32_OPTS } from '../../bn.js';
import { hmacShaAsU8a } from '../../hmac/index.js';
import { secp256k1PairFromSeed, secp256k1PrivateKeyTweakAdd } from '../../secp256k1/index.js';
import { HARDENED, hdValidatePath } from '../validatePath.js';
interface CodedKeypair extends Keypair {
chainCode: Uint8Array;
}
const MASTER_SECRET = stringToU8a('Bitcoin seed');
function createCoded (secretKey: Uint8Array, chainCode: Uint8Array): CodedKeypair {
return {
chainCode,
publicKey: secp256k1PairFromSeed(secretKey).publicKey,
secretKey
};
}
function deriveChild (hd: CodedKeypair, index: number): CodedKeypair {
const indexBuffer = bnToU8a(index, BN_BE_32_OPTS);
const data = index >= HARDENED
? u8aConcat(new Uint8Array(1), hd.secretKey, indexBuffer)
: u8aConcat(hd.publicKey, indexBuffer);
try {
const I = hmacShaAsU8a(hd.chainCode, data, 512);
return createCoded(
secp256k1PrivateKeyTweakAdd(hd.secretKey, I.slice(0, 32)),
I.slice(32)
);
} catch {
// In case parse256(IL) >= n or ki == 0, proceed with the next value for i
return deriveChild(hd, index + 1);
}
}
export function hdEthereum (seed: Uint8Array, path = ''): Keypair {
const I = hmacShaAsU8a(MASTER_SECRET, seed, 512);
let hd = createCoded(I.slice(0, 32), I.slice(32));
if (!path || path === 'm' || path === 'M' || path === "m'" || path === "M'") {
return hd;
}
if (!hdValidatePath(path)) {
throw new Error('Invalid derivation path');
}
const parts = path.split('/').slice(1);
for (const p of parts) {
hd = deriveChild(hd, parseInt(p, 10) + (
(p.length > 1) && p.endsWith("'")
? HARDENED
: 0
));
}
return hd;
}
-2
View File
@@ -1,6 +1,4 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { hdEthereum } from './ethereum/index.js';
export { hdLedger } from './ledger/index.js';
export { hdValidatePath } from './validatePath.js';
@@ -1,34 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { BN_EIGHT, bnToU8a, u8aConcat, u8aToBn } from '@polkadot/util';
import { BN_LE_32_OPTS, BN_LE_512_OPTS, BN_LE_OPTS } from '../../bn.js';
import { hmacShaAsU8a } from '../../hmac/index.js';
// performs hard-only derivation on the xprv
export function ledgerDerivePrivate (xprv: Uint8Array, index: number): Uint8Array {
const kl = xprv.subarray(0, 32);
const kr = xprv.subarray(32, 64);
const cc = xprv.subarray(64, 96);
const data = u8aConcat([0], kl, kr, bnToU8a(index, BN_LE_32_OPTS));
const z = hmacShaAsU8a(cc, data, 512);
data[0] = 0x01;
return u8aConcat(
bnToU8a(
u8aToBn(kl, BN_LE_OPTS).iadd(
u8aToBn(z.subarray(0, 28), BN_LE_OPTS).imul(BN_EIGHT)
),
BN_LE_512_OPTS
).subarray(0, 32),
bnToU8a(
u8aToBn(kr, BN_LE_OPTS).iadd(
u8aToBn(z.subarray(32, 64), BN_LE_OPTS)
),
BN_LE_512_OPTS
).subarray(0, 32),
hmacShaAsU8a(cc, data, 512).subarray(32, 64)
);
}
@@ -1,64 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { hdLedger } from '../index.js';
const MNE_0 = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const MNE_1 = 'open jelly jeans corn ketchup supreme brief element armed lens vault weather original scissors rug priority vicious lesson raven spot gossip powder person volcano';
const MNE_P = `${MNE_1} testing`;
const TESTS = {
Kusama: {
slip44: 0x01b2,
tests: [
{
ed25519: '0x98cb4e14e0e08ea876f88d728545ea7572dc07dbbe69f1731c418fb827e69d41',
index: [0, 0],
mnemonic: MNE_0
},
{
ed25519: '0x70e9010e84c81095aaa5f63b1c5a6a66a1dcbec017a23c2f3b7a1b08fe5ea65a',
index: [0, 0],
mnemonic: MNE_1
},
{
ed25519: '0xf06730efb1e6ea59ac752a7c3620fade3909062fb88597856cc3af72045fa65a',
index: [5, 7],
mnemonic: MNE_1
}
]
},
Polkadot: {
slip44: 0x0162,
tests: [
{
ed25519: '0xe8c68348586d53e4e8d1a864b0e4e17c75e4eb06e0c63c1432bef2ba29e69d41',
index: [0, 0],
mnemonic: MNE_0
},
{
ed25519: '0x3890e8db837eba3f8f25215c753e1091062298ce671a51441e7ef89a7adc4f48',
index: [0, 0],
mnemonic: MNE_P
}
]
}
};
describe('ledgerDerive', (): void => {
Object.entries(TESTS).forEach(([network, { slip44, tests }]): void => {
tests.forEach(({ ed25519, index: [account, address], mnemonic }, index): void => {
it(`derives a known ed25519 seed for ${network} (${index})`, (): void => {
expect(u8aToHex(
hdLedger(mnemonic, `m/44'/${slip44}'/${account}'/0'/${address}'`)
.secretKey
.slice(0, 32)
)).toEqual(ed25519);
});
});
});
});
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { ed25519PairFromSeed } from '../../ed25519/index.js';
import { mnemonicValidate } from '../../mnemonic/index.js';
import { HARDENED, hdValidatePath } from '../validatePath.js';
import { ledgerDerivePrivate } from './derivePrivate.js';
import { ledgerMaster } from './master.js';
export function hdLedger (_mnemonic: string, path: string): Keypair {
const words = _mnemonic
.split(' ')
.map((s) => s.trim())
.filter((s) => s);
if (![12, 24, 25].includes(words.length)) {
throw new Error('Expected a mnemonic with 24 words (or 25 including a password)');
}
const [mnemonic, password] = words.length === 25
? [words.slice(0, 24).join(' '), words[24]]
: [words.join(' '), ''];
if (!mnemonicValidate(mnemonic)) {
throw new Error('Invalid mnemonic passed to ledger derivation');
} else if (!hdValidatePath(path)) {
throw new Error('Invalid derivation path');
}
const parts = path.split('/').slice(1);
let seed = ledgerMaster(mnemonic, password);
for (const p of parts) {
const n = parseInt(p.replace(/'$/, ''), 10);
seed = ledgerDerivePrivate(seed, (n < HARDENED) ? (n + HARDENED) : n);
}
return ed25519PairFromSeed(seed.slice(0, 32));
}
@@ -1,19 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { ledgerMaster } from './master.js';
const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const XPRV = '0x402b03cd9c8bed9ba9f9bd6cd9c315ce9fcc59c7c25d37c85a36096617e69d418e35cb4a3b737afd007f0688618f21a8831643c0e6c77fc33c06026d2a0fc93832596435e70647d7d98ef102a32ea40319ca8fb6c851d7346d3bd8f9d1492658';
describe('ledgerDerive', (): void => {
it('derives a known master xprv', (): void => {
expect(u8aToHex(
ledgerMaster(MNEMONIC)
)).toEqual(XPRV);
});
});
@@ -1,26 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { u8aConcat } from '@polkadot/util';
import { hmacShaAsU8a } from '../../hmac/index.js';
import { mnemonicToSeedSync } from '../../mnemonic/bip39.js';
const ED25519_CRYPTO = 'ed25519 seed';
// gets an xprv from a mnemonic
export function ledgerMaster (mnemonic: string, password?: string): Uint8Array {
const seed = mnemonicToSeedSync(mnemonic, password);
const chainCode = hmacShaAsU8a(ED25519_CRYPTO, new Uint8Array([1, ...seed]), 256);
let priv;
while (!priv || (priv[31] & 0b0010_0000)) {
priv = hmacShaAsU8a(ED25519_CRYPTO, priv || seed, 512);
}
priv[0] &= 0b1111_1000;
priv[31] &= 0b0111_1111;
priv[31] |= 0b0100_0000;
return u8aConcat(priv, chainCode);
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair, KeypairType } from '../types.js';
import type { DeriveJunction } from './DeriveJunction.js';
import { keyHdkdEcdsa } from './hdkdEcdsa.js';
import { keyHdkdEd25519 } from './hdkdEd25519.js';
import { keyHdkdSr25519 } from './hdkdSr25519.js';
const generators = {
ecdsa: keyHdkdEcdsa,
ed25519: keyHdkdEd25519,
// FIXME This is Substrate-compatible, not Ethereum-compatible
ethereum: keyHdkdEcdsa,
sr25519: keyHdkdSr25519
};
export function keyFromPath (pair: Keypair, path: DeriveJunction[], type: KeypairType): Keypair {
const keyHdkd = generators[type as keyof typeof generators] as typeof keyHdkdEd25519 | undefined;
// ML-DSA has no junction derivation to fall back on. Lattice keys are not
// publicly derivable, so a soft junction cannot mean what BIP-32 implies, and
// the chain's own `Pair::derive` refuses for the same reason. Quantus keys
// derive from a hardened BIP44 path instead — see quantus/common#4 — which is
// a different entry point, not a different generator in this map.
//
// This throws rather than returning the pair unchanged: silently ignoring a
// derivation path would hand the caller the *parent* key under a child's
// name, and fund an address nobody asked for.
if (!keyHdkd) {
throw new Error(`Unable to derive on this keypair type: ${type}`);
}
let result = pair;
for (const junction of path) {
result = keyHdkd(result, junction);
}
return result;
}
@@ -1,17 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import type { DeriveJunction } from './DeriveJunction.js';
export function createSeedDeriveFn (fromSeed: (seed: Uint8Array) => Keypair, derive: (seed: Uint8Array, chainCode: Uint8Array) => Uint8Array): (keypair: Keypair, junction: DeriveJunction) => Keypair {
return (keypair: Keypair, { chainCode, isHard }: DeriveJunction): Keypair => {
if (!isHard) {
throw new Error('A soft key was found in the path and is not supported');
}
return fromSeed(
derive(keypair.secretKey.subarray(0, 32), chainCode)
);
};
}
@@ -1,8 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1DeriveHard } from '../secp256k1/deriveHard.js';
import { secp256k1PairFromSeed } from '../secp256k1/pair/fromSeed.js';
import { createSeedDeriveFn } from './hdkdDerive.js';
export const keyHdkdEcdsa = /*#__PURE__*/ createSeedDeriveFn(secp256k1PairFromSeed, secp256k1DeriveHard);
@@ -1,7 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ed25519DeriveHard, ed25519PairFromSeed } from '../ed25519/index.js';
import { createSeedDeriveFn } from './hdkdDerive.js';
export const keyHdkdEd25519 = /*#__PURE__*/ createSeedDeriveFn(ed25519PairFromSeed, ed25519DeriveHard);
@@ -1,14 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import type { DeriveJunction } from './DeriveJunction.js';
import { sr25519DeriveHard } from '../sr25519/deriveHard.js';
import { sr25519DeriveSoft } from '../sr25519/deriveSoft.js';
export function keyHdkdSr25519 (keypair: Keypair, { chainCode, isSoft }: DeriveJunction): Keypair {
return isSoft
? sr25519DeriveSoft(keypair, chainCode)
: sr25519DeriveHard(keypair, chainCode);
}
-4
View File
@@ -6,7 +6,3 @@
*/
export { keyExtractPath } from './extractPath.js';
export { keyExtractSuri } from './extractSuri.js';
export { keyFromPath } from './fromPath.js';
export { keyHdkdEcdsa } from './hdkdEcdsa.js';
export { keyHdkdEd25519 } from './hdkdEd25519.js';
export { keyHdkdSr25519 } from './hdkdSr25519.js';
@@ -3,6 +3,10 @@
/// <reference types="@polkadot/dev-test/globals.d.ts" />
// The standard BIP39 test vectors (Trezor): mnemonic, entropy, seed, and
// upstream's sr25519 expanded secret. Moved here from sr25519/ when that
// directory was removed (quantus/common#6); the mnemonic and entropy columns are
// what is still used.
// mnemonic, entropy, seed, secret (expanded)
type Test = [string, string, string, string];
@@ -7,5 +7,4 @@
export { mnemonicGenerate } from './generate.js';
export { mnemonicToEntropy } from './toEntropy.js';
export { mnemonicToLegacySeed } from './toLegacySeed.js';
export { mnemonicToMiniSecret } from './toMiniSecret.js';
export { mnemonicValidate } from './validate.js';
@@ -6,8 +6,8 @@
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '../index.js';
import tests from '../sr25519/pair/testing.spec.js';
import { french as frenchWords } from './wordlists/index.js';
import tests from './bip39Vectors.spec.js';
import { mnemonicToEntropy } from './toEntropy.js';
await cryptoWaitReady();
@@ -1,67 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aEq, u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '../index.js';
import tests from '../sr25519/pair/testing.spec.js';
import { korean as koreanWords } from './wordlists/index.js';
import { mnemonicToMiniSecret } from './toMiniSecret.js';
const MNEMONIC = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const SEED = '0x4d1ab2a57929edfd018aaa974e62ed557e3f54b4104acabedf73c8f5a1dbb029';
await cryptoWaitReady();
describe('mnemonicToMiniSecret', (): void => {
for (const password of [undefined, 'foo', 'bar']) {
it(`generates Wasm & Js equivalents for password=${password || 'undefined'}`, (): void => {
expect(
u8aEq(
mnemonicToMiniSecret(MNEMONIC, password, undefined, true),
mnemonicToMiniSecret(MNEMONIC, password, undefined, false)
)
).toEqual(true);
});
}
it('creates a known minisecret from a non-english mnemonic', (): void => {
const mnemonic = '엉덩이 능동적 숫자 팩시밀리 비난 서적 파출소 도움 독창적 인생 상류 먼지 답변 음반 수박 사업 노란색 공사 우체국 특급 도대체 금지 굉장히 고무신';
expect(
() => mnemonicToMiniSecret(mnemonic, 'testing')
).toThrow();
expect(
u8aToHex(mnemonicToMiniSecret(mnemonic, 'testing', koreanWords))
).toEqual('0xefa278a62535581767a2f49cb542ed91b65fb911e1b05e7a09c702b257f10c13');
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('generates a valid seed', (): void => {
expect(
u8aToHex(mnemonicToMiniSecret(MNEMONIC, undefined, undefined, onlyJs))
).toEqual(SEED);
});
it('fails with non-mnemonics', (): void => {
expect(
() => mnemonicToMiniSecret('foo bar baz', undefined, undefined, onlyJs)
).toThrow(/mnemonic specified/);
});
tests.forEach(([mnemonic, , seed], index): void => {
it(`Created correct seed for ${index}`, (): void => {
expect(
u8aToHex(mnemonicToMiniSecret(mnemonic, 'Substrate', undefined, onlyJs))
).toEqual(
// mini returned here, only check first 32-bytes (64 hex + 2 prefix)
seed.substring(0, 66)
);
});
});
});
}
});
@@ -1,23 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { stringToU8a } from '@polkadot/util';
import { bip39ToMiniSecret, isReady } from '@polkadot/wasm-crypto';
import { pbkdf2Encode } from '../pbkdf2/index.js';
import { mnemonicToEntropy } from './toEntropy.js';
import { mnemonicValidate } from './validate.js';
export function mnemonicToMiniSecret (mnemonic: string, password = '', wordlist?: string[], onlyJs?: boolean): Uint8Array {
if (!mnemonicValidate(mnemonic, wordlist, onlyJs)) {
throw new Error('Invalid bip39 mnemonic specified');
} else if (!wordlist && !onlyJs && isReady()) {
return bip39ToMiniSecret(mnemonic, password);
}
const entropy = mnemonicToEntropy(mnemonic, wordlist);
const salt = stringToU8a(`mnemonic${password}`);
// return the first 32 bytes as the seed
return pbkdf2Encode(entropy, salt).password.slice(0, 32);
}
@@ -1,64 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { arrayRange, u8aEq } from '@polkadot/util';
import { cryptoWaitReady, ed25519PairFromSeed, mnemonicGenerate, mnemonicToMiniSecret, sr25519PairFromSeed } from '../index.js';
// NOTE: This basically controls how long stuff runs for, YMMV
//
// - 100 runs with 5 checks, takes 2mins on _my_ machine
// - 10_000 runs with 5 checks should be ~3hrs
const NUM_RUNS = 100;
const NUM_CHECKS = 5;
await cryptoWaitReady();
// generate either a JS or WASM mnemonic
for (const onlyJsMnemonic of [false, true]) {
describe(`mnemonicToMiniSecret (conpare), onlyJs${(onlyJsMnemonic && 'true') || 'false'}`, (): void => {
for (const i of arrayRange(NUM_RUNS)) {
// loop through lots of mnemonics
describe(`run=${i + 1}`, (): void => {
// compare both JS and WASM outputs against original
for (const onlyJsMini of [false, true]) {
describe(`onlyJsMini=${(onlyJsMini && 'true') || 'false'}`, (): void => {
// NOTE we cannot actually use the onlyJsMnemonic flag here
const mnemonic = mnemonicGenerate(12);
describe(`${mnemonic}`, (): void => {
// do iterations to check and re-check that all matches
for (const count of arrayRange(NUM_CHECKS)) {
it(`check=${count + 1}`, (): void => {
const minisecret = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', undefined, onlyJsMnemonic);
const edpub = ed25519PairFromSeed(minisecret).publicKey;
const srpub = sr25519PairFromSeed(minisecret).publicKey;
const testmini = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', undefined, onlyJsMini);
// explicit minisecret compare
expect(
u8aEq(minisecret, testmini)
).toEqual(true);
// compare the sr25519 keypair generated
expect(
u8aEq(srpub, sr25519PairFromSeed(testmini).publicKey)
).toEqual(true);
// compare ed both in WASM and JS
[true, false].forEach((onlyJsEd): void => {
expect(
u8aEq(edpub, ed25519PairFromSeed(testmini, onlyJsEd).publicKey)
).toEqual(true);
});
});
}
});
});
}
});
}
});
}
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/util-crypto', path: 'auto', type: 'auto', version: '14.0.3-quantus.2' };
export const packageInfo = { name: '@polkadot/util-crypto', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };
@@ -1,47 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { secp256k1Compress } from './index.js';
describe('secp256k1Compress', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('returns a compressed key as-is', (): void => {
expect(
secp256k1Compress(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'),
onlyJs
)
).toEqual(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
);
});
it('compresses a known key', (): void => {
expect(
secp256k1Compress(
hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af'),
onlyJs
)
).toEqual(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
);
});
});
}
perfWasm('secp256k1Compress', 100000, (input, onlyJs) => secp256k1Compress(input, onlyJs), [[
hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
]]
);
});
@@ -1,21 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1 } from '@noble/curves/secp256k1';
import { hasBigInt } from '@polkadot/util';
import { isReady, secp256k1Compress as wasm } from '@polkadot/wasm-crypto';
export function secp256k1Compress (publicKey: Uint8Array, onlyJs?: boolean): Uint8Array {
if (![33, 65].includes(publicKey.length)) {
throw new Error(`Invalid publicKey provided, received ${publicKey.length} bytes input`);
}
if (publicKey.length === 33) {
return publicKey;
}
return !hasBigInt || (!onlyJs && isReady())
? wasm(publicKey)
: secp256k1.ProjectivePoint.fromHex(publicKey).toRawBytes(true);
}
@@ -1,17 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { compactAddLength, isU8a, stringToU8a, u8aConcat } from '@polkadot/util';
import { blake2AsU8a } from '../blake2/asU8a.js';
const HDKD = compactAddLength(stringToU8a('Secp256k1HDKD'));
export function secp256k1DeriveHard (seed: Uint8Array, chainCode: Uint8Array): Uint8Array {
if (!isU8a(chainCode) || chainCode.length !== 32) {
throw new Error('Invalid chainCode passed to derive');
}
// NOTE This is specific to the Substrate HDD derivation, so always use the blake2 hasher
return blake2AsU8a(u8aConcat(HDKD, seed, chainCode), 256);
}
@@ -1,47 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { secp256k1Expand } from './index.js';
describe('secp256k1Expand', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('expands a known key', (): void => {
expect(
secp256k1Expand(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'),
onlyJs
)
).toEqual(
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
);
});
it('expands a known full key', (): void => {
expect(
secp256k1Expand(
hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af'),
onlyJs
)
).toEqual(
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
);
});
});
}
perfWasm('secp256k1Expand', 2000, (input, onlyJs) => secp256k1Expand(input, onlyJs), [[
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
]]
);
});
@@ -1,30 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1 } from '@noble/curves/secp256k1';
import { bnToU8a, hasBigInt, u8aConcat } from '@polkadot/util';
import { isReady, secp256k1Expand as wasm } from '@polkadot/wasm-crypto';
import { BN_BE_256_OPTS } from '../bn.js';
export function secp256k1Expand (publicKey: Uint8Array, onlyJs?: boolean): Uint8Array {
if (![33, 65].includes(publicKey.length)) {
throw new Error(`Invalid publicKey provided, received ${publicKey.length} bytes input`);
}
if (publicKey.length === 65) {
return publicKey.subarray(1);
}
if (!hasBigInt || (!onlyJs && isReady())) {
return wasm(publicKey).subarray(1);
}
const { x, y } = secp256k1.ProjectivePoint.fromHex(publicKey);
return u8aConcat(
bnToU8a(x, BN_BE_256_OPTS),
bnToU8a(y, BN_BE_256_OPTS)
);
}
@@ -1,24 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hasher } from './hasher.js';
describe('hasher', (): void => {
it('creates a blake2 hash', (): void => {
expect(
hasher('blake2', 'abc')
).toEqual(
new Uint8Array([189, 221, 129, 60, 99, 66, 57, 114, 49, 113, 239, 63, 238, 152, 87, 155, 148, 150, 78, 59, 177, 203, 62, 66, 114, 98, 200, 192, 104, 213, 35, 25])
);
});
it('creates a keccak hash', (): void => {
expect(
hasher('keccak', 'abc')
).toEqual(
new Uint8Array([78, 3, 101, 122, 234, 69, 169, 79, 199, 212, 123, 168, 38, 200, 214, 103, 192, 209, 230, 227, 58, 100, 160, 54, 236, 68, 245, 143, 161, 45, 108, 69])
);
});
});
@@ -1,10 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { secp256k1Compress } from './compress.js';
export { secp256k1Expand } from './expand.js';
export { secp256k1PairFromSeed } from './pair/fromSeed.js';
export { secp256k1Recover } from './recover.js';
export { secp256k1Sign } from './sign.js';
export { secp256k1PrivateKeyTweakAdd } from './tweakAdd.js';
export { secp256k1Verify } from './verify.js';
@@ -1,75 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a, u8aToHex } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { mnemonicToMiniSecret } from '../../mnemonic/index.js';
import { perfWasm } from '../../test/index.js';
import { secp256k1PairFromSeed } from '../index.js';
// mnemonic, secret, public, account_id
type Test = [string, string, string, string];
const tests: Test[] = [
[
'life fee table ahead modify maximum dumb such tobacco boss dry nurse',
'0xf2360e871c830d397fe221382b503f07ddd8763df81a94bb2504390a2fb91f59',
'0x036b0aa6beab469dd2b748a0ff5ddbe3d13df1e15c9d28a2aa057212994e127bea',
'0xae8e8fcacbaeb607bcdf0bbd7e615f2b4ef484ee54f19d68a7393fb6db2dd9cd'
],
[
'tide survey cradle cover column ugly author wait eye state elder blame',
'0x5385355a5118ec732b9dbcf1668ba21db38b07cf79082dafa9a7cc4b52e4abb0',
'0x03929e4f93cdad265751ad8f6365185d8e937610d19b510400f5867d542d60a313',
'0xf80ea815da66c42f870b687e1530770d5a7936ae81a147b009506d85bd6d621c'
],
[
'laugh fish flee cake approve butter april dynamic myth license ticket lobster',
'0x83ec65cf9a8a7442d808aef6f8987599f1ba3be880769bb3a20621b13adbd476',
'0x0388299e4cfaa33d180a026bd54a46ad98df129a131320a9d2fd6f80e64bc3db39',
'0x35036238dd195f4c2169379354bda6cba5746f67bde03ef59a77a4cea80729bc'
],
[
'animal thing fork recipe exotic pilot inquiry pledge obey slab obtain reveal',
'0x0fd50580eb5a58b0eee60c77656dffa50094b539262366f1227d3babfd7343e5',
'0x036edc954685ad89f0a23b0fb1eb2b9c3a8600eee9091c758426dfb2bc7889a7c3',
'0x2a94b10d1f28810dc4628e7e424b2d08bd3d17fb08f9416d112f17e86c8fa77c'
]
];
describe('secp256k1PairFromSeed', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
const TEST = hexToU8a('0x4380de832af797688026ce24f85204d508243f201650c1a134929e5458b7fbae');
const RESULT = {
publicKey: hexToU8a('0x03fd8c74f795ced92064b86191cb2772b1e3a0947740aa0a5a6e379592471fd85b'),
secretKey: hexToU8a('0x4380de832af797688026ce24f85204d508243f201650c1a134929e5458b7fbae')
};
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('generates a valid publicKey/secretKey pair (u8a)', (): void => {
expect(secp256k1PairFromSeed(TEST, onlyJs)).toEqual(RESULT);
});
tests.forEach(([mnemonic, secretKey, publicKey], index): void => {
it(`creates valid against known (${index})`, (): void => {
const seed = mnemonicToMiniSecret(mnemonic);
const pair = secp256k1PairFromSeed(seed, onlyJs);
expect(u8aToHex(pair.secretKey)).toEqual(secretKey);
expect(u8aToHex(pair.publicKey)).toEqual(publicKey);
});
});
});
}
perfWasm('secp256k1PairFromSeed', 500, (input, onlyJs) =>
secp256k1PairFromSeed(input, onlyJs)
);
});
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { secp256k1 } from '@noble/curves/secp256k1';
import { hasBigInt, u8aEmpty } from '@polkadot/util';
import { isReady, secp256k1FromSeed } from '@polkadot/wasm-crypto';
/**
* @name secp256k1PairFromSeed
* @description Returns a object containing a `publicKey` & `secretKey` generated from the supplied seed.
*/
export function secp256k1PairFromSeed (seed: Uint8Array, onlyJs?: boolean): Keypair {
if (seed.length !== 32) {
throw new Error('Expected valid 32-byte private key as a seed');
}
if (!hasBigInt || (!onlyJs && isReady())) {
const full = secp256k1FromSeed(seed);
const publicKey = full.slice(32);
// There is an issue with the secp256k1 when running in an ASM.js environment where
// it seems that the lazy static section yields invalid results on the _first_ run.
// If this happens, fail outright, we cannot allow invalid return values
// https://github.com/polkadot-js/wasm/issues/307
if (u8aEmpty(publicKey)) {
throw new Error('Invalid publicKey generated from WASM interface');
}
return {
publicKey,
secretKey: full.slice(0, 32)
};
}
return {
publicKey: secp256k1.getPublicKey(seed, true),
secretKey: seed
};
}
@@ -1,35 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex, u8aToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { keccakAsU8a } from '../keccak/index.js';
import { perfWasm } from '../test/index.js';
import { secp256k1Recover } from './index.js';
const sig = u8aToU8a('0x7505f2880114da51b3f5d535f8687953c0ab9af4ab81e592eaebebf53b728d2b6dfd9b5bcd70fee412b1f31360e7c2774009305cb84fc50c1d0ff8034dfa5fff');
const msg = u8aToU8a('0xa30b64ce1eedf409c8afb801d72c05234e64849ea538c15dd3c8cf4ffcf166c9');
describe('secp256k1Recover', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('recovers a publicKey', (): void => {
const pubKey = '0x93a9fc7154c6da3c826415df01eb0e37fb4da4b0';
const res = keccakAsU8a(secp256k1Recover(msg, sig, 0, undefined, onlyJs));
expect(u8aToHex(res.subarray(-20))).toEqual(pubKey);
});
});
}
perfWasm('secp256k1Recover', 200, (_, onlyJs) =>
secp256k1Recover(msg, sig, 0, undefined, onlyJs)
);
});
@@ -1,36 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from './types.js';
import { secp256k1 } from '@noble/curves/secp256k1';
import { hasBigInt, u8aToU8a } from '@polkadot/util';
import { isReady, secp256k1Recover as wasm } from '@polkadot/wasm-crypto';
import { secp256k1Compress } from './compress.js';
import { secp256k1Expand } from './expand.js';
/**
* @name secp256k1Recover
* @description Recovers a publicKey from the supplied signature
*/
export function secp256k1Recover (msgHash: string | Uint8Array, signature: string | Uint8Array, recovery: number, hashType: HashType = 'blake2', onlyJs?: boolean): Uint8Array {
const sig = u8aToU8a(signature).subarray(0, 64);
const msg = u8aToU8a(msgHash);
const publicKey = !hasBigInt || (!onlyJs && isReady())
? wasm(msg, sig, recovery)
: secp256k1.Signature
.fromCompact(sig)
.addRecoveryBit(recovery)
.recoverPublicKey(msg)
.toRawBytes();
if (!publicKey) {
throw new Error('Unable to recover publicKey from signature');
}
return hashType === 'keccak'
? secp256k1Expand(publicKey, onlyJs)
: secp256k1Compress(publicKey, onlyJs);
}
@@ -1,39 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { secp256k1PairFromSeed, secp256k1Sign } from './index.js';
const pair = secp256k1PairFromSeed(hexToU8a('0x4380de832af797688026ce24f85204d508243f201650c1a134929e5458b7fbae'));
const msg = hexToU8a('0xa30b64ce1eedf409c8afb801d72c05234e64849ea538c15dd3c8cf4ffcf166c9');
describe('sign', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('generates a known signature', (): void => {
expect(
secp256k1Sign(msg, pair, undefined, onlyJs)
).toEqual(hexToU8a(
// from elliptic, this is - 0xdf92f73d9f060cefacf187b5414491cb992998ace017fa48839b5cda3e264ba8c4efa521361678d9b8582744d77aa4b8d886d7380b7808a683174afad9c4700300
// libsecp256k1 & @noble/hashes do agree here...
'0xdf92f73d9f060cefacf187b5414491cb992998ace017fa48839b5cda3e264ba83b105adec9e9872647a7d8bb28855b45e22805aea3d097953cbb1391f671d13e01'
));
});
});
}
// since the libsecp256k1 signatures don't match (but can be verified), we
// do both signing and verification here (checking the extracted key)
perfWasm('secp256k1Sign', 1000, (_, onlyJs) =>
secp256k1Sign(msg, pair, undefined, onlyJs)
);
});
@@ -1,37 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import type { HashType } from './types.js';
import { secp256k1 } from '@noble/curves/secp256k1';
import { bnToU8a, hasBigInt, u8aConcat } from '@polkadot/util';
import { isReady, secp256k1Sign as wasm } from '@polkadot/wasm-crypto';
import { BN_BE_256_OPTS } from '../bn.js';
import { hasher } from './hasher.js';
/**
* @name secp256k1Sign
* @description Returns message signature of `message`, using the supplied pair
*/
export function secp256k1Sign (message: Uint8Array | string, { secretKey }: Partial<Keypair>, hashType: HashType = 'blake2', onlyJs?: boolean): Uint8Array {
if (secretKey?.length !== 32) {
throw new Error('Expected valid secp256k1 secretKey, 32-bytes');
}
const data = hasher(hashType, message, onlyJs);
if (!hasBigInt || (!onlyJs && isReady())) {
return wasm(data, secretKey);
}
const signature = secp256k1.sign(data, secretKey, { lowS: true });
return u8aConcat(
bnToU8a(signature.r, BN_BE_256_OPTS),
bnToU8a(signature.s, BN_BE_256_OPTS),
new Uint8Array([signature.recovery || 0])
);
}
@@ -1,94 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import '../bundleInit.js';
import { stringToU8a } from '@polkadot/util';
import { randomAsU8a } from '../random/asU8a.js';
import { hasher } from './hasher.js';
import { secp256k1Expand, secp256k1PairFromSeed, secp256k1Sign, secp256k1Verify } from './index.js';
const MESSAGE = stringToU8a('this is a message');
describe('sign and verify', (): void => {
it('verify message signature', (): void => {
const address = '0x59f587c045d4d4e9aa1016eae43770fc0551df8a385027723342753a876aeef0';
const sig = '0x92fcacf0946bbd10b31dfe16d567ed1d3014e81007dd9e5256e19c0f07eacc1643b151ca29e449a765e16a7ce59b88d800467d6b3412d30ea8ad22307a59664b00';
const msg = stringToU8a('secp256k1');
expect(secp256k1Verify(msg, sig, address)).toEqual(true);
});
it('has 65-byte signatures', (): void => {
const pair = secp256k1PairFromSeed(randomAsU8a());
expect(secp256k1Sign(MESSAGE, pair)).toHaveLength(65);
});
it('signs/verifies a message by random key (blake2)', (): void => {
const pair = secp256k1PairFromSeed(randomAsU8a());
const signature = secp256k1Sign(MESSAGE, pair);
const address = hasher('blake2', pair.publicKey);
expect(secp256k1Verify(MESSAGE, signature, address)).toEqual(true);
});
it('signs/verifies a message by random key (keccak)', (): void => {
const pair = secp256k1PairFromSeed(randomAsU8a());
const signature = secp256k1Sign(MESSAGE, pair, 'keccak');
const address = hasher('keccak', secp256k1Expand(pair.publicKey));
expect(secp256k1Verify(MESSAGE, signature, address, 'keccak')).toEqual(true);
});
it('fails verification on hasher mismatches', (): void => {
const pair = secp256k1PairFromSeed(randomAsU8a());
const signature = secp256k1Sign(MESSAGE, pair, 'keccak');
const address = hasher('keccak', secp256k1Expand(pair.publicKey));
expect(secp256k1Verify(MESSAGE, signature, address, 'blake2')).toEqual(false);
});
it('works over a range of random keys (blake2)', (): void => {
for (let i = 0; i < 256; i++) {
const pair = secp256k1PairFromSeed(randomAsU8a());
try {
expect(
secp256k1Verify(
MESSAGE,
secp256k1Sign(MESSAGE, pair, 'blake2'),
hasher('blake2', pair.publicKey),
'blake2'
)
).toEqual(true);
} catch (error) {
console.error(`blake2 failed on #${i}`);
throw error;
}
}
}, 120000);
it('works over a range of random keys (keccak)', (): void => {
for (let i = 0; i < 256; i++) {
const pair = secp256k1PairFromSeed(randomAsU8a());
try {
expect(
secp256k1Verify(
MESSAGE,
secp256k1Sign(MESSAGE, pair, 'keccak'),
hasher('keccak', secp256k1Expand(pair.publicKey)),
'keccak'
)
).toEqual(true);
} catch (error) {
console.error(`keccak failed on #${i}`);
throw error;
}
}
}, 120000);
});
@@ -1,35 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { secp256k1PrivateKeyTweakAdd } from './tweakAdd.js';
describe('TweakAdd', (): void => {
it('fails for wrong array length', (): void => {
const A = new Uint8Array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
const B = new Uint8Array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]);
expect(
() => secp256k1PrivateKeyTweakAdd(A, B)
).toThrow(/Expected tweak to be an Uint8Array/);
});
for (const onlyBn of [false, true]) {
describe(`onlyBn=${(onlyBn && 'true') || 'false'}`, (): void => {
it('succeeds for a simple case', (): void => {
const A = new Uint8Array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
const B = new Uint8Array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]);
expect(
secp256k1PrivateKeyTweakAdd(A, B, onlyBn)
).toEqual(new Uint8Array([
3, 4, 3, 4, 3, 4, 3, 4, 3,
4, 3, 4, 3, 4, 3, 4, 3, 4,
3, 4, 3, 4, 3, 4, 3, 4, 3,
4, 3, 4, 3, 4
]));
});
});
}
});
@@ -1,65 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { _0n, BN, bnToU8a, hasBigInt, isU8a, nToU8a, u8aToBigInt } from '@polkadot/util';
import { BigInt } from '@polkadot/x-bigint';
import { BN_BE_256_OPTS, BN_BE_OPTS } from '../bn.js';
// pre-defined curve param as lifted form elliptic
// https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/curves.js#L182
const N = 'ffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141'.replace(/ /g, '');
const N_BI = BigInt(`0x${N}`);
const N_BN = new BN(N, 'hex');
function addBi (seckey: Uint8Array, tweak: Uint8Array): Uint8Array {
let res = u8aToBigInt(tweak, BN_BE_OPTS);
if (res >= N_BI) {
throw new Error('Tweak parameter is out of range');
}
res += u8aToBigInt(seckey, BN_BE_OPTS);
if (res >= N_BI) {
res -= N_BI;
}
if (res === _0n) {
throw new Error('Invalid resulting private key');
}
return nToU8a(res, BN_BE_256_OPTS);
}
function addBn (seckey: Uint8Array, tweak: Uint8Array): Uint8Array {
const res = new BN(tweak);
if (res.cmp(N_BN) >= 0) {
throw new Error('Tweak parameter is out of range');
}
res.iadd(new BN(seckey));
if (res.cmp(N_BN) >= 0) {
res.isub(N_BN);
}
if (res.isZero()) {
throw new Error('Invalid resulting private key');
}
return bnToU8a(res, BN_BE_256_OPTS);
}
export function secp256k1PrivateKeyTweakAdd (seckey: Uint8Array, tweak: Uint8Array, onlyBn?: boolean): Uint8Array {
if (!isU8a(seckey) || seckey.length !== 32) {
throw new Error('Expected seckey to be an Uint8Array with length 32');
} else if (!isU8a(tweak) || tweak.length !== 32) {
throw new Error('Expected tweak to be an Uint8Array with length 32');
}
return !hasBigInt || onlyBn
? addBn(seckey, tweak)
: addBi(seckey, tweak);
}
@@ -1,4 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export type HashType = 'blake2' | 'keccak';
@@ -1,81 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import '../bundleInit.js';
import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { hasher } from './hasher.js';
import { secp256k1PairFromSeed, secp256k1Verify } from './index.js';
const message = 'Pay KSMs to the Kusama account:88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee';
describe('secp256k1Verify', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('validates known ETH against address', (): void => {
expect(
secp256k1Verify(
`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`,
'0x55bd020bdbbdc02de34e915effc9b18a99002f4c29f64e22e8dcbb69e722ea6c28e1bb53b9484063fbbfd205e49dcc1f620929f520c9c4c3695150f05a28f52a01',
'0x002309df96687e44280bb72c3818358faeeb699c',
'keccak',
onlyJs
)
).toEqual(true);
});
for (const isPublic of [false, true]) {
describe(`validation against known, isPublic=${(isPublic && 'true') || 'false'}`, (): void => {
const pair = secp256k1PairFromSeed(hexToU8a('0x4380de832af797688026ce24f85204d508243f201650c1a134929e5458b7fbae'));
const msg = hexToU8a('0xa30b64ce1eedf409c8afb801d72c05234e64849ea538c15dd3c8cf4ffcf166c9');
const addr = isPublic
? pair.publicKey
: hasher('blake2', pair.publicKey, onlyJs);
it('signature from JS', (): void => {
expect(
secp256k1Verify(
msg,
'0xdf92f73d9f060cefacf187b5414491cb992998ace017fa48839b5cda3e264ba8c4efa521361678d9b8582744d77aa4b8d886d7380b7808a683174afad9c4700300',
addr,
'blake2',
onlyJs
)
).toEqual(true);
});
it('signature from wasm', (): void => {
expect(
secp256k1Verify(
msg,
'0xdf92f73d9f060cefacf187b5414491cb992998ace017fa48839b5cda3e264ba83b105adec9e9872647a7d8bb28855b45e22805aea3d097953cbb1391f671d13e01',
addr,
'blake2',
onlyJs
)
).toEqual(true);
});
});
}
});
}
perfWasm('secp256k1Verify', 100, (_, onlyJs) =>
secp256k1Verify(
`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`,
'0x55bd020bdbbdc02de34e915effc9b18a99002f4c29f64e22e8dcbb69e722ea6c28e1bb53b9484063fbbfd205e49dcc1f620929f520c9c4c3695150f05a28f52a01',
'0x002309df96687e44280bb72c3818358faeeb699c',
'keccak',
onlyJs
)
);
});
@@ -1,32 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from './types.js';
import { u8aEq, u8aToU8a } from '@polkadot/util';
import { hasher } from './hasher.js';
import { secp256k1Recover } from './recover.js';
/**
* @name secp256k1Verify
* @description Verifies the signature of `message`, using the supplied pair
*/
export function secp256k1Verify (msgHash: string | Uint8Array, signature: string | Uint8Array, address: string | Uint8Array, hashType: HashType = 'blake2', onlyJs?: boolean): boolean {
const sig = u8aToU8a(signature);
if (sig.length !== 65) {
throw new Error(`Expected signature with 65 bytes, ${sig.length} found instead`);
}
const publicKey = secp256k1Recover(hasher(hashType, msgHash), sig, sig[64], hashType, onlyJs);
const signerAddr = hasher(hashType, publicKey, onlyJs);
const inputAddr = u8aToU8a(address);
// for Ethereum (keccak) the last 20 bytes is the address
return u8aEq(publicKey, inputAddr) || (
hashType === 'keccak'
? u8aEq(signerAddr.slice(-20), inputAddr.slice(-20))
: u8aEq(signerAddr, inputAddr)
);
}
@@ -1,8 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/**
* @summary Utilities for working with signatures
*/
export { signatureVerify } from './verify.js';
@@ -1,230 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a, stringToU8a, u8aConcat, u8aToHex, u8aWrapBytes } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { decodeAddress } from '../address/index.js';
import { secp256k1Sign } from '../secp256k1/sign.js';
import { signatureVerify } from './index.js';
const ADDR_ED = 'DxN4uvzwPzJLtn17yew6jEffPhXQfdKHTp2brufb98vGbPN';
const ADDR_SR = 'EK1bFgKm2FsghcttHT7TB7rNyXApFgs9fCbijMGQNyFGBQm';
const ADDR_SR_WRAP = 'J9nD3s7zssCX7bion1xctAF6xcVexcpy2uwy4jTm9JL8yuK';
const ADDR_EC = 'XyFVXiGaHxoBhXZkSh6NS2rjFyVaVNUo5UiZDqZbuSfUdji';
const ADDR_ET = '0x54Dab85EE2c7b9F7421100d7134eFb5DfA4239bF';
const MESSAGE = 'hello world';
const SIG_ED = '0x299d3bf4c8bb51af732f8067b3a3015c0862a5ff34721749d8ed6577ea2708365d1c5f76bd519009971e41156f12c70abc2533837ceb3bad9a05a99ab923de06';
const SIG_SR = '0xca01419b5a17219f7b78335658cab3b126db523a5df7be4bfc2bef76c2eb3b1dcf4ca86eb877d0a6cf6df12db5995c51d13b00e005d053b892bd09c594434288';
const SIG_SR_WRAP = '0x84b6afb1c8e54bbcb3f4872baf172580e21310e9387a53742627d6652d121447fa406b82805ed3184fb7bd519175cc9f99f283f97954d95cf966ee164df85489';
const SIG_EC = '0x994638ee586d2c5dbd9bacacbc35d9b7e9018de8f7892f00c900db63bc57b1283e2ee7bc51a9b1c1dae121ac4f4b9e2a41cd1d6bf4bb3e24d7fed6faf6d85e0501';
const SIG_ET = '0x4e35aad35793b71f08566615661c9b741d7c605bc8935ac08608dff685324d71b5704fbd14c9297d2f584ea0735f015dcf0def66b802b3f555e1db916eda4b7700';
const MUL_ED = u8aToHex(u8aConcat(new Uint8Array([0]), hexToU8a(SIG_ED)));
const MUL_SR = u8aToHex(u8aConcat(new Uint8Array([1]), hexToU8a(SIG_SR)));
const MUL_EC = u8aToHex(u8aConcat(new Uint8Array([2]), hexToU8a(SIG_EC)));
const MUL_ET = u8aToHex(u8aConcat(new Uint8Array([2]), hexToU8a(SIG_ET)));
describe('signatureVerify', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
it('throws on invalid signature length', (): void => {
expect(
() => signatureVerify(MESSAGE, new Uint8Array(32), ADDR_ED)
).toThrow('Invalid signature length, expected [64..66] bytes, found 32');
});
describe('verifyDetect', (): void => {
it('verifies ed25519 signature', (): void => {
expect(signatureVerify(MESSAGE, SIG_ED, ADDR_ED)).toEqual({
crypto: 'ed25519',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_ED)
});
});
it('verifies ecdsa signatures', (): void => {
expect(signatureVerify(MESSAGE, SIG_EC, ADDR_EC)).toEqual({
crypto: 'ecdsa',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_EC)
});
});
it('verifies an ethereum signature', (): void => {
expect(signatureVerify(MESSAGE, SIG_ET, ADDR_ET)).toEqual({
crypto: 'ethereum',
isValid: true,
isWrapped: false,
publicKey: hexToU8a(ADDR_ET)
});
});
it('verifies an ethereum signature (known)', (): void => {
const message = 'Pay KSMs to the Kusama account:88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee';
expect(signatureVerify(
`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`,
'0x55bd020bdbbdc02de34e915effc9b18a99002f4c29f64e22e8dcbb69e722ea6c28e1bb53b9484063fbbfd205e49dcc1f620929f520c9c4c3695150f05a28f52a01',
'0x002309df96687e44280bb72c3818358faeeb699c'
)).toEqual({
crypto: 'ethereum',
isValid: true,
isWrapped: true,
publicKey: hexToU8a('0x002309df96687e44280bb72c3818358faeeb699c')
});
});
it('fails on invalid ethereum signature', (): void => {
expect(signatureVerify(MESSAGE, SIG_EC, ADDR_ET)).toEqual({
crypto: 'none',
isValid: false,
isWrapped: false,
publicKey: hexToU8a(ADDR_ET)
});
});
it('verifies an sr25519 signature', (): void => {
expect(signatureVerify(MESSAGE, SIG_SR, ADDR_SR)).toEqual({
crypto: 'sr25519',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_SR)
});
});
it('verifies an sr25519 signature (with msg wrapper, without wrapped sig)', (): void => {
expect(signatureVerify(u8aWrapBytes(MESSAGE), SIG_SR_WRAP, ADDR_SR_WRAP)).toEqual({
crypto: 'sr25519',
isValid: true,
isWrapped: true,
publicKey: decodeAddress(ADDR_SR_WRAP)
});
});
it('verifies an sr25519 signature (without msg wrapper, with wrapped sig)', (): void => {
expect(signatureVerify(MESSAGE, SIG_SR_WRAP, ADDR_SR_WRAP)).toEqual({
crypto: 'sr25519',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_SR_WRAP)
});
});
it('allows various inputs', (): void => {
expect(signatureVerify(stringToU8a(MESSAGE), hexToU8a(SIG_ED), decodeAddress(ADDR_ED))).toEqual({
crypto: 'ed25519',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_ED)
});
});
it('fails on an invalid signature', (): void => {
expect(signatureVerify(MESSAGE, SIG_SR, ADDR_ED)).toEqual({
crypto: 'none',
isValid: false,
isWrapped: false,
publicKey: decodeAddress(ADDR_ED)
});
});
});
describe('verifyMultisig', (): void => {
it('verifies an ed25519 signature', (): void => {
expect(signatureVerify(MESSAGE, MUL_ED, ADDR_ED)).toEqual({
crypto: 'ed25519',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_ED)
});
});
it('verifies an ecdsa signature', (): void => {
expect(signatureVerify(MESSAGE, MUL_EC, ADDR_EC)).toEqual({
crypto: 'ecdsa',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_EC)
});
});
it('verifies an ethereum signature', (): void => {
expect(signatureVerify(MESSAGE, MUL_ET, ADDR_ET)).toEqual({
crypto: 'ethereum',
isValid: true,
isWrapped: false,
publicKey: hexToU8a(ADDR_ET)
});
});
it('verifies an sr25519 signature', (): void => {
expect(signatureVerify(MESSAGE, MUL_SR, ADDR_SR)).toEqual({
crypto: 'sr25519',
isValid: true,
isWrapped: false,
publicKey: decodeAddress(ADDR_SR)
});
});
it('fails on an invalid signature', (): void => {
expect(signatureVerify(MESSAGE, MUL_SR, ADDR_ED)).toEqual({
crypto: 'none',
isValid: false,
isWrapped: false,
publicKey: new Uint8Array([61, 12, 55, 211, 0, 211, 97, 199, 4, 37, 17, 213, 81, 175, 166, 23, 251, 199, 144, 210, 19, 83, 186, 1, 196, 231, 14, 156, 171, 46, 141, 146])
});
});
/**
* ref: https://github.com/polkadot-js/common/issues/1898
*
* The following test ensures that we cover a reproduction that showed
* an inherent issue with verifying ecdsa signatures which is fixed in
* https://github.com/polkadot-js/common/pull/1973.
*
* It uses a random secretKey, and publicKey pair along with `secp256k1Sign`
* as the signer which is used for `ecdsa`.
*/
it('Ensure ecdsa can sign and verify 1000 messages', (): void => {
const verifyThousandMessages = () => {
const secretKey = new Uint8Array([
103, 97, 114, 98, 97, 103, 101, 32, 114, 105, 100,
103, 101, 32, 107, 105, 99, 107, 32, 114, 111, 115,
101, 32, 101, 110, 100, 32, 115, 113, 117, 101
]);
const publicKey = new Uint8Array([
2, 179, 102, 92, 246, 50, 172, 88,
81, 116, 8, 211, 192, 131, 154, 184,
122, 83, 180, 104, 4, 227, 214, 195,
140, 11, 82, 229, 49, 211, 185, 176,
63
]);
for (let i = 0; i < 1000; i++) {
const message = `message ${i}`;
const encodedMessage = stringToU8a(message);
const signature = secp256k1Sign(encodedMessage, { secretKey });
const { isValid: valid } = signatureVerify(
message,
signature,
publicKey
);
if (!valid) {
return false;
}
}
return true;
};
expect(verifyThousandMessages()).toEqual(true);
});
});
});
@@ -1,114 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeypairType, VerifyResult } from '../types.js';
import { u8aIsWrapped, u8aToU8a, u8aUnwrapBytes, u8aWrapBytes } from '@polkadot/util';
import { decodeAddress } from '../address/decode.js';
import { ed25519Verify } from '../ed25519/verify.js';
import { secp256k1Verify } from '../secp256k1/verify.js';
import { sr25519Verify } from '../sr25519/verify.js';
interface VerifyInput {
message: Uint8Array;
publicKey: Uint8Array;
signature: Uint8Array;
}
type Verifier = [KeypairType, (message: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array) => boolean];
type VerifyFn = (result: VerifyResult, input: VerifyInput) => VerifyResult;
const secp256k1VerifyHasher = (hashType: 'blake2' | 'keccak') =>
(message: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array) =>
secp256k1Verify(message, signature, publicKey, hashType, true);
const VERIFIERS_ECDSA: Verifier[] = [
['ecdsa', secp256k1VerifyHasher('blake2')],
['ethereum', secp256k1VerifyHasher('keccak')]
];
const VERIFIERS: Verifier[] = [
['ed25519', ed25519Verify],
['sr25519', sr25519Verify]
];
function verifyDetect (result: VerifyResult, { message, publicKey, signature }: VerifyInput, verifiers = [...VERIFIERS, ...VERIFIERS_ECDSA]): VerifyResult {
result.isValid = verifiers.some(([crypto, verify]): boolean => {
try {
if (verify(message, signature, publicKey)) {
result.crypto = crypto;
return true;
}
} catch {
// do nothing, result.isValid still set to false
}
return false;
});
return result;
}
function verifyMultisig (result: VerifyResult, { message, publicKey, signature }: VerifyInput): VerifyResult {
if (![0, 1, 2].includes(signature[0]) || ![65, 66].includes(signature.length)) {
throw new Error(`Unknown crypto type, expected signature prefix [0..2], found ${signature[0]}`);
}
// If the signature is 66 bytes it must be an ecdsa signature
// containing: prefix [1 byte] + signature [65] bytes.
// Remove the and then verify
if (signature.length === 66) {
result = verifyDetect(result, { message, publicKey, signature: signature.subarray(1) }, VERIFIERS_ECDSA);
} else {
// The signature contains 65 bytes which is either
// - A ed25519 or sr25519 signature [1 byte prefix + 64 bytes]
// - An ecdsa signature [65 bytes]
result = verifyDetect(result, { message, publicKey, signature: signature.subarray(1) }, VERIFIERS);
if (!result.isValid) {
result = verifyDetect(result, { message, publicKey, signature }, VERIFIERS_ECDSA);
}
// If both failed, explicitly set crypto to 'none'
if (!result.isValid) {
result.crypto = 'none';
}
}
return result;
}
function getVerifyFn (signature: Uint8Array): VerifyFn {
return [0, 1, 2].includes(signature[0]) && [65, 66].includes(signature.length)
? verifyMultisig
: verifyDetect;
}
export function signatureVerify (message: string | Uint8Array, signature: string | Uint8Array, addressOrPublicKey: string | Uint8Array): VerifyResult {
const signatureU8a = u8aToU8a(signature);
if (![64, 65, 66].includes(signatureU8a.length)) {
throw new Error(`Invalid signature length, expected [64..66] bytes, found ${signatureU8a.length}`);
}
const publicKey = decodeAddress(addressOrPublicKey);
const input = { message: u8aToU8a(message), publicKey, signature: signatureU8a };
const result: VerifyResult = { crypto: 'none', isValid: false, isWrapped: u8aIsWrapped(input.message, true), publicKey };
const isWrappedBytes = u8aIsWrapped(input.message, false);
const verifyFn = getVerifyFn(signatureU8a);
verifyFn(result, input);
if (result.crypto !== 'none' || (result.isWrapped && !isWrappedBytes)) {
return result;
}
input.message = isWrappedBytes
? u8aUnwrapBytes(input.message)
: u8aWrapBytes(input.message);
return verifyFn(result, input);
}
@@ -1,31 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { Keypair } from '../types.js';
import { u8aToHex } from '@polkadot/util';
import { sr25519Agreement, sr25519PairFromSeed } from './index.js';
describe('agreement', (): void => {
let pairA: Keypair;
let pairB: Keypair;
beforeEach((): void => {
pairA = sr25519PairFromSeed('0x98b3d305d5a5eace562387e47e59badd4d77e3f72cabfb10a60f8a197059f0a8');
pairB = sr25519PairFromSeed('0x9732eea001851ff862d949a1699c9971f3a26edbede2ad7922cbbe9a0701f366');
});
it('matches a known agreement (both ways)', (): void => {
const TEST = '0xb03a0b198c34c16f35cae933d88b16341b4cef3e84e851f20e664c6a30527f4e';
expect(
u8aToHex(sr25519Agreement(pairA.secretKey, pairB.publicKey))
).toEqual(TEST);
expect(
u8aToHex(sr25519Agreement(pairB.secretKey, pairA.publicKey))
).toEqual(TEST);
});
});
@@ -1,23 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { getSharedSecret } from '@scure/sr25519';
import { u8aToU8a } from '@polkadot/util';
/**
* @name sr25519Agreement
* @description Key agreement between other's public key and self secret key
*/
export function sr25519Agreement (secretKey: string | Uint8Array, publicKey: string | Uint8Array): Uint8Array {
const secretKeyU8a = u8aToU8a(secretKey);
const publicKeyU8a = u8aToU8a(publicKey);
if (publicKeyU8a.length !== 32) {
throw new Error(`Invalid publicKey, received ${publicKeyU8a.length} bytes, expected 32`);
} else if (secretKeyU8a.length !== 64) {
throw new Error(`Invalid secretKey, received ${secretKeyU8a.length} bytes, expected 64`);
}
return getSharedSecret(secretKeyU8a, publicKeyU8a);
}
@@ -1,21 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import * as sr25519 from '@scure/sr25519';
import { isU8a } from '@polkadot/util';
export function createDeriveFn (derive: (pair: Uint8Array, cc: Uint8Array) => Uint8Array): (keypair: Keypair, chainCode: Uint8Array) => Keypair {
return (keypair: Keypair, chainCode: Uint8Array): Keypair => {
if (!isU8a(chainCode) || chainCode.length !== 32) {
throw new Error('Invalid chainCode passed to derive');
}
const secretKey = derive(keypair.secretKey, chainCode);
const publicKey = sr25519.getPublicKey(secretKey);
return { publicKey, secretKey };
};
}
@@ -1,9 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import * as sr25519 from '@scure/sr25519';
import { createDeriveFn } from './derive.js';
// eslint-disable-next-line @typescript-eslint/unbound-method
export const sr25519DeriveHard = /*#__PURE__*/ createDeriveFn(sr25519.HDKD.secretHard);
@@ -1,18 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import * as sr25519 from '@scure/sr25519';
import { isU8a, u8aToU8a } from '@polkadot/util';
export function sr25519DerivePublic (publicKey: string | Uint8Array, chainCode: Uint8Array): Uint8Array {
const publicKeyU8a = u8aToU8a(publicKey);
if (!isU8a(chainCode) || chainCode.length !== 32) {
throw new Error('Invalid chainCode passed to derive');
} else if (publicKeyU8a.length !== 32) {
throw new Error(`Invalid publicKey, received ${publicKeyU8a.length} bytes, expected 32`);
}
return sr25519.HDKD.publicSoft(publicKeyU8a, chainCode);
}
@@ -1,9 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import * as sr25519 from '@scure/sr25519';
import { createDeriveFn } from './derive.js';
// eslint-disable-next-line @typescript-eslint/unbound-method
export const sr25519DeriveSoft = /*#__PURE__*/ createDeriveFn(sr25519.HDKD.secretSoft);
-12
View File
@@ -1,12 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { sr25519Agreement } from './agreement.js';
export { sr25519DeriveHard } from './deriveHard.js';
export { sr25519DerivePublic } from './derivePublic.js';
export { sr25519DeriveSoft } from './deriveSoft.js';
export { sr25519PairFromSeed } from './pair/fromSeed.js';
export { sr25519Sign } from './sign.js';
export { sr25519Verify } from './verify.js';
export { sr25519VrfSign } from './vrfSign.js';
export { sr25519VrfVerify } from './vrfVerify.js';
@@ -1,35 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { stringToU8a, u8aToHex } from '@polkadot/util';
import { mnemonicToMiniSecret } from '../../mnemonic/index.js';
import { sr25519PairFromSeed } from '../index.js';
import tests from './testing.spec.js';
describe('sr25519PairFromSeed', (): void => {
const TEST = stringToU8a('12345678901234567890123456789012');
const RESULT = {
publicKey: new Uint8Array([116, 28, 8, 160, 111, 65, 197, 150, 96, 143, 103, 116, 37, 155, 217, 4, 51, 4, 173, 250, 93, 62, 234, 98, 118, 11, 217, 190, 151, 99, 77, 99]),
secretKey: new Uint8Array([240, 16, 102, 96, 195, 221, 162, 63, 22, 218, 169, 172, 91, 129, 27, 150, 48, 119, 245, 188, 10, 248, 159, 133, 128, 79, 13, 232, 228, 36, 240, 80, 249, 141, 102, 243, 148, 66, 80, 111, 249, 71, 253, 145, 31, 24, 199, 167, 165, 218, 99, 154, 99, 232, 211, 180, 226, 51, 247, 65, 67, 217, 81, 193])
};
it('generates a valid publicKey/secretKey pair (u8a)', (): void => {
expect(
sr25519PairFromSeed(TEST)
).toEqual(RESULT);
});
tests.forEach(([mnemonic, , , secret], index): void => {
it(`creates valid against known (${index})`, (): void => {
const seed = mnemonicToMiniSecret(mnemonic, 'Substrate');
const pair = sr25519PairFromSeed(seed);
expect(
u8aToHex(pair.secretKey)
).toEqual(secret);
});
});
});
@@ -1,28 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import * as sr25519 from '@scure/sr25519';
import { u8aToU8a } from '@polkadot/util';
/**
* @name sr25519PairFromSeed
* @description Returns a object containing a `publicKey` & `secretKey` generated from the supplied seed.
*/
export function sr25519PairFromSeed (seed: string | Uint8Array): Keypair {
const seedU8a = u8aToU8a(seed);
if (seedU8a.length !== 32) {
throw new Error(`Expected a seed matching 32 bytes, found ${seedU8a.length}`);
}
const sec = sr25519.secretFromSeed(seedU8a);
const pub = sr25519.getPublicKey(sec);
return {
publicKey: pub,
secretKey: sec
};
}
@@ -1,23 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { u8aToU8a } from '@polkadot/util';
const SEC_LEN = 64;
const PUB_LEN = 32;
const TOT_LEN = SEC_LEN + PUB_LEN;
export function sr25519PairFromU8a (full: string | Uint8Array): Keypair {
const fullU8a = u8aToU8a(full);
if (fullU8a.length !== TOT_LEN) {
throw new Error(`Expected keypair with ${TOT_LEN} bytes, found ${fullU8a.length}`);
}
return {
publicKey: fullU8a.slice(SEC_LEN, TOT_LEN),
secretKey: fullU8a.slice(0, SEC_LEN)
};
}
@@ -1,10 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { u8aConcat } from '@polkadot/util';
export function sr25519KeypairToU8a ({ publicKey, secretKey }: Keypair): Uint8Array {
return u8aConcat(secretKey, publicKey).slice();
}

Some files were not shown because too many files have changed in this diff Show More