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 script for signing, verifying, and deploying registry #18

Merged
merged 11 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PRIVATE_KEY_PATH=./secp256k1-key
PUBLIC_KEY_PATH=./secp256k1-key.pub
REGISTRY_PATH=./src/registry.json
SIGNATURE_PATH=./src/signature.json
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ module.exports = {
},

{
files: ['*.js'],
files: ['*.js', 'scripts/*.ts'],
parserOptions: {
sourceType: 'script',
},
extends: ['@metamask/eslint-config-nodejs'],
rules: {
'node/no-process-env': 'off',
},
},

{
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ jobs:
exit 1
fi

publish-registry:
# Filtering by `push` events ensures that we only release from the `main` branch.
if: github.event_name == 'push'
needs: all-jobs-pass
uses: ./.github/workflows/publish-registry.yml
secrets:
REGISTRY_PRIVATE_KEY: ${{ secrets.REGISTRY_PRIVATE_KEY }}
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
METAMASKBOT_TOKEN: ${{ secrets.METAMASKBOT_TOKEN }}

is-release:
# Filtering by `push` events ensures that we only release from the `main` branch, which is a
# requirement for our npm publishing environment.
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/publish-registry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Publish Registry

on:
workflow_call:
secrets:
REGISTRY_PRIVATE_KEY:
required: true
METAMASKBOT_TOKEN:
required: true

jobs:
check-updated:
name: Check if registry file was updated
runs-on: ubuntu-latest
outputs:
UPDATED: ${{ steps.updated.outputs.UPDATED }}
steps:
- uses: actions/checkout@v3
- name: Check if registry file was updated
id: updated
run: |
git fetch --prune --unshallow
if git diff --name-only HEAD^ HEAD | grep src/registry.json
then
echo "UPDATED=true" >> "$GITHUB_OUTPUT"
else
echo "UPDATED=false" >> "$GITHUB_OUTPUT"
fi

publish-registry:
name: Deploy registry to `gh-pages` branch
environment: npm-publish
needs: check-updated
if: ${{ needs.check-updated.outputs.UPDATED == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install Yarn dependencies
run: yarn --immutable
- name: Sign registry
run: yarn sign
env:
PRIVATE_KEY: ${{ secrets.REGISTRY_PRIVATE_KEY }}
- run: |
mkdir -p dist
cp src/registry.json dist/registry.json
cp src/signature.json dist/signature.json
- name: Deploy registry
uses: peaceiris/actions-gh-pages@de7ea6f8efb354206b205ef54722213d99067935
with:
personal_token: ${{ secrets.METAMASKBOT_TOKEN }}
publish_dir: ./dist
destination_dir: latest
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,8 @@ node_modules/
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Signature file
src/signature.json

secp256k1-key
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
branches: 0,
functions: 0,
lines: 81.25,
statements: 81.25,
},
},

Expand Down
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,26 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/"
"dist/",
"secp256k1-key.pub"
],
"scripts": {
"build": "tsc --project tsconfig.build.json",
"build:clean": "rimraf dist && yarn build",
"create-key": "ts-node scripts/create-key.ts",
"lint": "yarn lint:eslint && yarn lint:misc --check",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern",
"prepack": "./scripts/prepack.sh",
"sign": "ts-node scripts/sign-registry.ts",
"test": "jest && jest-it-up",
"test:watch": "jest --watch"
"test:watch": "jest --watch",
"verify": "ts-node scripts/verify-registry.ts"
},
"dependencies": {
"@metamask/utils": "^5.0.0",
"@noble/secp256k1": "^1.7.1",
"superstruct": "^1.0.3"
},
"devDependencies": {
Expand All @@ -33,10 +38,13 @@
"@metamask/eslint-config-jest": "^11.1.0",
"@metamask/eslint-config-nodejs": "^11.1.0",
"@metamask/eslint-config-typescript": "^11.1.0",
"@noble/curves": "^0.9.0",
"@noble/hashes": "^1.3.0",
"@types/jest": "^28.1.6",
"@types/node": "^17.0.23",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"dotenv": "^16.0.3",
"eslint": "^8.27.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
Expand Down
65 changes: 65 additions & 0 deletions scripts/create-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { bytesToHex, hasProperty } from '@metamask/utils';
import * as secp256k1 from '@noble/secp256k1';
import assert from 'assert';
import * as dotenv from 'dotenv';
import fs from 'fs/promises';
import path from 'path';

dotenv.config();

/**
* Create a private key to be used for signing of the registry.json.
*/
async function main() {
const force = process.argv.includes('-f') || process.argv.includes('--force');
const privateKeyPath = process.env.PRIVATE_KEY_PATH;
const publicKeyPath = process.env.PUBLIC_KEY_PATH;

assert(privateKeyPath !== undefined, 'PRIVATE_KEY_PATH must be set.');
assert(publicKeyPath !== undefined, 'PUBLIC_KEY_PATH must be set.');

const privateKeyBytes = secp256k1.utils.randomPrivateKey();
const publicKey = bytesToHex(secp256k1.getPublicKey(privateKeyBytes, true));

console.log(`Key "${publicKey}" created.`);

const privateKeyHex = bytesToHex(privateKeyBytes);

try {
// Write to file only if it doesn't exist.
const flag = force ? 'w' : 'wx';

await Promise.all([
fs.writeFile(privateKeyPath, `${privateKeyHex}\n`, {
flag,
}),
fs.writeFile(publicKeyPath, `${publicKey}\n`, {
flag,
}),
]);
} catch (error) {
if (
typeof error === 'object' &&
error !== null &&
hasProperty(error, 'code') &&
error.code === 'EEXIST'
) {
console.error(
`File ${
hasProperty(error, 'path') ? String(error.path) : '(unknown path)'
} already exists. Use --force to overwrite.`,
);

// eslint-disable-next-line node/no-process-exit
process.exit(1);
}
throw error;
}

console.log(`Successfully wrote to files.
-> Private key: ${path.resolve(privateKeyPath)}
-> Public key: ${path.resolve(publicKeyPath)}`);
}
main().catch((error) => {
throw error;
});
90 changes: 90 additions & 0 deletions scripts/sign-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
add0x,
assert,
bytesToHex,
hexToBytes,
isHexString,
} from '@metamask/utils';
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import * as dotenv from 'dotenv';
import { promises as fs } from 'fs';
import path from 'path';
import { format } from 'prettier';

dotenv.config();

/**
* Get the private key from an environment variable. Either PRIVATE_KEY_PATH or
* REGISTRY_PRIVATE_KEY must be set.
*
* @returns The private key.
* @throws If neither environment variable is set, or if the private key cannot
* be read from the file.
*/
async function getPrivateKey() {
const privateKeyPath = process.env.PRIVATE_KEY_PATH;
const privateKeyEnv = process.env.REGISTRY_PRIVATE_KEY;

if (privateKeyPath) {
console.log('Using key from PRIVATE_KEY_PATH file.');
return await fs.readFile(privateKeyPath, 'utf-8').then((key) => key.trim());
}

if (privateKeyEnv) {
console.log('Using key from REGISTRY_PRIVATE_KEY variable.');
return privateKeyEnv;
}

throw new Error(
'Either PRIVATE_KEY_PATH or REGISTRY_PRIVATE_KEY environment variable must be set.',
);
}

/**
* Signs the registry with the given private key.
*/
async function main() {
const registryPath = process.env.REGISTRY_PATH;
const signaturePath = process.env.SIGNATURE_PATH;

assert(
registryPath !== undefined,
'REGISTRY_PATH environment variable must be set.',
);
assert(
signaturePath !== undefined,
'SIGNATURE_PATH environment variable must be set.',
);

const privateKey = await getPrivateKey();

assert(isHexString(privateKey), 'Private key must be a hex string.');
const privateKeyBytes = hexToBytes(privateKey);
assert(privateKeyBytes.length === 32, 'Private key must be 32 bytes');
const publicKey = bytesToHex(secp256k1.getPublicKey(privateKeyBytes));

const registry = await fs.readFile(registryPath);

const signature = add0x(
secp256k1.sign(sha256(registry), privateKeyBytes).toDERHex(),
);

const signatureObject = format(
JSON.stringify({
signature,
curve: 'secp256k1',
format: 'DER',
}),
{ filepath: path.resolve(process.cwd(), signaturePath) },
);

await fs.writeFile(signaturePath, signatureObject, 'utf-8');
console.log(
`Signature signed using "${publicKey}" and written to "${signaturePath}".`,
);
}

main().catch((error) => {
throw error;
});
70 changes: 70 additions & 0 deletions scripts/verify-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { assert, assertStruct, isHexString } from '@metamask/utils';
import * as dotenv from 'dotenv';
import { promises as fs } from 'fs';

import { SignatureStruct, verify } from '../src';

dotenv.config();

/**
* Get the private key from an environment variable. Either PRIVATE_KEY_PATH or
* REGISTRY_PRIVATE_KEY must be set.
*
* @returns The private key.
* @throws If neither environment variable is set, or if the private key cannot
* be read from the file.
*/
async function getPublicKey() {
const publicKeyPath = process.env.PUBLIC_KEY_PATH;
const registryPublicKey = process.env.REGISTRY_PUBLIC_KEY;

if (publicKeyPath) {
console.log('Using key from PUBLIC_KEY_PATH file.');
return await fs.readFile(publicKeyPath, 'utf-8').then((key) => key.trim());
}

if (registryPublicKey) {
console.log('Using key from REGISTRY_PUBLIC_KEY variable.');
return registryPublicKey;
}

throw new Error(
'Either PRIVATE_KEY_PATH or REGISTRY_PRIVATE_KEY environment variable must be set.',
);
}

/**
* Verify the signature of the registry.
*/
async function main() {
const registryPath = process.env.REGISTRY_PATH;
const signaturePath = process.env.SIGNATURE_PATH;

assert(
registryPath !== undefined,
'REGISTRY_PATH environment variable must be set.',
);
assert(
signaturePath !== undefined,
'SIGNATURE_PATH environment variable must be set.',
);

const publicKey = await getPublicKey();
assert(isHexString(publicKey), 'Public key must be a hex string.');

const signature = JSON.parse(await fs.readFile(signaturePath, 'utf-8'));
assertStruct(signature, SignatureStruct);
const registry = await fs.readFile(registryPath, 'utf-8');

const isValid = await verify({ registry, signature, publicKey });
if (!isValid) {
console.error('Signature is invalid.');
// eslint-disable-next-line node/no-process-exit
process.exit(1);
}
console.log('Signature is valid.');
}

main().catch((error) => {
throw error;
});
1 change: 1 addition & 0 deletions secp256k1-key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ export const SnapsRegistryDatabaseStruct = object({
});

export type SnapsRegistryDatabase = Infer<typeof SnapsRegistryDatabaseStruct>;

export * from './verify';
Loading