Skip to content

Commit

Permalink
Add snap_getEntropy JSON-RPC method (#940)
Browse files Browse the repository at this point in the history
* Add snap_getEntropy JSON-RPC method

* Disallow deriving SIP-6 purpose

* Update bls-signer example

* Fix review comments

* Fix bls-signer manifest
  • Loading branch information
Mrtenz committed Nov 14, 2022
1 parent af98e50 commit 54ad32c
Show file tree
Hide file tree
Showing 13 changed files with 379 additions and 9 deletions.
2 changes: 1 addition & 1 deletion packages/examples/examples/bls-signer/snap.manifest.json
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps-monorepo.git"
},
"source": {
"shasum": "qNYd+o17O8kEwz40rxgBftRiyss2sYS7ir++rlLBQXo=",
"shasum": "V9PaaBMIyXuDzYQfUWBZjNStPgSv1MBp6g9En7iV6x4=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
11 changes: 8 additions & 3 deletions packages/examples/examples/bls-signer/src/index.js
Expand Up @@ -27,14 +27,19 @@ module.exports.onRpcRequest = async ({ request }) => {
'BLS signature request',
`Do you want to BLS sign ${data} with ${pubKey}?`,
);

if (!approved) {
throw rpcErrors.eth.unauthorized();
}

const PRIVATE_KEY = await wallet.request({
method: 'snap_getAppKey',
method: 'snap_getEntropy',
params: {
version: 1,
},
});
const signature = await bls.sign(request.params[0], PRIVATE_KEY, DOMAIN);
return signature;

return await bls.sign(request.params[0], PRIVATE_KEY, DOMAIN);
}

default:
Expand Down
8 changes: 4 additions & 4 deletions packages/rpc-methods/jest.config.js
Expand Up @@ -5,10 +5,10 @@ module.exports = deepmerge(baseConfig, {
coveragePathIgnorePatterns: ['./src/index.ts'],
coverageThreshold: {
global: {
branches: 87.01,
functions: 87.03,
lines: 78.96,
statements: 78.96,
branches: 87.5,
functions: 88.13,
lines: 80.89,
statements: 80.89,
},
},
testTimeout: 2500,
Expand Down
1 change: 1 addition & 0 deletions packages/rpc-methods/package.json
Expand Up @@ -31,6 +31,7 @@
"@metamask/snaps-utils": "^0.23.0",
"@metamask/types": "^1.1.0",
"@metamask/utils": "^3.3.1",
"@noble/hashes": "^1.1.3",
"eth-rpc-errors": "^4.0.2",
"nanoid": "^3.1.31",
"superstruct": "^0.16.7"
Expand Down
35 changes: 35 additions & 0 deletions packages/rpc-methods/src/restricted/__fixtures__/entropy.ts
@@ -0,0 +1,35 @@
/**
* SIP-6 test vectors.
*/
export const ENTROPY_VECTORS = [
{
snapId: 'foo',
derivationPath:
"m/1399742832'/1323571613'/1848851859'/458888073'/1339050117'/513522582'/1371866341'/2121938770'/1014285256'",
entropy:
'0x8bbb59ec55a4a8dd5429268e367ebbbe54eee7467c0090ca835c64d45c33a155',
},
{
snapId: 'bar',
derivationPath:
"m/1399742832'/767024459'/1206550137'/1427647479'/1048031962'/1656784813'/1860822351'/1362389435'/2133253878'",
entropy:
'0xbdae5c0790d9189d8ae27fd4860b3b57bab420b6594c420ae9ae3a9f87c1ea14',
},
{
snapId: 'foo',
salt: 'bar',
derivationPath:
"m/1399742832'/2002032866'/301374032'/1159533269'/453247377'/187127851'/1859522268'/152471137'/187531423'",
entropy:
'0x59cbec1fa877ecb38d88c3a2326b23bff374954b39ad9482c9b082306ac4b3ad',
},
{
snapId: 'bar',
salt: 'baz',
derivationPath:
"m/1399742832'/734358031'/701613791'/1618075622'/1535938847'/1610213550'/18831365'/356906080'/2095933563'",
entropy:
'0x814c1f121eb4067d1e1d177246461e8a1cc6a1b1152756737aba7fa9c2161ba2',
},
];
1 change: 1 addition & 0 deletions packages/rpc-methods/src/restricted/__fixtures__/index.ts
@@ -0,0 +1 @@
export * from './entropy';
31 changes: 30 additions & 1 deletion packages/rpc-methods/src/restricted/getBip32Entropy.test.ts
@@ -1,4 +1,4 @@
import { SnapCaveatType } from '@metamask/snaps-utils';
import { SIP_6_MAGIC_VALUE, SnapCaveatType } from '@metamask/snaps-utils';
import {
getBip32EntropyBuilder,
getBip32EntropyCaveatMapper,
Expand Down Expand Up @@ -196,6 +196,22 @@ describe('getBip32EntropyCaveatSpecifications', () => {
'The requested path is not permitted. Allowed paths must be specified in the snap manifest.',
);
});

it('throws if the purpose is not allowed', async () => {
const fn = jest.fn().mockImplementation(() => 'foo');

await expect(
getBip32EntropyCaveatSpecifications[
SnapCaveatType.PermittedDerivationPaths
].decorator(fn, {
type: SnapCaveatType.PermittedDerivationPaths,
value: [params],
// @ts-expect-error Missing other required properties.
})({ params: { ...params, path: ['m', SIP_6_MAGIC_VALUE, "0'"] } }),
).rejects.toThrow(
'Invalid BIP-32 entropy path definition: At path: path -- The purpose "1399742832\'" is not allowed for entropy derivation.',
);
});
});

describe('validator', () => {
Expand All @@ -209,6 +225,19 @@ describe('getBip32EntropyCaveatSpecifications', () => {
}),
).toThrow('At path: value.0.path -- Path must start with "m".');
});

it('throws if the caveat values contain forbidden paths', () => {
expect(() =>
getBip32EntropyCaveatSpecifications[
SnapCaveatType.PermittedDerivationPaths
].validator?.({
type: SnapCaveatType.PermittedDerivationPaths,
value: [{ path: ['m', SIP_6_MAGIC_VALUE, "0'"], curve: 'secp256k1' }],
}),
).toThrow(
'Invalid BIP-32 entropy caveat: At path: value.0.path -- The purpose "1399742832\'" is not allowed for entropy derivation.',
);
});
});
});

Expand Down
83 changes: 83 additions & 0 deletions packages/rpc-methods/src/restricted/getEntropy.test.ts
@@ -0,0 +1,83 @@
import { PermissionType } from '@metamask/controllers';
import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils';
import { deriveEntropy, getEntropyBuilder } from './getEntropy';
import { ENTROPY_VECTORS } from './__fixtures__';

const TEST_SECRET_RECOVERY_PHRASE =
'test test test test test test test test test test test ball';

describe('getEntropyBuilder', () => {
it('has the expected shape', () => {
expect(getEntropyBuilder).toStrictEqual({
targetKey: 'snap_getEntropy',
specificationBuilder: expect.any(Function),
methodHooks: {
getMnemonic: true,
getUnlockPromise: true,
},
});
});

it('returns the expected specification', () => {
const methodHooks = {
getMnemonic: jest.fn(),
getUnlockPromise: jest.fn(),
};

expect(
getEntropyBuilder.specificationBuilder({ methodHooks }),
).toStrictEqual({
permissionType: PermissionType.RestrictedMethod,
targetKey: 'snap_getEntropy',
allowedCaveats: null,
methodImplementation: expect.any(Function),
});
});
});

describe('deriveEntropy', () => {
it.each(ENTROPY_VECTORS)(
'derives entropy from the given parameters',
async () => {
const { snapId, salt, entropy } = ENTROPY_VECTORS[0];

expect(
await deriveEntropy(snapId, TEST_SECRET_RECOVERY_PHRASE, salt ?? ''),
).toStrictEqual(entropy);
},
);
});

describe('getEntropyImplementation', () => {
it('returns the expected result', async () => {
const getMnemonic = jest
.fn()
.mockImplementation(() => TEST_SECRET_RECOVERY_PHRASE);

const getUnlockPromise = jest.fn();

const methodHooks = {
getMnemonic,
getUnlockPromise,
};

const implementation = getEntropyBuilder.specificationBuilder({
methodHooks,
}).methodImplementation;

const result = await implementation({
method: 'snap_getEntropy',
params: {
version: 1,
salt: 'foo',
},
context: {
origin: MOCK_SNAP_ID,
},
});

expect(result).toStrictEqual(
'0x6d8e92de419401c7da3cedd5f60ce5635b26059c2a4a8003877fec83653a4921',
);
});
});

0 comments on commit 54ad32c

Please sign in to comment.