Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
74 changes: 50 additions & 24 deletions js/sign/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm install wbn-sign

## Requirements

This plugin requires Node v16.0.0+.
This plugin requires Node v22.13.0+.

## Usage

Expand Down Expand Up @@ -99,13 +99,48 @@ This package also includes 2 CLI tools
- `wbn-dump-id` which can be used to calculate the Web Bundle ID corresponding
to your signing key.

### Running wbn-sign
### Running wbn-sign
#### New usage form (>= 0.2.7)

There are the following command-line flags available:
The base usage from is: `wbn-sign [command] [options] <arguments...>`

Currenly supported commands are:
```
Usage: wbn-sign sign [options] <web_bundle> <private_keys...>

Signs the given web bundle with private key(s). Produces signed web bundle output file.

Arguments:
web_bundle a web bundle (file `*.wbn`) to sign
private_keys private keys (files `*.pem`) with which the web bundle will be signed. EcdsaP256 and ed25519 keys (encrypted and not encrypted) are supported.

Options:
-o, --output <file> signed web bundle output file (default: "signed.swbn")
--web-bundle-id <web-bundle-id> web bundle ID. Derived from the first key if not specified.
-h, --help display help for command
```

For more details check `wbn-sign help [command]`.

Example commands:

```bash
wbn-sign sign ~/path/to/webbundle.wbn ~/path/to/ed25519key.pem -o ~/path/to/signed-webbundle.swbn
```

```bash
wbn-sign sign ~/path/to/webbundle.wbn ~/path/to/ed25519key.pem ~/path/to/ecdsa_p256key.pem \
--web-bundle-id amfcf7c4bmpbjbmq4h4yptcobves56hfdyr7tm3doxqvfmsk5ss6maacai \
-o ~/path/to/signed-webbundle.swbn
```


#### Legacy usage (<0.2.6)
Previously the CLI tool used only options (no command). This usage form will be deprecated, but in the actual version is still supported.

In `wbn-sign [options]` form followling options are available:
- (required) `--private-key <filePath>` (`-k <filePath>`)
which takes the path to ed25519 private key. If chosen format is `v2`, this
can be specified multiple times.
which takes the path to ed25519/ecdsaP256 private key. Can be specified multiple times.
- (required) `--input <filePath>` (`-i <filePath>`)
which takes the path to the web bundle to be signed.
- (optional) `--output <filePath>` (`-o <filePath>`)
Expand All @@ -115,25 +150,6 @@ There are the following command-line flags available:
`--web-bundle-id <web-bundle-id>`
which takes the `web-bundle-id` to be associated with the web bundle.

Example commands:

```bash
wbn-sign \
-i ~/path/to/webbundle.wbn \
-o ~/path/to/signed-webbundle.swbn \
-k ~/path/to/ed25519key.pem
```

```bash
wbn-sign \
-i ~/path/to/webbundle.wbn \
-o ~/path/to/signed-webbundle.swbn \
-k ~/path/to/ed25519key.pem \
-k ~/path/to/ecdsa_p256key.pem
--web-bundle-id \
amfcf7c4bmpbjbmq4h4yptcobves56hfdyr7tm3doxqvfmsk5ss6maacai
```

### Running wbn-dump-id

There are the following command-line flags available:
Expand Down Expand Up @@ -195,6 +211,16 @@ environment variable named `WEB_BUNDLE_SIGNING_PASSPHRASE`.

## Release Notes

### v0.2.7
- The new command line interface that supports commands introduced for wbn-sign tool

### v0.2.6
Mostly developer changes, not affecting usage:

- Adds eslint to control style consistency and prevent errors with `npx eslint .`
- Adds prettifier module to keep imports always sorted
- Updates some devDependecy packages version

### v0.2.5

- Add support for dumping bundle IDs from public keys (used to be private-only).
Expand Down
2 changes: 1 addition & 1 deletion js/sign/bin/wbn-sign.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node
import { main } from '../lib/cli-sign.js';

main();
await main();
14 changes: 7 additions & 7 deletions js/sign/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions js/sign/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "wbn-sign",
"version": "0.2.6",
"description": "Signing tool to sign a web bundle with integrity block",
"version": "0.2.7",
"description": "Tool to sign web bundles and manage signatures of signed web bundles.",
"homepage": "https://github.com/WICG/webpackage/tree/main/js/sign",
"main": "./lib/wbn-sign.cjs",
"type": "module",
Expand All @@ -15,6 +15,8 @@
"build:esm": "tsc",
"build:cjs": "esbuild --bundle --format=cjs --outfile=lib/wbn-sign.cjs src/wbn-sign.ts --platform=node",
"test": "jasmine tests/*.js tests/*.cjs",
"test:cli": "jasmine tests/cli_test.js",
"prepublishOnly": "npm test",
"lint": "npx prettier --write . --ignore-unknown --config ./package.json"
},
"bin": {
Expand Down Expand Up @@ -42,7 +44,7 @@
"dependencies": {
"base32-encode": "^2.0.0",
"cborg": "^4.2.14",
"commander": "^7.0.0",
"commander": "^14.0.0",
"read": "^2.0.0"
},
"devDependencies": {
Expand Down
149 changes: 127 additions & 22 deletions js/sign/src/cli-sign.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,89 @@
import { KeyObject } from 'crypto';
import * as fs from 'fs';
import { createRequire } from 'module';

import { Command } from 'commander';

import {
errorLog,
greenConsoleLog,
infoLog,
parseMaybeEncryptedKeyFromFile,
warnLog,
} from './utils/cli-utils.js';
import {
IntegrityBlockSigner,
NodeCryptoSigningStrategy,
WebBundleId,
} from './wbn-sign.js';

const require = createRequire(import.meta.url);
const { name, version } = require('../package.json');

const program = new Command()
.name('wbn-sign')
.name(name)
.version(version)
.description(
'A simple CLI tool to sign the given web bundle with the given private key.'
`A simple CLI tool for managing signatures and keys of (signed) web bundles.
The primary use case is signing web bundles with private keys.`
);

function readOptions() {
return program
.requiredOption(
'-i, --input <file>',
'input web bundle to be signed (required)'
async function parseArguments(): Promise<void> {
// The main use case - signing web bundles
program
.command('sign')
.description(
'Signs the given web bundle with private key(s). Produces signed web bundle output file.'
)
.argument('<web_bundle>', 'a web bundle (file `*.wbn`) to sign')
.argument(
'<private_keys...>',
'private keys (files `*.pem`) with which the web bundle will be signed. EcdsaP256 and ed25519 keys (encrypted and not encrypted) are supported.'
)
.option(
'-o, --output <file>',
'signed web bundle output file',
/*defaultValue=*/ 'signed.swbn'
)
.requiredOption(
.option(
'--web-bundle-id <web-bundle-id>',
'web bundle ID. Derived from the first key if not specified.'
)
.showHelpAfterError()
.action(async (webBundle, privateKeys, options) => {
if (!options.webBundleId) {
infoLog(
`The bundle id was not specified. It will be derived from the ${
privateKeys.length > 1 ? 'first ' : ''
}given key.`
);
}
await readFilesAndSignWebBundle(
webBundle,
privateKeys,
options.output,
options.webBundleId
);
});

program
.command('sing', { hidden: true })
.argument('[anything...]')
.action(() => {
greenConsoleLog('🎶 Never gonna let you down, lalala la lala... 🎶 \n');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use this instead

errorLog("Unrecognized command 'sing'. Use 'sign' instead.\n");
process.exit(1);
});

// This default command provides backward compatibility.
// The tool in the past only supported signing and didn't use commands.
program
.command('backward-compatibility-sign', { isDefault: true, hidden: true })
// That's the workaround for proper error message, when an improper command is used.
// It's then interpreted as an argument of the default command, so falls here.
.argument('[...]', '')
.option('-i, --input <file>', 'input web bundle to be signed (required)')
.option(
'-k, --private-key <file...>',
'paths to Ed25519 / ECDSA P-256 private key(s) (required)'
)
Expand All @@ -34,36 +92,83 @@ function readOptions() {
'signed web bundle output file',
/*defaultValue=*/ 'signed.swbn'
)
.option('--web-bundle-id <web-bundle-id>', 'web bundle ID (only for v2)')
.action((options) => {
.option('--web-bundle-id <web-bundle-id>', 'web bundle ID')
// Command-specific error message on parsing error (e.g. no value, or incorrect option)
.showHelpAfterError()
.action(async (args, options, command) => {
// Wrong command
if (args.length > 0) {
// Help finishes the program internally
program.help();
}

// Does it seem like old usage? If not just show help.
if (
!('input' in options) &&
!('privateKey' in options) &&
!('webBundleId' in options)
) {
program.help();
}

// Backward-compatible mode
warnLog(
'This `wbn-sign` usage is deprecated. Please check `wbn-sign help`. This CLI usage form may be not supported in the future.'
);

if (!('input' in options) || !('privateKey' in options)) {
errorLog(
`input and private key options are required! Please, consider using new cli (see \`wbn-sign help\`)`
);
command.help();
}

if (options.privateKey.length > 1 && !options.webBundleId) {
throw new Error(
errorLog(
`--web-bundle-id must be specified if there's more than 1 signing key involved.`
);
command.help();
}
})
.parse(process.argv)
.opts();

await readFilesAndSignWebBundle(
options.input,
options.privateKey,
options.output,
options.webBundleId
);
});

program.helpCommand(true);

await program.parseAsync(process.argv);
}

export async function main() {
const options = readOptions();
const webBundle = fs.readFileSync(options.input);
async function readFilesAndSignWebBundle(
wbnFilePath: string,
keyFilesPaths: string[],
outputFilePath: string,
maybeWebBundleId?: string
) {
const webBundle = fs.readFileSync(wbnFilePath);

const privateKeys = new Array<KeyObject>();
for (const privateKey of options.privateKey) {
for (const privateKey of keyFilesPaths) {
privateKeys.push(await parseMaybeEncryptedKeyFromFile(privateKey));
}

const webBundleId = options.webBundleId
? options.webBundleId
: new WebBundleId(privateKeys[0]).serialize();
const webBundleId =
maybeWebBundleId ?? new WebBundleId(privateKeys[0]).serialize();

const signer = new IntegrityBlockSigner(
Uint8Array.from(webBundle),
webBundleId,
privateKeys.map((privateKey) => new NodeCryptoSigningStrategy(privateKey))
);
const { signedWebBundle } = await signer.sign();
greenConsoleLog(`${webBundleId}`);
fs.writeFileSync(options.output, signedWebBundle);
fs.writeFileSync(outputFilePath, signedWebBundle);
}

export async function main() {
await parseArguments();
}
Loading