Ethereum keypair support (#661)
* Ethereum keypair support * keypair support from seed * Signature verification * Cleanups * Cleanup encode (no extra toLower) * u8aToU8a * Simulate random failure with range * Cleanup BN conversions
This commit is contained in:
@@ -12,6 +12,14 @@ import { DEV_PHRASE } from './defaults';
|
||||
import createPair from './pair';
|
||||
import Pairs from './pairs';
|
||||
|
||||
const keypairFromSeed = {
|
||||
ecdsa: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
|
||||
ed25519: (seed: Uint8Array): Keypair => naclFromSeed(seed),
|
||||
// FIXME This is not Ethereum-compatible
|
||||
ethereum: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
|
||||
sr25519: (seed: Uint8Array): Keypair => schnorrkelFromSeed(seed)
|
||||
};
|
||||
|
||||
/**
|
||||
* # @polkadot/keyring
|
||||
*
|
||||
@@ -128,13 +136,7 @@ export default class Keyring implements KeyringInstance {
|
||||
* `addPair` to store in a keyring pair dictionary the public key of the generated pair as a key and the pair as the associated value.
|
||||
*/
|
||||
public addFromSeed (seed: Uint8Array, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
|
||||
const keypair = {
|
||||
ecdsa: (): Keypair => secp256k1FromSeed(seed),
|
||||
ed25519: (): Keypair => naclFromSeed(seed),
|
||||
sr25519: (): Keypair => schnorrkelFromSeed(seed)
|
||||
}[type]();
|
||||
|
||||
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, keypair, meta, null));
|
||||
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, keypairFromSeed[type](seed), meta, null));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,12 +182,7 @@ export default class Keyring implements KeyringInstance {
|
||||
}
|
||||
}
|
||||
|
||||
const keypair = {
|
||||
ecdsa: (): Keypair => secp256k1FromSeed(seed),
|
||||
ed25519: (): Keypair => naclFromSeed(seed),
|
||||
sr25519: (): Keypair => schnorrkelFromSeed(seed)
|
||||
};
|
||||
const derived = keyFromPath(keypair[type](), path, type);
|
||||
const derived = keyFromPath(keypairFromSeed[type](seed), path, type);
|
||||
|
||||
return createPair({ toSS58: this.encodeAddress, type }, derived, meta, null);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { KeyringPair, KeyringPair$Json, KeyringPair$JsonEncodingTypes, KeyringPa
|
||||
import { PairInfo } from './types';
|
||||
|
||||
import { assert, u8aConcat } from '@polkadot/util';
|
||||
import { keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclSign, naclVerify, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVerify, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, secp256k1Verify, blake2AsU8a } from '@polkadot/util-crypto';
|
||||
import { blake2AsU8a, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclSign, naclVerify, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVerify, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, secp256k1Verify } from '@polkadot/util-crypto';
|
||||
|
||||
import decode from './decode';
|
||||
import encode from './encode';
|
||||
@@ -22,6 +22,7 @@ const SIG_TYPE_NONE = new Uint8Array();
|
||||
const SIG_TYPE_ED25519 = new Uint8Array([0]);
|
||||
const SIG_TYPE_SR25519 = new Uint8Array([1]);
|
||||
const SIG_TYPE_ECDSA = new Uint8Array([2]);
|
||||
const SIG_TYPE_ETHEREUM = SIG_TYPE_ECDSA;
|
||||
|
||||
function isEmpty (u8a: Uint8Array): boolean {
|
||||
return u8a.reduce((count, u8): number => count + u8, 0) === 0;
|
||||
@@ -31,6 +32,8 @@ function fromSeed (type: KeypairType, seed: Uint8Array): Keypair {
|
||||
return {
|
||||
ecdsa: (): Keypair => secp256k1FromSeed(seed),
|
||||
ed25519: (): Keypair => naclFromSeed(seed),
|
||||
// FIXME This needs to actually be Ethereum-compatible
|
||||
ethereum: (): Keypair => secp256k1FromSeed(seed),
|
||||
sr25519: (): Keypair => schnorrkelFromSeed(seed)
|
||||
}[type]();
|
||||
}
|
||||
@@ -39,14 +42,16 @@ function multiSignaturePrefix (type: KeypairType): Uint8Array {
|
||||
return {
|
||||
ecdsa: SIG_TYPE_ECDSA,
|
||||
ed25519: SIG_TYPE_ED25519,
|
||||
ethereum: SIG_TYPE_ETHEREUM,
|
||||
sr25519: SIG_TYPE_SR25519
|
||||
}[type];
|
||||
}
|
||||
|
||||
function sign (type: KeypairType, message: Uint8Array, pair: Partial<Keypair>, { withType = false }: SignOptions = {}): Uint8Array {
|
||||
const signature = {
|
||||
ecdsa: (): Uint8Array => secp256k1Sign(message, pair),
|
||||
ecdsa: (): Uint8Array => secp256k1Sign(message, pair, 'blake2'),
|
||||
ed25519: (): Uint8Array => naclSign(message, pair),
|
||||
ethereum: (): Uint8Array => secp256k1Sign(message, pair, 'keccak'),
|
||||
sr25519: (): Uint8Array => schnorrkelSign(message, pair)
|
||||
}[type]();
|
||||
|
||||
@@ -62,15 +67,18 @@ function sign (type: KeypairType, message: Uint8Array, pair: Partial<Keypair>, {
|
||||
|
||||
function verify (type: KeypairType, message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean {
|
||||
return {
|
||||
ecdsa: (): boolean => secp256k1Verify(message, signature, blake2AsU8a(publicKey, 256)),
|
||||
ecdsa: (): boolean => secp256k1Verify(message, signature, blake2AsU8a(publicKey), 'blake2'),
|
||||
ed25519: (): boolean => naclVerify(message, signature, publicKey),
|
||||
ethereum: (): boolean => secp256k1Verify(message, signature, keccakAsU8a(publicKey), 'keccak'),
|
||||
sr25519: (): boolean => schnorrkelVerify(message, signature, publicKey)
|
||||
}[type]();
|
||||
}
|
||||
|
||||
function getAddress (type: KeypairType, publicKey: Uint8Array): Uint8Array {
|
||||
if (type === 'ecdsa' && publicKey.length > 32) {
|
||||
return blake2AsU8a(publicKey, 256);
|
||||
return blake2AsU8a(publicKey);
|
||||
} else if (type === 'ethereum' && publicKey.length > 32) {
|
||||
return keccakAsU8a(publicKey);
|
||||
} else {
|
||||
return publicKey;
|
||||
}
|
||||
@@ -122,7 +130,9 @@ export default function createPair ({ toSS58, type }: Setup, { publicKey, secret
|
||||
|
||||
return {
|
||||
get address (): string {
|
||||
return toSS58(getAddress(type, publicKey));
|
||||
return type === 'ethereum'
|
||||
? ethereumEncode(getAddress(type, publicKey))
|
||||
: toSS58(getAddress(type, publicKey));
|
||||
},
|
||||
get isLocked (): boolean {
|
||||
return isLocked(secretKey);
|
||||
|
||||
@@ -12,13 +12,13 @@ export default function ethereumEncode (_address?: string | Uint8Array): string
|
||||
return '0x';
|
||||
}
|
||||
|
||||
const address = u8aToHex(u8aToU8a(_address), -1, false).toLowerCase();
|
||||
const address = u8aToHex(u8aToU8a(_address), -1, false);
|
||||
|
||||
if (!isEthereumAddress(`0x${address}`)) {
|
||||
return '0x';
|
||||
}
|
||||
|
||||
const hash = u8aToHex(keccakAsU8a(address.toLowerCase()), -1, false);
|
||||
const hash = u8aToHex(keccakAsU8a(address), -1, false);
|
||||
let result = '';
|
||||
|
||||
for (let index = 0; index < 40; index++) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import keyHdkdEcdsa from './hdkdEcdsa';
|
||||
const generators = {
|
||||
ecdsa: keyHdkdEcdsa,
|
||||
ed25519: keyHdkdEd15519,
|
||||
// FIXME This is Substrate-compatible, not Ethereum-compatible
|
||||
ethereum: keyHdkdEcdsa,
|
||||
sr25519: keyHdkdSr15519
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const HASH_TYPES = ['blake2', 'keccak'];
|
||||
|
||||
export default function secp256k1Hasher (hashType: HashType, data: Uint8Array | string): Uint8Array {
|
||||
if (hashType === 'blake2') {
|
||||
return blake2AsU8a(data, 256);
|
||||
return blake2AsU8a(data);
|
||||
} else if (hashType === 'keccak') {
|
||||
return keccakAsU8a(data);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Keypair } from '../../types';
|
||||
|
||||
import elliptic from 'elliptic';
|
||||
import { assert, hexToU8a } from '@polkadot/util';
|
||||
import { assert, bnToU8a } from '@polkadot/util';
|
||||
|
||||
const EC = elliptic.ec;
|
||||
const ec = new EC('secp256k1');
|
||||
@@ -20,7 +20,7 @@ export default function secp256k1KeypairFromSeed (seed: Uint8Array): Keypair {
|
||||
const key = ec.keyFromPrivate(seed);
|
||||
|
||||
return {
|
||||
publicKey: new Uint8Array(key.getPublic().encodeCompressed('array')),
|
||||
secretKey: hexToU8a(`0x${key.getPrivate('hex')}`)
|
||||
publicKey: new Uint8Array(key.getPublic().encodeCompressed()),
|
||||
secretKey: bnToU8a(key.getPrivate(), { bitLength: 256, isLe: false })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Keypair } from '../types';
|
||||
import { HashType } from './types';
|
||||
|
||||
import elliptic from 'elliptic';
|
||||
import { assert, u8aConcat } from '@polkadot/util';
|
||||
import { assert, bnToU8a, u8aConcat } from '@polkadot/util';
|
||||
|
||||
import hasher from './hasher';
|
||||
|
||||
@@ -22,9 +22,10 @@ export default function secp256k1Sign (message: Uint8Array | string, { secretKey
|
||||
|
||||
const key = ec.keyFromPrivate(secretKey);
|
||||
const ecsig = key.sign(hasher(hashType, message));
|
||||
const rParam = new Uint8Array(ecsig.r.toArray());
|
||||
const sParam = new Uint8Array(ecsig.s.toArray());
|
||||
const recoveryParam = Uint8Array.of(ecsig.recoveryParam || 0);
|
||||
|
||||
return u8aConcat(rParam, sParam, recoveryParam);
|
||||
return u8aConcat(
|
||||
bnToU8a(ecsig.r, { bitLength: 256, isLe: false }),
|
||||
bnToU8a(ecsig.s, { bitLength: 256, isLe: false }),
|
||||
new Uint8Array([ecsig.recoveryParam || 0])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('sign and verify', (): void => {
|
||||
expect(sign(MESSAGE, pair)).toHaveLength(65);
|
||||
});
|
||||
|
||||
it('can sign and verify a message by random key', (): void => {
|
||||
it('signs/verifies a message by random key (blake2)', (): void => {
|
||||
const pair = pairFromSeed(randomAsU8a());
|
||||
const signature = sign(MESSAGE, pair);
|
||||
const address = hasher('blake2', pair.publicKey);
|
||||
@@ -35,7 +35,7 @@ describe('sign and verify', (): void => {
|
||||
expect(verify(MESSAGE, signature, address)).toBe(true);
|
||||
});
|
||||
|
||||
it('can sign and verify a message by random key (keccak)', (): void => {
|
||||
it('signs/verifies a message by random key (keccak)', (): void => {
|
||||
const pair = pairFromSeed(randomAsU8a());
|
||||
const signature = sign(MESSAGE, pair, 'keccak');
|
||||
const address = hasher('keccak', pair.publicKey);
|
||||
@@ -43,11 +43,37 @@ describe('sign and verify', (): void => {
|
||||
expect(verify(MESSAGE, signature, address, 'keccak')).toBe(true);
|
||||
});
|
||||
|
||||
it('can fals verification on hasher mismatches', (): void => {
|
||||
it('fails verification on hasher mismatches', (): void => {
|
||||
const pair = pairFromSeed(randomAsU8a());
|
||||
const signature = sign(MESSAGE, pair, 'keccak');
|
||||
const address = hasher('keccak', pair.publicKey);
|
||||
|
||||
expect(verify(MESSAGE, signature, address, 'blake2')).toBe(false);
|
||||
});
|
||||
|
||||
it('works over a range of random keys (blake2)', (): void => {
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const pair = pairFromSeed(randomAsU8a());
|
||||
|
||||
try {
|
||||
expect(verify(MESSAGE, sign(MESSAGE, pair, 'blake2'), hasher('blake2', pair.publicKey), 'blake2')).toBe(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 = pairFromSeed(randomAsU8a());
|
||||
|
||||
try {
|
||||
expect(verify(MESSAGE, sign(MESSAGE, pair, 'keccak'), hasher('keccak', pair.publicKey), 'keccak')).toBe(true);
|
||||
} catch (error) {
|
||||
console.error(`keccak failed on #${i}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { HashType } from './types';
|
||||
|
||||
import elliptic from 'elliptic';
|
||||
import { u8aToU8a } from '@polkadot/util';
|
||||
import { assert, u8aToU8a } from '@polkadot/util';
|
||||
|
||||
import hasher from './hasher';
|
||||
|
||||
@@ -18,6 +18,9 @@ const ec = new EC('secp256k1');
|
||||
*/
|
||||
export default function secp256k1Verify (message: Uint8Array | string, signature: Uint8Array | string, address: Uint8Array | string, hashType: HashType = 'blake2'): boolean {
|
||||
const signatureU8a = u8aToU8a(signature);
|
||||
|
||||
assert(signatureU8a.length === 65, `Expected signature with 65 bytes, ${signatureU8a.length} found instead`);
|
||||
|
||||
const sig = {
|
||||
r: signatureU8a.slice(0, 32),
|
||||
s: signatureU8a.slice(32, 64)
|
||||
@@ -26,7 +29,7 @@ export default function secp256k1Verify (message: Uint8Array | string, signature
|
||||
const publicKey = new Uint8Array(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
|
||||
ec.recoverPubKey(hasher(hashType, message), sig, recovery)
|
||||
.encodeCompressed(null)
|
||||
.encodeCompressed()
|
||||
);
|
||||
|
||||
return Buffer.compare(hasher(hashType, publicKey), u8aToU8a(address)) === 0;
|
||||
|
||||
@@ -14,13 +14,23 @@ import secp256k1Verify from '../secp256k1/verify';
|
||||
const VERIFIERS: [KeypairType, (message: Uint8Array | string, signature: Uint8Array | string, publicKey: Uint8Array | string) => boolean][] = [
|
||||
['ed25519', naclVerify],
|
||||
['sr25519', schnorrkelVerify],
|
||||
['ecdsa', secp256k1Verify]
|
||||
[
|
||||
'ecdsa',
|
||||
(message: Uint8Array | string, signature: Uint8Array | string, publicKey: Uint8Array | string) =>
|
||||
secp256k1Verify(message, signature, publicKey, 'blake2')
|
||||
],
|
||||
[
|
||||
'ethereum',
|
||||
(message: Uint8Array | string, signature: Uint8Array | string, publicKey: Uint8Array | string) =>
|
||||
secp256k1Verify(message, signature, publicKey, 'keccak')
|
||||
]
|
||||
];
|
||||
|
||||
const CRYPTO_TYPES: KeypairType[] = [
|
||||
'ed25519',
|
||||
'sr25519',
|
||||
'ecdsa'
|
||||
'ecdsa',
|
||||
'ethereum'
|
||||
];
|
||||
|
||||
function verifyDetect (result: VerifyResult, message: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array): VerifyResult {
|
||||
@@ -48,8 +58,9 @@ function verifyMultisig (result: VerifyResult, message: Uint8Array | string, sig
|
||||
|
||||
try {
|
||||
result.isValid = {
|
||||
ecdsa: (): boolean => secp256k1Verify(message, signature.subarray(1), publicKey),
|
||||
ecdsa: (): boolean => secp256k1Verify(message, signature.subarray(1), publicKey, 'blake2'),
|
||||
ed25519: (): boolean => naclVerify(message, signature.subarray(1), publicKey),
|
||||
ethereum: (): boolean => secp256k1Verify(message, signature.subarray(1), publicKey, 'keccak'),
|
||||
none: (): boolean => { throw Error('no verify for `none` crypto type'); },
|
||||
sr25519: (): boolean => schnorrkelVerify(message, signature.subarray(1), publicKey)
|
||||
}[result.crypto]();
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface Seedpair {
|
||||
seed: Uint8Array;
|
||||
}
|
||||
|
||||
export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa';
|
||||
export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum';
|
||||
|
||||
export interface VerifyResult {
|
||||
crypto: 'none' | KeypairType;
|
||||
|
||||
Reference in New Issue
Block a user