From fc7f6c1b73c55b59260a449f1bb1ac269b3abe3e Mon Sep 17 00:00:00 2001 From: Sonja Date: Wed, 2 Aug 2023 13:10:47 +0000 Subject: [PATCH] wbn-sign: Add support for calculating the Web Bundle ID with CLI tool This adds the support to automatically calculate the Web Bundle ID also when using the package's Node CLI tool without Webpack / Rollup plugins. In the case of a bash script, the Web Bundle ID can then be saved into e.g. an environment variable or a file as instructed in the readme. --- js/sign/README.md | 60 ++++++++++++- js/sign/bin/wbn-dump-id.js | 4 + js/sign/bin/wbn-sign.js | 2 +- js/sign/package-lock.json | 1 + js/sign/package.json | 3 +- js/sign/src/cli-dump-id.ts | 38 ++++++++ js/sign/src/cli-sign.ts | 48 ++++++++++ js/sign/src/cli.ts | 87 ------------------- js/sign/src/utils/cli-utils.ts | 55 ++++++++++++ js/sign/src/utils/utils.ts | 3 + .../tests/{cli_test.js => cli-utils_test.js} | 20 +++-- 11 files changed, 224 insertions(+), 97 deletions(-) create mode 100755 js/sign/bin/wbn-dump-id.js create mode 100644 js/sign/src/cli-dump-id.ts create mode 100644 js/sign/src/cli-sign.ts delete mode 100644 js/sign/src/cli.ts create mode 100644 js/sign/src/utils/cli-utils.ts rename js/sign/tests/{cli_test.js => cli-utils_test.js} (81%) diff --git a/js/sign/README.md b/js/sign/README.md index cf02e05b..89ff4054 100644 --- a/js/sign/README.md +++ b/js/sign/README.md @@ -92,8 +92,24 @@ const webBundleIdWithIWAOrigin = new wbnSign.WebBundleId( ## CLI -This package also includes a CLI tool `wbn-sign` which lets you sign a web -bundle easily without having to write any additional JavaScript. +This package also includes 2 CLI tools + +- `wbn-sign` which lets you sign a web bundle easily without having to write any + additional JavaScript. +- `wbn-dump-id` which can be used to calculate the Web Bundle ID corresponding + to your signing key. + +### Running wbn-sign + +There are the following command-line flags available: + +- (required) `--privateKey ` (`-k `) + which takes the path to ed25519 private key. +- (required) `--input ` (`-i `) + which takes the path to the web bundle to be signed. +- (optional) `--output ` (`-o `) + which takes the path to the wanted signed web bundle output. Default: + `signed.swbn`. Example command: @@ -104,6 +120,42 @@ wbn-sign \ -k ~/path/to/ed25519key.pem ``` +### Running wbn-dump-id + +There are the following command-line flags available: + +- (required) `--privateKey ` (`-k `) + which takes the path to ed25519 private key. +- (optional) `--withIwaScheme ` (`-s`) + which dumps the Web Bundle ID with isolated-app:// scheme. By default it only + dumps the ID. Default: `false`. + +Example command: + +```bash +wbn-sign -s -k ~/path/to/ed25519key.pem +``` + +This would print the Web Bundle ID calculated from `ed25519key.pem` into the +console with the `isolated-app://` scheme. + +If one wants to save the ID into a file or into an environment variable, one can +do the following (respectively): + +```bash +wbn-dump-id -k file_enc.pem -s > webbundleid.txt +``` + +```bash +export DUMP_WEB_BUNDLE_ID="$(wbn-dump-id -k file_enc.pem -s)" +``` + +The environment variable set like this, can then be used in other scripts, for +example in `--baseURL` when creating a web bundle with +[wbn CLI tool](https://github.com/WICG/webpackage/tree/main/js/bundle#cli). + +## Generating Ed25519 key + An unencrypted ed25519 private key can be generated with: ``` @@ -127,6 +179,10 @@ environment variable named `WEB_BUNDLE_SIGNING_PASSPHRASE`. ## Release Notes +### v0.1.2 + +- Add support for calculating the Web Bundle ID with the CLI tool. + ### v0.1.1 - Add support for bypassing the passphrase prompt for encrypted private keys by diff --git a/js/sign/bin/wbn-dump-id.js b/js/sign/bin/wbn-dump-id.js new file mode 100755 index 00000000..5f56d98b --- /dev/null +++ b/js/sign/bin/wbn-dump-id.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { main } from '../lib/cli-dump-id.js'; + +main(); diff --git a/js/sign/bin/wbn-sign.js b/js/sign/bin/wbn-sign.js index 0b4af79c..9a69317f 100755 --- a/js/sign/bin/wbn-sign.js +++ b/js/sign/bin/wbn-sign.js @@ -1,4 +1,4 @@ #!/usr/bin/env node -import { main } from '../lib/cli.js'; +import { main } from '../lib/cli-sign.js'; main(); diff --git a/js/sign/package-lock.json b/js/sign/package-lock.json index 520da219..427d557a 100644 --- a/js/sign/package-lock.json +++ b/js/sign/package-lock.json @@ -15,6 +15,7 @@ "read": "^2.0.0" }, "bin": { + "wbn-dump-id": "bin/wbn-dump-id.js", "wbn-sign": "bin/wbn-sign.js" }, "devDependencies": { diff --git a/js/sign/package.json b/js/sign/package.json index eb11b878..8abdf3ec 100644 --- a/js/sign/package.json +++ b/js/sign/package.json @@ -18,7 +18,8 @@ "lint": "npx prettier --write . --ignore-unknown --config ./package.json" }, "bin": { - "wbn-sign": "./bin/wbn-sign.js" + "wbn-sign": "./bin/wbn-sign.js", + "wbn-dump-id": "./bin/wbn-dump-id.js" }, "repository": { "type": "git", diff --git a/js/sign/src/cli-dump-id.ts b/js/sign/src/cli-dump-id.ts new file mode 100644 index 00000000..a40ac5e4 --- /dev/null +++ b/js/sign/src/cli-dump-id.ts @@ -0,0 +1,38 @@ +import commander from 'commander'; +import { WebBundleId } from './wbn-sign.js'; +import * as fs from 'fs'; +import { greenConsoleLog, parseMaybeEncryptedKey } from './utils/cli-utils.js'; +import { KeyObject } from 'crypto'; + +const program = new commander.Command() + .name('wbn-dump-id') + .description( + 'A simple CLI tool to dump the Web Bundle ID matching to the given private key.' + ); + +function readOptions() { + return program + .requiredOption( + '-k, --privateKey ', + 'Reads an ed25519 private key from the given path. (required)' + ) + .option( + '-s, --withIwaScheme', + 'Dumps the Web Bundle ID with isolated-app:// scheme. By default it only dumps the ID. (optional)', + /*defaultValue=*/ false + ) + .parse(process.argv); +} + +export async function main() { + const options = readOptions(); + const parsedPrivateKey: KeyObject = await parseMaybeEncryptedKey( + fs.readFileSync(options.privateKey) + ); + + const webBundleId: string = options.withIwaScheme + ? new WebBundleId(parsedPrivateKey).serializeWithIsolatedWebAppOrigin() + : new WebBundleId(parsedPrivateKey).serialize(); + + greenConsoleLog(webBundleId); +} diff --git a/js/sign/src/cli-sign.ts b/js/sign/src/cli-sign.ts new file mode 100644 index 00000000..0bd0855e --- /dev/null +++ b/js/sign/src/cli-sign.ts @@ -0,0 +1,48 @@ +import commander from 'commander'; +import { + NodeCryptoSigningStrategy, + IntegrityBlockSigner, + WebBundleId, +} from './wbn-sign.js'; +import * as fs from 'fs'; +import { greenConsoleLog, parseMaybeEncryptedKey } from './utils/cli-utils.js'; +import { KeyObject } from 'crypto'; + +const program = new commander.Command() + .name('wbn-sign') + .description( + 'A simple CLI tool to sign the given web bundle with the given private key.' + ); + +function readOptions() { + return program + .requiredOption( + '-i, --input ', + 'input web bundle to be signed (required)' + ) + .requiredOption( + '-k, --privateKey ', + 'path to ed25519 private key (required)' + ) + .option( + '-o, --output ', + 'signed web bundle output file', + /*defaultValue=*/ 'signed.swbn' + ) + .parse(process.argv); +} + +export async function main() { + const options = readOptions(); + const webBundle = fs.readFileSync(options.input); + const parsedPrivateKey: KeyObject = await parseMaybeEncryptedKey( + fs.readFileSync(options.privateKey) + ); + const signer = new IntegrityBlockSigner( + webBundle, + new NodeCryptoSigningStrategy(parsedPrivateKey) + ); + const { signedWebBundle } = await signer.sign(); + greenConsoleLog(`${new WebBundleId(parsedPrivateKey)}`); + fs.writeFileSync(options.output, signedWebBundle); +} diff --git a/js/sign/src/cli.ts b/js/sign/src/cli.ts deleted file mode 100644 index 5be0707a..00000000 --- a/js/sign/src/cli.ts +++ /dev/null @@ -1,87 +0,0 @@ -import commander from 'commander'; -import { - NodeCryptoSigningStrategy, - IntegrityBlockSigner, - WebBundleId, - parsePemKey, - readPassphrase, -} from './wbn-sign.js'; -import * as fs from 'fs'; -import { KeyObject } from 'crypto'; - -function readOptions() { - return commander - .requiredOption( - '-i, --input ', - 'input web bundle to be signed (required)' - ) - .requiredOption( - '-k, --privateKey ', - 'path to ed25519 private key (required)' - ) - .option( - '-o, --output ', - 'signed web bundle output file', - 'signed.wbn' - ) - .parse(process.argv); -} - -// Parses either an unencrypted or encrypted private key. For encrypted keys, it -// reads the passphrase to decrypt them from either the -// `WEB_BUNDLE_SIGNING_PASSPHRASE` environment variable, or, if not set, prompts -// the user for the passphrase. -export async function parseMaybeEncryptedKey( - privateKeyFile: Buffer -): Promise { - // Read unencrypted private key. - try { - return parsePemKey(privateKeyFile); - } catch (e) { - console.warn('This key is probably an encrypted private key.'); - } - - const hasEnvVarSet = - process.env.WEB_BUNDLE_SIGNING_PASSPHRASE && - process.env.WEB_BUNDLE_SIGNING_PASSPHRASE !== ''; - - // Read encrypted private key. - try { - return parsePemKey( - privateKeyFile, - hasEnvVarSet - ? process.env.WEB_BUNDLE_SIGNING_PASSPHRASE - : await readPassphrase() - ); - } catch (e) { - throw Error( - `Failed decrypting encrypted private key with passphrase read from ${ - hasEnvVarSet - ? '`WEB_BUNDLE_SIGNING_PASSPHRASE` environment variable' - : 'prompt' - }` - ); - } -} - -export async function main() { - const options = readOptions(); - const webBundle = fs.readFileSync(options.input); - const parsedPrivateKey = await parseMaybeEncryptedKey( - fs.readFileSync(options.privateKey) - ); - const signer = new IntegrityBlockSigner( - webBundle, - new NodeCryptoSigningStrategy(parsedPrivateKey) - ); - const { signedWebBundle } = await signer.sign(); - - const consoleLogColor = { green: '\x1b[32m', reset: '\x1b[0m' }; - console.log( - `${consoleLogColor.green}${new WebBundleId(parsedPrivateKey)}${ - consoleLogColor.reset - }` - ); - - fs.writeFileSync(options.output, signedWebBundle); -} diff --git a/js/sign/src/utils/cli-utils.ts b/js/sign/src/utils/cli-utils.ts new file mode 100644 index 00000000..10bcaa39 --- /dev/null +++ b/js/sign/src/utils/cli-utils.ts @@ -0,0 +1,55 @@ +import tty from 'tty'; +import { KeyObject } from 'crypto'; +import { parsePemKey, readPassphrase } from '../wbn-sign.js'; + +// Parses either an unencrypted or encrypted private key. For encrypted keys, it +// reads the passphrase to decrypt them from either the +// `WEB_BUNDLE_SIGNING_PASSPHRASE` environment variable, or, if not set, prompts +// the user for the passphrase. +export async function parseMaybeEncryptedKey( + privateKeyFile: Buffer +): Promise { + // Read unencrypted private key. + try { + return parsePemKey(privateKeyFile); + } catch (e) { + console.warn('This key is probably an encrypted private key.'); + } + + const hasEnvVarSet = + process.env.WEB_BUNDLE_SIGNING_PASSPHRASE && + process.env.WEB_BUNDLE_SIGNING_PASSPHRASE !== ''; + + // Read encrypted private key. + try { + return parsePemKey( + privateKeyFile, + hasEnvVarSet + ? process.env.WEB_BUNDLE_SIGNING_PASSPHRASE + : await readPassphrase() + ); + } catch (e) { + throw Error( + `Failed decrypting encrypted private key with passphrase read from ${ + hasEnvVarSet + ? '`WEB_BUNDLE_SIGNING_PASSPHRASE` environment variable' + : 'prompt' + }` + ); + } +} + +export function greenConsoleLog(text: string): void { + const logColor = { green: '\x1b[32m', reset: '\x1b[0m' }; + + // @ts-expect-error Unknown property `fd`. + const fileDescriptor: number = process.stdout.fd ?? 1; + + // If the log is used for non-terminal (fd != 1), e.g., setting an environment + // variable, it shouldn't have any formatting. + console.log( + tty.isatty(fileDescriptor) + ? `${logColor.green}${text}${logColor.reset}` + : text + ); +} diff --git a/js/sign/src/utils/utils.ts b/js/sign/src/utils/utils.ts index de272281..d71dcdaa 100644 --- a/js/sign/src/utils/utils.ts +++ b/js/sign/src/utils/utils.ts @@ -9,6 +9,9 @@ export async function readPassphrase(): Promise { prompt: 'Passphrase for the key: ', silent: true, replace: '*', + // Output must be != `stdout`. Otherwise saving the `wbn-dump-id` + // result into a file or an environment variable also includes the prompt. + output: process.stderr, }); return passphrase; } catch (er) { diff --git a/js/sign/tests/cli_test.js b/js/sign/tests/cli-utils_test.js similarity index 81% rename from js/sign/tests/cli_test.js rename to js/sign/tests/cli-utils_test.js index 9ccd482a..21b59e56 100644 --- a/js/sign/tests/cli_test.js +++ b/js/sign/tests/cli-utils_test.js @@ -1,4 +1,4 @@ -import * as cli from '../lib/cli.js'; +import * as cliUtils from '../lib/utils/cli-utils.js'; import * as mockStdin from 'mock-stdin'; const TEST_UNENCRYPTED_PRIVATE_KEY = Buffer.from( @@ -37,19 +37,25 @@ describe('CLI key parsing', () => { }); it('works for unencrypted key.', async () => { - const key = await cli.parseMaybeEncryptedKey(TEST_UNENCRYPTED_PRIVATE_KEY); + const key = await cliUtils.parseMaybeEncryptedKey( + TEST_UNENCRYPTED_PRIVATE_KEY + ); expect(key.type).toEqual('private'); }); it('works for encrypted key read from `WEB_BUNDLE_SIGNING_PASSPHRASE`.', async () => { process.env.WEB_BUNDLE_SIGNING_PASSPHRASE = PASSPHRASE; - const key = await cli.parseMaybeEncryptedKey(TEST_ENCRYPTED_PRIVATE_KEY); + const key = await cliUtils.parseMaybeEncryptedKey( + TEST_ENCRYPTED_PRIVATE_KEY + ); expect(key.type).toEqual('private'); }); it('works for encrypted key read from a prompt.', async () => { const stdin = mockStdin.stdin(); - const keyPromise = cli.parseMaybeEncryptedKey(TEST_ENCRYPTED_PRIVATE_KEY); + const keyPromise = cliUtils.parseMaybeEncryptedKey( + TEST_ENCRYPTED_PRIVATE_KEY + ); stdin.send(`${PASSPHRASE}\n`); expect((await keyPromise).type).toEqual('private'); }); @@ -58,14 +64,16 @@ describe('CLI key parsing', () => { process.env.WEB_BUNDLE_SIGNING_PASSPHRASE = 'helloworld1'; await expectToThrowErrorAsync(() => - cli.parseMaybeEncryptedKey(TEST_ENCRYPTED_PRIVATE_KEY) + cliUtils.parseMaybeEncryptedKey(TEST_ENCRYPTED_PRIVATE_KEY) ); }); it('fails for faulty passphrase read from a prompt.', async () => { await expectToThrowErrorAsync(async () => { const stdin = mockStdin.stdin(); - const keyPromise = cli.parseMaybeEncryptedKey(TEST_ENCRYPTED_PRIVATE_KEY); + const keyPromise = cliUtils.parseMaybeEncryptedKey( + TEST_ENCRYPTED_PRIVATE_KEY + ); stdin.send(`${PASSPHRASE}1\n`); await keyPromise; });