Use @noble/secp256k1 (#1189)

* Use @noble/secp256k1

* Remove elliptic

* Remove @types

* -19

* verify uses recover internally

* fromSeed

* Init Noble utils inside cryptoInit

* remove bn.js dep (used from @polkadot/util)

* With x-noble-secp256k1

* Adjust

* Remove unused functions

* CHANGELOG

* Fix useless assignment

* Cleanup cryptoInit

* adjust hasher

* bn.js only via util

* align versions

* Remove x-secp256k1 (to be pulled in from master merge)

* Remove (now) invalid test

* Remove assertBigInt

* hasBigInt

* cleanups

* Apply suggestions from code review

* evmAddress fixes

* Extra comment
This commit is contained in:
Jaco
2021-12-03 17:23:46 +02:00
committed by GitHub
parent 88c2d16951
commit 1eba41b2e6
17 changed files with 94 additions and 122 deletions
@@ -5,9 +5,9 @@ import type { HexString } from '@polkadot/util/types';
import type { HashType } from '../secp256k1/types';
import type { Prefix } from './types';
import { u8aConcat } from '@polkadot/util';
import { assert, u8aConcat } from '@polkadot/util';
import { secp256k1Hasher } from '../secp256k1/hasher';
import { hasher } from '../secp256k1/hasher';
import { encodeAddress } from './encode';
/**
@@ -15,15 +15,9 @@ import { encodeAddress } from './encode';
* @summary Converts an EVM address to its corresponding SS58 address.
*/
export function evmToAddress (evmAddress: HexString | string | Uint8Array, ss58Format?: Prefix, hashType: HashType = 'blake2'): string {
const wrapError = (message: string) => `Converting ${evmAddress as string}: ${message}`;
const message = u8aConcat('evm:', evmAddress);
if (message.length !== 24) {
throw new Error(wrapError('Invalid evm address length'));
}
assert(message.length === 24, () => `Converting ${evmAddress as string}: Invalid evm address length`);
const address = secp256k1Hasher(hashType, message);
return encodeAddress(address, ss58Format);
return encodeAddress(hasher(hashType, message), ss58Format);
}
+9
View File
@@ -1,7 +1,16 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { u8aConcat } from '@polkadot/util';
import { utils as utilsNobleSecp256k1 } from '@polkadot/x-noble-secp256k1';
import { cryptoWaitReady } from './crypto';
import { hmacSha256AsU8a } from './hmac';
// Set overrides on the secp256k1 utils
// - hmacShaSync - This needs to be set, unset by default
utilsNobleSecp256k1.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]) =>
hmacSha256AsU8a(key, u8aConcat(...messages));
// start init process immediately
cryptoWaitReady().catch((): void => {
+4 -10
View File
@@ -1,10 +1,9 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { assert } from '@polkadot/util';
import { assert, hasBigInt } from '@polkadot/util';
import { isReady, secp256k1Compress as wasm } from '@polkadot/wasm-crypto';
import { secp256k1 } from './secp256k1';
import { Point } from '@polkadot/x-noble-secp256k1';
export function secp256k1Compress (publicKey: Uint8Array, onlyJs?: boolean): Uint8Array {
if (publicKey.length === 33) {
@@ -13,12 +12,7 @@ export function secp256k1Compress (publicKey: Uint8Array, onlyJs?: boolean): Uin
assert(publicKey.length === 65, 'Invalid publicKey provided');
return !onlyJs && isReady()
return !hasBigInt || (!onlyJs && isReady())
? wasm(publicKey)
: new Uint8Array(
secp256k1
.keyFromPublic(publicKey)
.getPublic()
.encodeCompressed()
);
: Point.fromHex(publicKey).toRawBytes(true);
}
+6 -8
View File
@@ -1,11 +1,11 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { assert, bnToU8a, u8aConcat } from '@polkadot/util';
import { assert, bnToU8a, hasBigInt, u8aConcat } from '@polkadot/util';
import { isReady, secp256k1Expand as wasm } from '@polkadot/wasm-crypto';
import { Point } from '@polkadot/x-noble-secp256k1';
import { BN_BE_256_OPTS } from '../bn';
import { secp256k1 } from './secp256k1';
export function secp256k1Expand (publicKey: Uint8Array, onlyJs?: boolean): Uint8Array {
if (publicKey.length === 65) {
@@ -14,16 +14,14 @@ export function secp256k1Expand (publicKey: Uint8Array, onlyJs?: boolean): Uint8
assert(publicKey.length === 33, 'Invalid publicKey provided');
if (!onlyJs && isReady()) {
if (!hasBigInt || (!onlyJs && isReady())) {
return wasm(publicKey).subarray(1);
}
const expanded = secp256k1
.keyFromPublic(publicKey)
.getPublic();
const { x, y } = Point.fromHex(publicKey);
return u8aConcat(
bnToU8a(expanded.getX(), BN_BE_256_OPTS),
bnToU8a(expanded.getY(), BN_BE_256_OPTS)
bnToU8a(x, BN_BE_256_OPTS),
bnToU8a(y, BN_BE_256_OPTS)
);
}
@@ -1,12 +1,12 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1Hasher } from './hasher';
import { hasher } from './hasher';
describe('secp256k1Hasher', (): void => {
describe('hasher', (): void => {
it('creates a blake2 hash', (): void => {
expect(
secp256k1Hasher('blake2', 'abc')
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])
);
@@ -14,7 +14,7 @@ describe('secp256k1Hasher', (): void => {
it('creates a keccak hash', (): void => {
expect(
secp256k1Hasher('keccak', 'abc')
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 -1
View File
@@ -6,7 +6,7 @@ import type { HashType } from './types';
import { blake2AsU8a } from '../blake2';
import { keccakAsU8a } from '../keccak';
export function secp256k1Hasher (hashType: HashType, data: Uint8Array | string, onlyJs?: boolean): Uint8Array {
export function hasher (hashType: HashType, data: Uint8Array | string, onlyJs?: boolean): Uint8Array {
return hashType === 'keccak'
? keccakAsU8a(data, undefined, onlyJs)
: blake2AsU8a(data, undefined, undefined, onlyJs);
@@ -3,11 +3,9 @@
import type { Keypair } from '../../types';
import { assert, bnToU8a } from '@polkadot/util';
import { assert, hasBigInt } from '@polkadot/util';
import { isReady, secp256k1FromSeed } from '@polkadot/wasm-crypto';
import { BN_BE_256_OPTS } from '../../bn';
import { secp256k1 } from '../secp256k1';
import { getPublicKey } from '@polkadot/x-noble-secp256k1';
/**
* @name secp256k1PairFromSeed
@@ -16,7 +14,7 @@ import { secp256k1 } from '../secp256k1';
export function secp256k1PairFromSeed (seed: Uint8Array, onlyJs?: boolean): Keypair {
assert(seed.length === 32, 'Expected valid 32-byte private key as a seed');
if (!onlyJs && isReady()) {
if (!hasBigInt || (!onlyJs && isReady())) {
const full = secp256k1FromSeed(seed);
return {
@@ -25,10 +23,8 @@ export function secp256k1PairFromSeed (seed: Uint8Array, onlyJs?: boolean): Keyp
};
}
const key = secp256k1.keyFromPrivate(seed);
return {
publicKey: new Uint8Array(key.getPublic().encodeCompressed()),
secretKey: bnToU8a(key.getPrivate(), BN_BE_256_OPTS)
publicKey: getPublicKey(seed, true),
secretKey: seed
};
}
+9 -11
View File
@@ -1,28 +1,26 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
import type { HashType } from './types';
import { assert } from '@polkadot/util';
import { assert, hasBigInt, u8aToU8a } from '@polkadot/util';
import { isReady, secp256k1Recover as wasm } from '@polkadot/wasm-crypto';
import { recoverPublicKey, Signature } from '@polkadot/x-noble-secp256k1';
import { secp256k1Compress } from './compress';
import { secp256k1Expand } from './expand';
import { secp256k1 } from './secp256k1';
/**
* @name secp256k1Recover
* @description Recovers a publicKey from the supplied signature
*/
export function secp256k1Recover (message: Uint8Array, signature: Uint8Array, recovery: number, hashType: HashType = 'blake2', onlyJs?: boolean): Uint8Array {
const publicKey = !onlyJs && isReady()
? wasm(message, signature.slice(0, 64), recovery)
: new Uint8Array(
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument
secp256k1
.recoverPubKey(message, { r: signature.slice(0, 32), s: signature.slice(32, 64) }, recovery)
.encode(null, true)
);
export function secp256k1Recover (msgHash: HexString | Uint8Array | string, signature: HexString | Uint8Array | string, 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)
: recoverPublicKey(msg, Signature.fromCompact(sig).toRawBytes(), recovery);
assert(publicKey, 'Unable to recover publicKey from signature');
@@ -1,7 +0,0 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import elliptic from 'elliptic';
// eslint-disable-next-line new-cap
export const secp256k1 = new elliptic.ec('secp256k1');
@@ -5,7 +5,7 @@ import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { performanceWasm } from '../../test/performance';
import { secp256k1PairFromSeed, secp256k1Recover, secp256k1Sign } from '.';
import { secp256k1PairFromSeed, secp256k1Sign } from '.';
const pair = secp256k1PairFromSeed(hexToU8a('0x4380de832af797688026ce24f85204d508243f201650c1a134929e5458b7fbae'));
const msg = hexToU8a('0xa30b64ce1eedf409c8afb801d72c05234e64849ea538c15dd3c8cf4ffcf166c9');
@@ -20,19 +20,16 @@ describe('sign', (): void => {
expect(
secp256k1Sign(msg, pair, undefined, onlyJs)
).toEqual(hexToU8a(
// as of libsecp256k1 0.7.0 the wasm yields different s & v values
onlyJs
? '0xdf92f73d9f060cefacf187b5414491cb992998ace017fa48839b5cda3e264ba8c4efa521361678d9b8582744d77aa4b8d886d7380b7808a683174afad9c4700300'
: '0xdf92f73d9f060cefacf187b5414491cb992998ace017fa48839b5cda3e264ba83b105adec9e9872647a7d8bb28855b45e22805aea3d097953cbb1391f671d13e01'
// 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)
performanceWasm('secp256k1Sign/Verify', 100, (_, onlyJs) => {
const sig = secp256k1Sign(msg, pair, undefined, onlyJs);
return secp256k1Recover(msg, sig, sig[64], undefined, onlyJs);
});
performanceWasm('secp256k1Sign', 1000, (_, onlyJs) =>
secp256k1Sign(msg, pair, undefined, onlyJs)
);
});
+10 -10
View File
@@ -4,12 +4,12 @@
import type { Keypair } from '../types';
import type { HashType } from './types';
import { assert, bnToU8a, u8aConcat } from '@polkadot/util';
import { assert, bnToU8a, hasBigInt, u8aConcat } from '@polkadot/util';
import { isReady, secp256k1Sign as wasm } from '@polkadot/wasm-crypto';
import { Signature, signSync } from '@polkadot/x-noble-secp256k1';
import { BN_BE_256_OPTS } from '../bn';
import { secp256k1Hasher } from './hasher';
import { secp256k1 } from './secp256k1';
import { hasher } from './hasher';
/**
* @name secp256k1Sign
@@ -18,18 +18,18 @@ import { secp256k1 } from './secp256k1';
export function secp256k1Sign (message: Uint8Array | string, { secretKey }: Partial<Keypair>, hashType: HashType = 'blake2', onlyJs?: boolean): Uint8Array {
assert(secretKey?.length === 32, 'Expected valid secp256k1 secretKey, 32-bytes');
const data = secp256k1Hasher(hashType, message, onlyJs);
const data = hasher(hashType, message, onlyJs);
if (!onlyJs && isReady()) {
if (!hasBigInt || (!onlyJs && isReady())) {
return wasm(data, secretKey);
}
const key = secp256k1.keyFromPrivate(secretKey);
const ecsig = key.sign(data);
const [sigBytes, recoveryParam] = signSync(data, secretKey, { canonical: true, recovered: true });
const { r, s } = Signature.fromHex(sigBytes);
return u8aConcat(
bnToU8a(ecsig.r, BN_BE_256_OPTS),
bnToU8a(ecsig.s, BN_BE_256_OPTS),
new Uint8Array([ecsig.recoveryParam || 0])
bnToU8a(r, BN_BE_256_OPTS),
bnToU8a(s, BN_BE_256_OPTS),
new Uint8Array([recoveryParam || 0])
);
}
@@ -1,10 +1,12 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '../cryptoInit';
import { stringToU8a } from '@polkadot/util';
import { randomAsU8a } from '../random/asU8a';
import { secp256k1Hasher } from './hasher';
import { hasher } from './hasher';
import { secp256k1Expand, secp256k1PairFromSeed, secp256k1Sign, secp256k1Verify } from '.';
const MESSAGE = stringToU8a('this is a message');
@@ -27,7 +29,7 @@ describe('sign and verify', (): void => {
it('signs/verifies a message by random key (blake2)', (): void => {
const pair = secp256k1PairFromSeed(randomAsU8a());
const signature = secp256k1Sign(MESSAGE, pair);
const address = secp256k1Hasher('blake2', pair.publicKey);
const address = hasher('blake2', pair.publicKey);
expect(secp256k1Verify(MESSAGE, signature, address)).toEqual(true);
});
@@ -35,7 +37,7 @@ describe('sign and verify', (): void => {
it('signs/verifies a message by random key (keccak)', (): void => {
const pair = secp256k1PairFromSeed(randomAsU8a());
const signature = secp256k1Sign(MESSAGE, pair, 'keccak');
const address = secp256k1Hasher('keccak', secp256k1Expand(pair.publicKey));
const address = hasher('keccak', secp256k1Expand(pair.publicKey));
expect(secp256k1Verify(MESSAGE, signature, address, 'keccak')).toEqual(true);
});
@@ -43,7 +45,7 @@ describe('sign and verify', (): void => {
it('fails verification on hasher mismatches', (): void => {
const pair = secp256k1PairFromSeed(randomAsU8a());
const signature = secp256k1Sign(MESSAGE, pair, 'keccak');
const address = secp256k1Hasher('keccak', secp256k1Expand(pair.publicKey));
const address = hasher('keccak', secp256k1Expand(pair.publicKey));
expect(secp256k1Verify(MESSAGE, signature, address, 'blake2')).toEqual(false);
});
@@ -57,7 +59,7 @@ describe('sign and verify', (): void => {
secp256k1Verify(
MESSAGE,
secp256k1Sign(MESSAGE, pair, 'blake2'),
secp256k1Hasher('blake2', pair.publicKey),
hasher('blake2', pair.publicKey),
'blake2'
)
).toEqual(true);
@@ -77,7 +79,7 @@ describe('sign and verify', (): void => {
secp256k1Verify(
MESSAGE,
secp256k1Sign(MESSAGE, pair, 'keccak'),
secp256k1Hasher('keccak', secp256k1Expand(pair.publicKey)),
hasher('keccak', secp256k1Expand(pair.publicKey)),
'keccak'
)
).toEqual(true);
@@ -1,11 +1,13 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '../cryptoInit';
import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { performanceWasm } from '../../test/performance';
import { secp256k1Hasher } from './hasher';
import { hasher } from './hasher';
import { secp256k1PairFromSeed, secp256k1Verify } from '.';
const message = 'Pay KSMs to the Kusama account:88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee';
@@ -33,7 +35,7 @@ describe('secp256k1Verify', (): void => {
const msg = hexToU8a('0xa30b64ce1eedf409c8afb801d72c05234e64849ea538c15dd3c8cf4ffcf166c9');
const addr = isPublic
? pair.publicKey
: secp256k1Hasher('blake2', pair.publicKey, onlyJs);
: hasher('blake2', pair.publicKey, onlyJs);
it('signature from JS', (): void => {
expect(
+6 -12
View File
@@ -6,26 +6,20 @@ import type { HashType } from './types';
import { assert, u8aEq, u8aToU8a } from '@polkadot/util';
import { secp256k1Hasher } from './hasher';
import { hasher } from './hasher';
import { secp256k1Recover } from './recover';
/**
* @name secp256k1Verify
* @description Verifies the signature of `message`, using the supplied pair
*/
export function secp256k1Verify (message: HexString | Uint8Array | string, signature: HexString | Uint8Array | string, address: HexString | Uint8Array | string, hashType: HashType = 'blake2', onlyJs?: boolean): boolean {
const u8a = u8aToU8a(signature);
export function secp256k1Verify (msgHash: HexString | Uint8Array | string, signature: HexString | Uint8Array | string, address: HexString | Uint8Array | string, hashType: HashType = 'blake2', onlyJs?: boolean): boolean {
const sig = u8aToU8a(signature);
assert(u8a.length === 65, `Expected signature with 65 bytes, ${u8a.length} found instead`);
assert(sig.length === 65, `Expected signature with 65 bytes, ${sig.length} found instead`);
const publicKey = secp256k1Recover(
secp256k1Hasher(hashType, message),
u8a.subarray(0, 64),
u8a[64],
hashType,
onlyJs
);
const signerAddr = secp256k1Hasher(hashType, publicKey, onlyJs);
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