Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate that master private key and seed are within bounds #118

Merged
merged 3 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/BIP44Node.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { bytesToHex } from '@metamask/utils';

import { BIP44Node, BIP44PurposeNodeToken } from '.';
import { BIP44Node, BIP44PurposeNodeToken, secp256k1 } from '.';
import fixtures from '../test/fixtures';
import { compressPublicKey } from './curves/secp256k1';
import { createBip39KeyFromSeed, deriveChildKey } from './derivers/bip39';
Expand All @@ -19,6 +19,7 @@ describe('BIP44Node', () => {
it('initializes a new node from a private key', async () => {
const { privateKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

// Ethereum coin type node
Expand All @@ -45,6 +46,7 @@ describe('BIP44Node', () => {
it('initializes a new node from JSON', async () => {
const { privateKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

// Ethereum coin type node
Expand Down Expand Up @@ -84,6 +86,7 @@ describe('BIP44Node', () => {
it('throws if the depth is invalid', async () => {
const { privateKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

await expect(
Expand Down Expand Up @@ -405,6 +408,7 @@ describe('BIP44Node', () => {
async ({ index, publicKey }) => {
const { privateKey, chainCode } = await createBip39KeyFromSeed(
hexStringToBytes(hexSeed),
secp256k1,
);

const node = await BIP44Node.fromExtendedKey({
Expand Down Expand Up @@ -434,6 +438,7 @@ describe('BIP44Node', () => {
async ({ index, address }) => {
const { privateKey, chainCode } = await createBip39KeyFromSeed(
hexStringToBytes(hexSeed),
secp256k1,
);

const node = await BIP44Node.fromExtendedKey({
Expand Down
7 changes: 7 additions & 0 deletions src/SLIP10Node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('SLIP10Node', () => {
it('initializes a new node from a private key', async () => {
const { privateKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

const node = await SLIP10Node.fromExtendedKey({
Expand All @@ -35,6 +36,7 @@ describe('SLIP10Node', () => {
it('initializes a new node from a hexadecimal private key and chain code', async () => {
const { privateKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

const node = await SLIP10Node.fromExtendedKey({
Expand All @@ -54,6 +56,7 @@ describe('SLIP10Node', () => {
it('initializes a new ed25519 node from a private key', async () => {
const { privateKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: ed25519,
});

const node = await SLIP10Node.fromExtendedKey({
Expand Down Expand Up @@ -88,6 +91,7 @@ describe('SLIP10Node', () => {
it('initializes a new node from a public key', async () => {
const { publicKeyBytes, chainCodeBytes } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

const node = await SLIP10Node.fromExtendedKey({
Expand Down Expand Up @@ -127,6 +131,7 @@ describe('SLIP10Node', () => {
it('initializes a new node from a hexadecimal public key and chain code', async () => {
const { publicKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

const node = await SLIP10Node.fromExtendedKey({
Expand All @@ -146,6 +151,7 @@ describe('SLIP10Node', () => {
it('initializes a new node from JSON', async () => {
const node = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

expect(await SLIP10Node.fromJSON(node.toJSON())).toStrictEqual(node);
Expand All @@ -154,6 +160,7 @@ describe('SLIP10Node', () => {
it('initializes a new node from JSON with a public key', async () => {
const { privateKey, chainCode } = await deriveChildKey({
path: fixtures.local.mnemonic,
curve: secp256k1,
});

const node = await SLIP10Node.fromExtendedKey({
Expand Down
16 changes: 12 additions & 4 deletions src/derivation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { bytesToHex } from '@metamask/utils';

import fixtures from '../test/fixtures';
import { HDPathTuple } from './constants';
import { secp256k1 } from './curves';
import { deriveKeyFromPath, validatePathSegment } from './derivation';
import { derivers } from './derivers';
import { privateKeyToEthAddress } from './derivers/bip32';
Expand Down Expand Up @@ -223,31 +224,35 @@ describe('derivation', () => {
let node: SLIP10Node;

/* eslint-disable require-atomic-updates */
node = await bip39Derive({ path: mnemonic });
node = await bip39Derive({ path: mnemonic, curve: secp256k1 });
node = await bip32Derive({
path: `44'`,
node,
curve: secp256k1,
});

node = await bip32Derive({
path: `60'`,
node,
curve: secp256k1,
});

node = await bip32Derive({
path: `0'`,
node,
curve: secp256k1,
});

node = await bip32Derive({
path: `0`,
node,
curve: secp256k1,
});
/* eslint-enable require-atomic-updates */

const keys = await Promise.all(
expectedAddresses.map(async (_, index) => {
return bip32Derive({ path: `${index}`, node });
return bip32Derive({ path: `${index}`, node, curve: secp256k1 });
}),
);

Expand All @@ -258,7 +263,7 @@ describe('derivation', () => {
});

it('throws for invalid inputs', async () => {
const node = await bip39Derive({ path: mnemonic });
const node = await bip39Derive({ path: mnemonic, curve: secp256k1 });
const inputs = [
String(-1),
String(1.1),
Expand All @@ -274,6 +279,7 @@ describe('derivation', () => {
bip32Derive({
path: input,
node,
curve: secp256k1,
}),
).rejects.toThrow(
'Invalid BIP-32 index: The index must be a non-negative decimal integer less than 2147483648.',
Expand All @@ -285,20 +291,22 @@ describe('derivation', () => {
await expect(
bip32Derive({
path: '0',
curve: secp256k1,
}),
).rejects.toThrow(
'Invalid parameters: Must specify a node to derive from.',
);
});

it('throws when trying to derive from a public key node', async () => {
const node = await bip39Derive({ path: mnemonic });
const node = await bip39Derive({ path: mnemonic, curve: secp256k1 });
const publicNode = node.neuter();

await expect(
bip32Derive({
path: `0'`,
node: publicNode,
curve: secp256k1,
}),
).rejects.toThrow(
'Invalid parameters: Cannot derive hardened child keys without a private key.',
Expand Down
67 changes: 67 additions & 0 deletions src/derivers/bip39.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
assert,
bigIntToBytes,
concatBytes,
hexToBytes,
} from '@metamask/utils';
import * as hmacModule from '@noble/hashes/hmac';

import { secp256k1 } from '../curves';
import { createBip39KeyFromSeed } from './bip39';

describe('createBip39KeyFromSeed', () => {
const RANDOM_SEED = hexToBytes(
'0xea82e6ee9d319c083007d0b011a37b0e480ae02417a988ac90355abd53cd04fc',
);

it('throws if the seed is less than 16 bytes', async () => {
await expect(
createBip39KeyFromSeed(new Uint8Array(15), secp256k1),
).rejects.toThrow(
'Invalid seed: The seed must be between 16 and 64 bytes long.',
);
});

it('throws if the seed is greater than 64 bytes', async () => {
await expect(
createBip39KeyFromSeed(new Uint8Array(65), secp256k1),
).rejects.toThrow(
'Invalid seed: The seed must be between 16 and 64 bytes long.',
);
});

it('throws if the private key is zero', async () => {
// Mock the hmac function to return a zero private key.
jest.spyOn(hmacModule, 'hmac').mockImplementation(() => new Uint8Array(64));

await expect(
createBip39KeyFromSeed(RANDOM_SEED, secp256k1),
).rejects.toThrow(
'Invalid private key: The private key must greater than 0 and less than the curve order.',
);
});

it.each([
bigIntToBytes(secp256k1.curve.n),
concatBytes([secp256k1.curve.n + BigInt(1)]),
])(
'throws if the private key is greater than or equal to the curve order',
async (privateKey) => {
// For this test to be effective, the private key must be 32 bytes.
assert(privateKey.length === 32);

// Mock the hmac function to return a private key larger than the curve order.
jest
.spyOn(hmacModule, 'hmac')
.mockImplementation(() =>
concatBytes([privateKey, new Uint8Array(32)]),
);

await expect(
createBip39KeyFromSeed(RANDOM_SEED, secp256k1),
).rejects.toThrow(
'Invalid private key: The private key must greater than 0 and less than the curve order.',
);
},
);
});
21 changes: 16 additions & 5 deletions src/derivers/bip39.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { mnemonicToSeed } from '@metamask/scure-bip39';
import { wordlist as englishWordlist } from '@metamask/scure-bip39/dist/wordlists/english';
import { assert } from '@metamask/utils';
import { hmac } from '@noble/hashes/hmac';
import { sha512 } from '@noble/hashes/sha512';

import { DeriveChildKeyArgs } from '.';
import { BIP39StringNode } from '../constants';
import { Curve, secp256k1 } from '../curves';
import { BIP39StringNode, BYTES_KEY_LENGTH } from '../constants';
import { Curve } from '../curves';
import { SLIP10Node } from '../SLIP10Node';
import { getFingerprint } from '../utils';

Expand Down Expand Up @@ -47,11 +48,21 @@ export async function deriveChildKey({
*/
export async function createBip39KeyFromSeed(
seed: Uint8Array,
curve: Curve = secp256k1,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made this non-optional, because we were forgetting to pass ed25519 to this function in certain tests. It's better to be explicit here.

curve: Curve,
): Promise<SLIP10Node> {
assert(
seed.length >= 16 && seed.length <= 64,
'Invalid seed: The seed must be between 16 and 64 bytes long.',
);

const key = hmac(sha512, curve.secret, seed);
const privateKey = key.slice(0, 32);
const chainCode = key.slice(32);
const privateKey = key.slice(0, BYTES_KEY_LENGTH);
const chainCode = key.slice(BYTES_KEY_LENGTH);

assert(
curve.isValidPrivateKey(privateKey),
'Invalid private key: The private key must greater than 0 and less than the curve order.',
);

const masterFingerprint = getFingerprint(
await curve.getPublicKey(privateKey, true),
Expand Down
2 changes: 1 addition & 1 deletion src/derivers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type DerivedKeys = {

export type DeriveChildKeyArgs = {
path: Uint8Array | string;
curve?: Curve;
curve: Curve;
node?: SLIP10Node;
};

Expand Down
10 changes: 5 additions & 5 deletions test/reference-implementations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
BIP44PurposeNodeToken,
HDPathTuple,
} from '../src';
import { ed25519 } from '../src/curves';
import { ed25519, secp256k1 } from '../src/curves';
import { deriveKeyFromPath } from '../src/derivation';
import { createBip39KeyFromSeed } from '../src/derivers/bip39';
import {
Expand Down Expand Up @@ -145,7 +145,7 @@ describe('reference implementation tests', () => {

describe('BIP44Node', () => {
it('derives the same keys as the reference implementation', async () => {
const parentNode = await createBip39KeyFromSeed(seed);
const parentNode = await createBip39KeyFromSeed(seed, secp256k1);
const node = await parentNode.derive(path.ours.tuple);

expect(node.privateKey).toStrictEqual(privateKey);
Expand All @@ -161,7 +161,7 @@ describe('reference implementation tests', () => {
});

it('derives the same keys as the reference implementation using public key derivation', async () => {
const parentNode = await createBip39KeyFromSeed(seed);
const parentNode = await createBip39KeyFromSeed(seed, secp256k1);
const node = await parentNode.derive(path.ours.tuple);

expect(node.privateKey).toStrictEqual(privateKey);
Expand All @@ -180,7 +180,7 @@ describe('reference implementation tests', () => {

describe('deriveKeyFromPath', () => {
it('derives the same keys as the reference implementation', async () => {
const node = await createBip39KeyFromSeed(seed);
const node = await createBip39KeyFromSeed(seed, secp256k1);
const childNode = await deriveKeyFromPath({
path: path.ours.tuple,
node,
Expand Down Expand Up @@ -210,7 +210,7 @@ describe('reference implementation tests', () => {
it('derives the test vector keys', async () => {
for (const vector of vectors) {
const seed = hexStringToBytes(vector.hexSeed);
const node = await createBip39KeyFromSeed(seed);
const node = await createBip39KeyFromSeed(seed, secp256k1);

for (const keyObj of vector.keys) {
const { path, privateKey } = keyObj;
Expand Down