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; });