Skip to content

Commit

Permalink
Add support for BIP-32-Ed25519 / CIP-3 key derivation (#2408)
Browse files Browse the repository at this point in the history
This bumps `@metamask/key-tree` to `9.1.0` and applies some changes to
support the new BIP-32-Ed25519 derivation.

Replaces #2400.
  • Loading branch information
Mrtenz authored May 15, 2024
1 parent 5348c82 commit 00c2a50
Show file tree
Hide file tree
Showing 25 changed files with 161 additions and 63 deletions.
2 changes: 1 addition & 1 deletion packages/examples/packages/bip32/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"lint:dependencies": "depcheck"
},
"dependencies": {
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/snaps-sdk": "workspace:^",
"@metamask/utils": "^8.3.0",
"@noble/ed25519": "^1.6.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/bip32/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "OQuxxP2wxKhjw8UnV1gmxF7p0xBJPjqDpZBPAopnTng=",
"shasum": "TRXRkxE2XflQwoOz9K7tXVk0D80XKI8+NVrxVx0poY8=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/bip44/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"lint:dependencies": "depcheck"
},
"dependencies": {
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/snaps-sdk": "workspace:^",
"@metamask/utils": "^8.3.0",
"@noble/bls12-381": "^1.2.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/bip44/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "VzcO1O0ZoxXGv/mrjeI8uKFb3Rzo1h+cmx3+BdyQVLQ=",
"shasum": "L9tWp7lXixx9ehd2wAVqDmYejKSDDR4P0q7D0EORqtU=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"lint:dependencies": "depcheck"
},
"dependencies": {
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/snaps-sdk": "workspace:^",
"@metamask/utils": "^8.3.0",
"@noble/hashes": "^1.3.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"lint:dependencies": "depcheck"
},
"dependencies": {
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/snaps-sdk": "workspace:^",
"@metamask/utils": "^8.3.0",
"@noble/curves": "^1.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "GDWZSza7jjO8eLmEB/eZLcguLJivt7NZ14QM12HBqxA=",
"shasum": "w5YHaOqduw/AElLXKKw6rtilWmloVNWOuBUAThx0xn0=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@metamask/eth-json-rpc-middleware": "^12.1.0",
"@metamask/json-rpc-engine": "^8.0.1",
"@metamask/json-rpc-middleware-stream": "^7.0.1",
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/permission-controller": "^9.0.2",
"@metamask/snaps-controllers": "workspace:^",
"@metamask/snaps-execution-environments": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-rpc-methods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"build:ci": "tsup --clean"
},
"dependencies": {
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/permission-controller": "^9.0.2",
"@metamask/rpc-errors": "^6.2.1",
"@metamask/snaps-sdk": "workspace:^",
Expand Down
28 changes: 28 additions & 0 deletions packages/snaps-rpc-methods/src/restricted/getBip32Entropy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,33 @@ describe('getBip32EntropyImplementation', () => {
}
`);
});

it('derives a path using ed25519Bip32', async () => {
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
const getMnemonic = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);

expect(
// @ts-expect-error Missing other required properties.
await getBip32EntropyImplementation({ getUnlockPromise, getMnemonic })({
params: {
path: ['m', "44'", "1'", "0'", "0'", "1'"],
curve: 'ed25519Bip32',
},
}),
).toMatchInlineSnapshot(`
{
"chainCode": "0x8b46a12626641c1d3b888ea73a0474760bbf6530c189a987ad4be6403b2b7320",
"curve": "ed25519Bip32",
"depth": 5,
"index": 2147483649,
"masterFingerprint": 1587894111,
"parentFingerprint": 3236688876,
"privateKey": "0x88a59d7aa9fe82d8f98843ef474195178eb71956dee597252e7a5fbeebbc734e9b5bfdd17f82144a2bea78c8ab19bef26dc93f36e96eaa41453b65cb3daa1817",
"publicKey": "0xd91d18b4540a2f30341e8463d5f9b25b14fae9a236dcbea338b668a318bb0867",
}
`);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ describe('getBip32PublicKeyImplementation', () => {
);
});

it('derives the ed25519Bip32 public key from the path', async () => {
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
const getMnemonic = jest
.fn()
.mockResolvedValue(TEST_SECRET_RECOVERY_PHRASE_BYTES);

expect(
await getBip32PublicKeyImplementation({
getUnlockPromise,
getMnemonic,
// @ts-expect-error Missing other required properties.
})({
params: {
path: ['m', "44'", "1'", "0'", "0'", "1'"],
curve: 'ed25519Bip32',
},
}),
).toMatchInlineSnapshot(
`"0xd91d18b4540a2f30341e8463d5f9b25b14fae9a236dcbea338b668a318bb0867"`,
);
});

it('derives the compressed public key from the path', async () => {
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
const getMnemonic = jest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import type {
import {
bip32entropy,
Bip32PathStruct,
CurveStruct,
SnapCaveatType,
} from '@metamask/snaps-utils';
import type { NonEmptyArray } from '@metamask/utils';
import { assertStruct } from '@metamask/utils';
import { boolean, enums, object, optional } from 'superstruct';
import { boolean, object, optional } from 'superstruct';

import type { MethodHooksObject } from '../utils';
import { getNode } from '../utils';
Expand Down Expand Up @@ -53,7 +54,7 @@ type GetBip32PublicKeySpecification = ValidPermissionSpecification<{
export const Bip32PublicKeyArgsStruct = bip32entropy(
object({
path: Bip32PathStruct,
curve: enums(['ed25519', 'secp256k1']),
curve: CurveStruct,
compressed: optional(boolean()),
}),
);
Expand Down
11 changes: 11 additions & 0 deletions packages/snaps-rpc-methods/src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ describe('getPathPrefix', () => {
it('returns "slip10" for "ed25519"', () => {
expect(getPathPrefix('ed25519')).toBe('slip10');
});

it('returns "cip3" for "ed25519Bip32"', () => {
expect(getPathPrefix('ed25519Bip32')).toBe('cip3');
});

it('throws for an unknown curve', () => {
// @ts-expect-error Invalid curve.
expect(() => getPathPrefix('foo')).toThrow(
'Invalid branch reached. Should be detected during compilation.',
);
});
});

describe('getNode', () => {
Expand Down
29 changes: 19 additions & 10 deletions packages/snaps-rpc-methods/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type {
HardenedBIP32Node,
BIP32Node,
SLIP10PathNode,
SupportedCurve,
} from '@metamask/key-tree';
import { SLIP10Node } from '@metamask/key-tree';
import type { MagicValue } from '@metamask/snaps-utils';
import type { Hex } from '@metamask/utils';
import {
assertExhaustive,
add0x,
assert,
concatBytes,
Expand Down Expand Up @@ -154,28 +156,34 @@ export async function deriveEntropy({
* Get the path prefix to use for key derivation in `key-tree`. This assumes the
* following:
*
* - The Secp256k1 curve always use the BIP-32 specification.
* - The Ed25519 curve always use the SLIP-10 specification.
* - The Secp256k1 curve always uses the BIP-32 specification.
* - The Ed25519 curve always uses the SLIP-10 specification.
* - The BIP-32-Ed25519 curve always uses the CIP-3 specification.
*
* While this does not matter in most situations (no known case at the time of
* writing), `key-tree` requires a specific specification to be used.
*
* @param curve - The curve to get the path prefix for. The curve is NOT
* validated by this function.
* @returns The path prefix, i.e., `secp256k1` or `ed25519`.
* @returns The path prefix, i.e., `bip32` or `slip10`.
*/
export function getPathPrefix(
curve: 'secp256k1' | 'ed25519',
): 'bip32' | 'slip10' {
if (curve === 'secp256k1') {
return 'bip32';
curve: SupportedCurve,
): 'bip32' | 'slip10' | 'cip3' {
switch (curve) {
case 'secp256k1':
return 'bip32';
case 'ed25519':
return 'slip10';
case 'ed25519Bip32':
return 'cip3';
default:
return assertExhaustive(curve);
}

return 'slip10';
}

type GetNodeArgs = {
curve: 'secp256k1' | 'ed25519';
curve: SupportedCurve;
secretRecoveryPhrase: Uint8Array;
path: string[];
};
Expand All @@ -199,6 +207,7 @@ export async function getNode({
path,
}: GetNodeArgs) {
const prefix = getPathPrefix(curve);

return await SLIP10Node.fromDerivationPath({
curve,
derivationPath: [
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"build:ci": "tsup --clean"
},
"dependencies": {
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/providers": "^16.1.0",
"@metamask/rpc-errors": "^6.2.1",
"@metamask/utils": "^8.3.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/snaps-sdk/src/types/permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ describe('Bip32Entropy', () => {

expectTypeOf(entropy).toMatchTypeOf<Bip32Entropy>();
});

it('supports ed25519Bip32', () => {
const entropy = {
curve: 'ed25519Bip32' as const,
path: ['m', "44'"],
};

expectTypeOf(entropy).toMatchTypeOf<Bip32Entropy>();
});
});

describe('Bip44Entropy', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/snaps-sdk/src/types/permissions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SupportedCurve } from '@metamask/key-tree';
import type { JsonRpcRequest } from '@metamask/utils';

import type { ChainId } from './caip';
Expand All @@ -22,7 +23,7 @@ export type NameLookupMatchers =
};

export type Bip32Entropy = {
curve: 'secp256k1' | 'ed25519';
curve: SupportedCurve;
path: string[];
};

Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-simulator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@metamask/eth-json-rpc-middleware": "^12.1.0",
"@metamask/json-rpc-engine": "^8.0.1",
"@metamask/json-rpc-middleware-stream": "^7.0.1",
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/permission-controller": "^9.0.2",
"@metamask/snaps-controllers": "workspace:^",
"@metamask/snaps-execution-environments": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-utils/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"branches": 96.7,
"functions": 98.72,
"lines": 98.81,
"statements": 94.78
"statements": 94.79
}
2 changes: 1 addition & 1 deletion packages/snaps-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@babel/core": "^7.23.2",
"@babel/types": "^7.23.0",
"@metamask/base-controller": "^5.0.2",
"@metamask/key-tree": "^9.0.0",
"@metamask/key-tree": "^9.1.0",
"@metamask/permission-controller": "^9.0.2",
"@metamask/rpc-errors": "^6.2.1",
"@metamask/slip44": "^3.1.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/snaps-utils/src/derivation-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ describe('getSnapDerivationPathName', () => {
);
});

it("returns a name from the hardcoded list starting with `1852'`, `1815'`", () => {
expect(
getSnapDerivationPathName(['m', `1852'`, `1815'`], 'ed25519Bip32'),
).toBe('Cardano');
});

it('returns a name from the SLIP44 list where applicable', () => {
expect(
getSnapDerivationPathName(['m', `44'`, `60'`, `0'`], 'secp256k1'),
Expand Down
5 changes: 5 additions & 0 deletions packages/snaps-utils/src/derivation-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ export const SNAPS_DERIVATION_PATHS: SnapsDerivationPath[] = [
curve: 'ed25519',
name: 'Vega',
},
{
path: ['m', `1852'`, `1815'`],
curve: 'ed25519Bip32',
name: 'Cardano',
},
];

/**
Expand Down
14 changes: 14 additions & 0 deletions packages/snaps-utils/src/manifest/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Bip32EntropyStruct,
Bip32PathStruct,
createSnapManifest,
CurveStruct,
EmptyObjectStruct,
isSnapManifest,
SnapIdsStruct,
Expand Down Expand Up @@ -82,6 +83,19 @@ describe('Bip32PathStruct', () => {
);
});

describe('CurveStruct', () => {
it.each(['secp256k1', 'ed25519', 'ed25519Bip32'])('validates %p', (curve) => {
expect(is(curve, CurveStruct)).toBe(true);
});

it.each([1, '', 'asd', {}, null, undefined])(
'does not validate %p',
(curve) => {
expect(is(curve, CurveStruct)).toBe(false);
},
);
});

describe('Bip32EntropyStruct', () => {
it('works with ed25519', () => {
expect(
Expand Down
9 changes: 8 additions & 1 deletion packages/snaps-utils/src/manifest/validation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SupportedCurve } from '@metamask/key-tree';
import { isValidBIP32PathSegment } from '@metamask/key-tree';
import type { EmptyObject, InitialPermissions } from '@metamask/snaps-sdk';
import {
Expand Down Expand Up @@ -107,11 +108,17 @@ export const bip32entropy = <
return true;
});

export const CurveStruct: Describe<SupportedCurve> = enums([
'ed25519',
'secp256k1',
'ed25519Bip32',
]);

// Used outside @metamask/snap-utils
export const Bip32EntropyStruct = bip32entropy(
type({
path: Bip32PathStruct,
curve: enums(['ed25519', 'secp256k1']),
curve: CurveStruct,
}),
);

Expand Down
Loading

0 comments on commit 00c2a50

Please sign in to comment.