Skip to content

Commit

Permalink
Add script for signing, verifying, and deploying registry (#18)
Browse files Browse the repository at this point in the history
* Add script for signing and verifying registry

* Remove unused import

* Run lint:fix

* Deploy registry and signature to gh-pages

* Don't use exit code for checking if file was updated

* Run workflow as part of main workflow

* Added public key creation and refactored scripts

* Changes after previous commit

* Add tests

* Fix lint

* Fix environment name

---------

Co-authored-by: Olaf Tomalka <[email protected]>
  • Loading branch information
Mrtenz and ritave authored Mar 24, 2023
1 parent df9d359 commit a6ff2a5
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 4 deletions.
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 }}
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: registry-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
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

0 comments on commit a6ff2a5

Please sign in to comment.