Implement first eth hd derivation test (#858)

* implement first eth hd test

* lint

* added gerald test address

* test now passes

* started implementing sith local secp256k1

* Pass publicKey through on verify

* Some test fixes

* renamed tweakadd

* added suggestions

* remove logs and complete test for tweakAdd

* lint

* reviewed

* solve linting problems

* relocate hd package

* Apply suggestions from code review

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* added asset for tolegacyseed

* yarn.lock and linting

* remove isUint8 util

* fix tests and added tests

* fixed test

Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
Antoine Estienne
2021-02-24 15:50:12 +01:00
committed by GitHub
co-authored by Jaco Greeff
parent c7e474cdc6
commit c46cd5030e
16 changed files with 472 additions and 39 deletions
+1
View File
@@ -27,6 +27,7 @@
"elliptic": "^6.5.4",
"hash.js": "^1.1.7",
"js-sha3": "^0.8.0",
"safe-buffer": "^5.1.1",
"scryptsy": "^2.1.0",
"tweetnacl": "^1.0.3",
"xxhashjs": "^0.2.2"
@@ -0,0 +1,55 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { mnemonicToLegacySeed } from '@polkadot/util-crypto';
import { HDKeyEth } from '.';
describe('HDKeyEth', (): 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 and a derivation path', (): void => {
// derive seed from mnemonic
const seed = mnemonicToLegacySeed(PHRASE, '', false, 64);
// obtain key from seed
const key = HDKeyEth.fromMasterSeed(seed);
expect(key.publicKey).toEqual(PUBLIC);
expect(key.privateKey).toEqual(SECRET);
// derive key from original key using derivation path
const { privateKey, publicKey } = key.derive(derivationPath);
expect(publicKey).toEqual(PUBLICDERIVED);
expect(privateKey).toEqual(SECRETDERIVED);
});
});
@@ -0,0 +1,222 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import * as crypto from 'crypto';
import { Buffer } from 'safe-buffer';
import { assert } from '@polkadot/util';
import { secp256k1KeypairFromSeed, secp256k1PrivateKeyTweakAdd } from '../..';
const MASTER_SECRET = Buffer.from('Bitcoin seed', 'utf8');
const HARDENED_OFFSET = 0x80000000;
// Bitcoin hardcoded by default, can use package `coininfo` for others
const BITCOIN_VERSIONS = { private: 0x0488ADE4, public: 0x0488B21E };
interface Versions{
private: number;
public: number;
}
// function to retype Buffer to ArrayBuffer in a controlled manner
function bufferAsArrayBuffer (buffer: Buffer): ArrayBuffer {
return buffer as unknown as ArrayBuffer;
}
// function to retype Uint8Array to Buffer in a controlled manner
function bytesAsBuffer (bytes: Uint8Array): Buffer {
return bytes as unknown as Buffer;
}
export class HDKeyEth {
versions: Versions;
public depth: number;
index: number;
private _privateKey: Uint8Array|null;
private _publicKey: Uint8Array|null;
public chainCode: Uint8Array|null;
private _fingerprint: number|null;
parentFingerprint: number|null;
private _identifier: Buffer|null;
constructor (versions?: Versions) {
this.versions = versions || BITCOIN_VERSIONS;
this.depth = 0;
this.index = 0;
this._privateKey = null;
this._publicKey = null;
this._identifier = null;
this.chainCode = null;
this._fingerprint = null;
this.parentFingerprint = 0;
}
// private key
set privateKey (value: Uint8Array|null) {
assert(value && value.length === 32, 'Private key must be 32 bytes.');
// TODO: implement privateKeyVerify for local secp256k1
// assert(secp256k1.privateKeyVerify(value) === true, 'Invalid private key');
this._privateKey = value;
if (value) {
this._publicKey = secp256k1KeypairFromSeed(value).publicKey;
this._identifier = this._publicKey ? this.hash160(this._publicKey) : null;
this._fingerprint = this._identifier ? this._identifier.slice(0, 4).readUInt32BE(0) : null;
}
}
get privateKey (): Uint8Array|null {
return this._privateKey;
}
// public key
get publicKey (): Uint8Array|null {
return this._publicKey;
}
set publicKey (value: Uint8Array|null) {
assert(value && (value.length === 33 || value.length === 65), 'Public key must be 33 or 65 bytes.');
// TODO: implement publicKeyVerify for local secp256k1
// assert(secp256k1.publicKeyVerify(value) === true, 'Invalid public key');
// TODO: should I use the compress function here?
this._publicKey = value; // new Uint8Array(Buffer.from(secp256k1.publicKeyConvert(value, true))); // force compressed point
this._identifier = this._publicKey ? this.hash160(this._publicKey) : null;
this._fingerprint = this._identifier ? this._identifier.slice(0, 4).readUInt32BE(0) : null;
this._privateKey = null;
}
// fingerprint
get fingerprint (): number|null {
return this._fingerprint;
}
// identifier
get identifier (): Buffer|null {
return this._identifier;
}
// fingerprint
get pubKeyHash (): Buffer|null {
return this.identifier;
}
// derive
public derive (path: string): HDKeyEth {
if (path === 'm' || path === 'M' || path === "m'" || path === "M'") {
return this;
}
const entries = path.split('/');
let hdkey = this as HDKeyEth;
entries.forEach(function (c, i) {
if (i === 0) {
assert(/^[mM]{1}/.test(c), 'Path must start with "m" or "M"');
return;
}
const hardened = (c.length > 1) && (c[c.length - 1] === "'");
let childIndex = parseInt(c, 10); // & (HARDENED_OFFSET - 1)
assert(childIndex < HARDENED_OFFSET, 'Invalid index');
if (hardened) childIndex += HARDENED_OFFSET;
hdkey = hdkey.deriveChild(childIndex);
});
return hdkey;
}
// deriveChild
private deriveChild (index: number): HDKeyEth {
const isHardened = index >= HARDENED_OFFSET;
const indexBuffer = Buffer.allocUnsafe(4);
indexBuffer.writeUInt32BE(index, 0);
let data;
if (isHardened) { // Hardened child
assert(this.privateKey, 'Could not derive hardened child key');
if (this.privateKey) {
const pk = this.privateKey;
const zb = Buffer.alloc(1);
const pkConcat = Buffer.concat([zb, bytesAsBuffer(pk)]);
// data = 0x00 || ser256(kpar) || ser32(index)
data = Buffer.concat([pkConcat, indexBuffer]);
} else {
throw new Error('Could not derive hardened child key : no privatekey');
}
} else { // Normal child
// data = serP(point(kpar)) || ser32(index)
// = serP(Kpar) || ser32(index)
if (this.publicKey) {
data = Buffer.concat([bytesAsBuffer(this.publicKey), indexBuffer]);
} else {
throw new Error('Could not derive hardened child key : no publicKey');
}
}
let I: Buffer = Buffer.from('');
if (this.chainCode) {
I = bytesAsBuffer(crypto.createHmac('sha512', this.chainCode).update(data as unknown as crypto.BinaryLike).digest());
} else {
throw new Error('deriveChild error: no chainCode');
}
const IL: Buffer = I.slice(0, 32);
const IR: Buffer = I.slice(32);
const hd = new HDKeyEth(this.versions);
// Private parent key -> private child key
if (this.privateKey) {
// ki = parse256(IL) + kpar (mod n)r
try {
hd.privateKey = new Uint8Array(bufferAsArrayBuffer(Buffer.from(secp256k1PrivateKeyTweakAdd(new Uint8Array(bufferAsArrayBuffer(Buffer.from(this.privateKey))), new Uint8Array(bufferAsArrayBuffer(IL))))));
// throw if IL >= n || (privateKey + IL) === 0
} catch (err) {
console.log('error when secp256k1PrivateKeyTweakAdd in eth key derivation', err);
// In case parse256(IL) >= n or ki == 0, one should proceed with the next value for i
return this.deriveChild(index + 1);
}
// Public parent key -> public child key
} else {
throw new Error('PublicKey derivation without private key is not supported at the moment');
}
hd.chainCode = new Uint8Array(bufferAsArrayBuffer(IR));
hd.depth = this.depth + 1;
hd.parentFingerprint = this.fingerprint;// .readUInt32BE(0)
hd.index = index;
return hd;
}
// fromMasterSeed
public static fromMasterSeed = function (seedBuffer: crypto.BinaryLike, versions?: Versions): HDKeyEth {
const I = crypto.createHmac('sha512', new Uint8Array(bufferAsArrayBuffer(MASTER_SECRET))).update(seedBuffer).digest();
const IL = I.slice(0, 32);
const IR = I.slice(32);
const hdkey = versions ? new HDKeyEth(versions) : new HDKeyEth();
hdkey.chainCode = new Uint8Array(IR);
hdkey.privateKey = new Uint8Array(IL);
return hdkey;
}
private hash160 (buf: crypto.BinaryLike): Buffer {
const sha = crypto.createHash('sha256').update(buf).digest();
return (crypto.createHash('ripemd160').update(sha).digest()) as unknown as Buffer;
}
}
+1
View File
@@ -2,3 +2,4 @@
// SPDX-License-Identifier: Apache-2.0
export { hdLedger } from './ledger';
export { HDKeyEth } from './ethereum';
@@ -7,6 +7,7 @@ import { DeriveJunction } from './DeriveJunction';
import { keyExtractPath } from './extractPath';
export interface ExtractResult {
derivePath: string,
password?: string;
path: DeriveJunction[];
phrase: string;
@@ -27,6 +28,7 @@ export function keyExtractSuri (suri: string): ExtractResult {
const { path } = keyExtractPath(derivePath);
return {
derivePath,
password,
path,
phrase
@@ -7,7 +7,8 @@ import { waitReady } from '@polkadot/wasm-crypto';
import { mnemonicToLegacySeed } from './';
const MNEMONIC = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const SEED = '0x3c121e20de068083b49c2315697fb59a2d9e8643c24e5ea7628132c58969a027';
const SEED_32 = '0x3c121e20de068083b49c2315697fb59a2d9e8643c24e5ea7628132c58969a027';
const SEED_64 = '0x3c121e20de068083b49c2315697fb59a2d9e8643c24e5ea7628132c58969a0275693dd5bd9d4cc9e648475eba9613ed4678f4d62560a9c42f75bac04022ded25';
describe('mnemonicToLegacySeed', (): void => {
beforeEach(async (): Promise<void> => {
@@ -26,10 +27,18 @@ describe('mnemonicToLegacySeed', (): void => {
});
[false, true].forEach((onlyJs): void => {
it(`generates a valid seed (onlyJs = ${onlyJs.toString()})`, (): void => {
it(`generates a valid 64bytes seed (onlyJs = ${onlyJs.toString()})`, (): void => {
expect(
u8aToHex(mnemonicToLegacySeed(MNEMONIC, undefined, onlyJs, 64))
).toEqual(SEED_64);
});
});
[false, true].forEach((onlyJs): void => {
it(`generates a valid 32bytes seed (onlyJs = ${onlyJs.toString()})`, (): void => {
expect(
u8aToHex(mnemonicToLegacySeed(MNEMONIC, undefined, onlyJs))
).toEqual(SEED);
).toEqual(SEED_32);
});
});
@@ -24,10 +24,17 @@ import { mnemonicValidate } from './validate';
* }
* ```
*/
export function mnemonicToLegacySeed (mnemonic: string, password = '', onlyJs = false): Uint8Array {
export function mnemonicToLegacySeed (mnemonic: string, password = '', onlyJs = false, byteLength: 32 | 64 = 32): Uint8Array {
assert(mnemonicValidate(mnemonic), 'Invalid bip39 mnemonic specified');
assert([32, 64].includes(byteLength), `Invalid seed length ${byteLength}, expected 32 or 64`);
return isReady() && !onlyJs
? bip39ToSeed(mnemonic, password)
: mnemonicToSeedSync(mnemonic, password).subarray(0, 32);
if (byteLength && byteLength === 32) {
return isReady() && !onlyJs
? bip39ToSeed(mnemonic, password)
: mnemonicToSeedSync(mnemonic, password).subarray(0, 32);
} else if (byteLength === 64) {
return mnemonicToSeedSync(mnemonic, password);
} else {
return new Uint8Array();
}
}
@@ -7,3 +7,4 @@ export { secp256k1KeypairFromSeed } from './keypair/fromSeed';
export { secp256k1Recover } from './recover';
export { secp256k1Sign } from './sign';
export { secp256k1Verify } from './verify';
export { secp256k1PrivateKeyTweakAdd } from './tweakAdd';
@@ -0,0 +1,36 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1PrivateKeyTweakAdd } from './tweakAdd';
describe('TweakAdd', (): void => {
describe('PrivateKey TweakAdd', (): 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).toString()
).toBe(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
]).toString());
});
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]);
let bool = false;
try {
secp256k1PrivateKeyTweakAdd(A, B);
} catch (e) {
expect((e as Error).toString()).toEqual('Error: Expected tweak to be an Uint8Array with length 32');
bool = true;
}
expect(bool).toBe(true);
});
});
});
@@ -0,0 +1,46 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import BN from 'bn.js';
import { ec as EC } from 'elliptic';
import { assert, isU8a } from '@polkadot/util';
const ec = new EC('secp256k1');
const ecparams = ec.curve as {n: BN};
const errors = {
PUBKEY_PARSE: 'Public Key could not be parsed',
TWEAK_ADD: 'The tweak was out of range or the resulted private key is invalid'
};
// Private key
export function secp256k1PrivateKeyTweakAdd (seckey: Uint8Array, tweak: Uint8Array): Uint8Array {
assert(isU8a(seckey) && seckey.length === 32, 'Expected seckey to be an Uint8Array with length 32');
assert(isU8a(tweak) && tweak.length === 32, 'Expected tweak to be an Uint8Array with length 32');
switch (_secp256k1PrivateKeyTweakAdd(seckey, tweak)) {
case 0:
return seckey;
case 1:
throw new Error(errors.TWEAK_ADD);
}
return seckey;
}
function _secp256k1PrivateKeyTweakAdd (seckey: Uint8Array, tweak: Uint8Array): number {
const bn = new BN(tweak);
if (bn.cmp(ecparams.n) >= 0) return 1;
bn.iadd(new BN(seckey));
if (bn.cmp(ecparams.n) >= 0) bn.isub(ecparams.n);
if (bn.isZero()) return 1;
const tweaked = bn.toArrayLike(Buffer, 'be', 32);
seckey.set(tweaked);
return 0;
}