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

Add support for BIP-32-Ed25519 / CIP-3 key derivation #2408

Merged
merged 4 commits into from
May 15, 2024
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
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
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 () => {
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
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);

Choose a reason for hiding this comment

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

NP: line 217, should it be

| BIP32Node[]
| SLIP10PathNode[]
| CIP3PathNode[]

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
Loading