Skip to content
Open
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
94 changes: 65 additions & 29 deletions js/sign/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ This is a Node.js module for signing
[Web Bundles](https://wpack-wg.github.io/bundled-responses/draft-ietf-wpack-bundled-responses.html)
with [integrityblock](../../explainers/integrity-signature.md).

The module takes an existing bundle file and an ed25519 private key, and emits a
new bundle file with cryptographic signature added to the integrity block.
The module takes an existing bundle file and an ed25519 or ecdsa-p256 private key, and emits a
new bundle file with cryptographic signature(s) added to the integrity block.

The module also support other operations on Integrity block like adding/removing/replacing signatures.

## Installation

Expand All @@ -24,7 +26,9 @@ This plugin requires Node v22.13.0+.
Please be aware that the APIs are not stable yet and are subject to change at
any time.

Signing a web bundle file:
### Signing a Web Bundle

The recommended way to sign a web bundle is using the `SignedWebBundle` class.

```javascript
import * as fs from 'fs';
Expand All @@ -37,42 +41,70 @@ const privateKey = wbnSign.parsePemKey(
fs.readFileSync('./path/to/privatekey.pem', 'utf-8')
);

// Option 1: With the default (`NodeCryptoSigningStrategy`) signing strategy.
const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(webBundle, {
key: privateKey,
}).sign();
// Sign with a single key.
const signedWebBundle = await wbnSign.SignedWebBundle.fromWebBundle(
webBundle,
[new wbnSign.NodeCryptoSigningStrategy(privateKey)]
);

// Get the signed bytes to save to a file.
const signedBytes = signedWebBundle.getSignedWebBundleBytes();
fs.writeFileSync('./path/to/signed.swbn', signedBytes);

// Get the Web Bundle ID (App ID).
console.log('Web Bundle ID:', signedWebBundle.getWebBundleId());
```

### Advanced Usage: Multiple Signatures

You can sign a bundle with multiple keys at once, or add signatures to an already signed bundle.

// Option 2: With specified signing strategy.
const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
```javascript
// Sign with multiple keys at once.
const multiSigned = await wbnSign.SignedWebBundle.fromWebBundle(
webBundle,
new wbnSign.NodeCryptoSigningStrategy(privateKey)
).sign();
[strategy1, strategy2]
);

// Or add a signature to an existing SignedWebBundle instance.
await multiSigned.addSignature(strategy3);

// Option 3: With ones own CustomSigningStrategy class implementing
// ISigningStrategy.
const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
// You can also load an existing signed web bundle from bytes.
const existingSigned = wbnSign.SignedWebBundle.fromBytes(
fs.readFileSync('./path/to/already_signed.swbn')
);

// And remove a signature by providing the public key.
existingSigned.removeSignature(publicKeyBytes);
```

### Custom Signing Strategies

You can implement your own signing strategy by implementing the `ISigningStrategy` interface.

```javascript
const customStrategy = new (class {
async sign(data: Uint8Array): Promise<Uint8Array> {
// Connect to an external signing service.
}
async getPublicKey(): Promise<KeyObject> {
// Return the public key from the service.
}
})();

const signedWebBundle = await wbnSign.SignedWebBundle.fromWebBundle(
webBundle,
new (class {
async sign(data: Uint8Array): Promise<Uint8Array> {
// E.g. connect to one's external signing service that signs the payload.
}
async getPublicKey(): Promise<KeyObject> {
/** E.g. connect to one's external signing service that returns the public
* key.*/
}
})()
).sign();

fs.writeFileSync(signedWebBundle);
[customStrategy]
);
```

### Calculating Web Bundle ID

This library also exposes a helper class to calculate the Web Bundle's ID (or
App ID) from the private or public ed25519 key, which can then be used when
App ID) from the private or public key, which can then be used when
bundling
[Isolated Web Apps](https://github.com/WICG/isolated-web-apps/blob/main/README.md).

Calculating the web bundle ID for Isolated Web Apps:

```javascript
import * as fs from 'fs';
import * as wbnSign from 'wbn-sign';
Expand Down Expand Up @@ -211,6 +243,10 @@ environment variable named `WEB_BUNDLE_SIGNING_PASSPHRASE`.

## Release Notes

### v0.3.0
- **Major architectural update**: Introduced `SignedWebBundle` as the primary interface for managing signed bundles.
- Support for **multi-signatures**: Add, remove, and replace signatures in already signed web bundles.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a NOTICE here that IntegrityBlockSigner will be removed in a future release and is deperecated now.


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

Expand Down
10 changes: 5 additions & 5 deletions js/sign/package-lock.json

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

2 changes: 1 addition & 1 deletion js/sign/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wbn-sign",
"version": "0.2.7",
"version": "0.3.0",
"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",
Expand Down
147 changes: 147 additions & 0 deletions js/sign/src/core/integrity-block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import assert from 'assert';

import { decode, encode } from 'cborg';

import {
INTEGRITY_BLOCK_MAGIC,
SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING,
VERSION_B2,
WEB_BUNDLE_ID_ATTRIBUTE_NAME,
type SignatureType,
} from '../utils/constants.js';

export type SignatureAttributes = {
[SignatureAttributeKey: string]: Uint8Array;
};

export type IntegritySignature = {
signatureAttributes: SignatureAttributes;
signature: Uint8Array;
};

export class IntegrityBlock {
private attributes: Map<string, string> = new Map();
private signatureStack: IntegritySignature[] = [];

/** @internal */
constructor() {}

static fromCbor(integrityBlockBytes: Uint8Array): IntegrityBlock {
const integrityBlock = new IntegrityBlock();
try {
const [magic, version, attributes, signatureStack] = decode(
integrityBlockBytes,
{ useMaps: true }
);

assert(magic instanceof Uint8Array, 'Invalid magic bytes');
assert.deepStrictEqual(
magic,
INTEGRITY_BLOCK_MAGIC,
'Invalid magic bytes'
);

assert(version instanceof Uint8Array, 'Invalid version');
assert.deepStrictEqual(version, VERSION_B2, 'Invalid version');

assert(attributes instanceof Map, 'Invalid attributes');
assert(
attributes.has(WEB_BUNDLE_ID_ATTRIBUTE_NAME),
`Missing ${WEB_BUNDLE_ID_ATTRIBUTE_NAME} attribute`
);
integrityBlock.setWebBundleId(
attributes.get(WEB_BUNDLE_ID_ATTRIBUTE_NAME)!
);

assert(signatureStack instanceof Array, 'Invalid signature stack');
assert(signatureStack.length > 0, 'Invalid signature stack');

for (const signatureBlock of signatureStack) {
assert(signatureBlock instanceof Array, 'Invalid signature');
assert.strictEqual(signatureBlock.length, 2, 'Invalid signature');

const [attributes, signature] = signatureBlock;
assert(attributes instanceof Map, 'Invalid signature attributes');
assert(signature instanceof Uint8Array, 'Invalid signature');
assert.equal(attributes.size, 1, 'Invalid signature attributes');

const [keyType, publicKey] = [...attributes][0];
assert(
SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING.has(keyType),
'Invalid signature attribute key type'
);
assert(
publicKey instanceof Uint8Array,
'Invalid signature attribute key'
);

integrityBlock.addIntegritySignature({
signatureAttributes: { [keyType]: publicKey },
signature: Buffer.from(signature),
});
}
return integrityBlock;
} catch (err) {
throw new Error(`Invalid integrity block: ${(err as Error).message}`, {
cause: err,
});
}
}

getWebBundleId(): string {
return this.attributes.get(WEB_BUNDLE_ID_ATTRIBUTE_NAME)!;
}

setWebBundleId(webBundleId: string) {
this.attributes.set(WEB_BUNDLE_ID_ATTRIBUTE_NAME, webBundleId);
}

addIntegritySignature(is: IntegritySignature) {
this.signatureStack.push(is);
}

removeIntegritySignature(publicKey: Uint8Array) {
this.signatureStack = this.signatureStack.filter((integritySignature) => {
// Uint8Arrays cannot be directly compared, but Buffer can
return !Buffer.from(
Object.values(integritySignature.signatureAttributes)[0]
).equals(publicKey);
});
}

getSignatureStack(): IntegritySignature[] {
return this.signatureStack;
}

toCbor(): Uint8Array {
return encode([
INTEGRITY_BLOCK_MAGIC,
VERSION_B2,
this.attributes,
this.signatureStack.map((integritySig) => {
// The CBOR must have an array of length 2 containing the following:
// (0) attributes and (1) signature. The order is important.
return [integritySig.signatureAttributes, integritySig.signature];
}),
]);
}

// Stripped CBOR does not include signatures and is a part of data which hash is signed
/** @internal */
toStrippedCbor(): Uint8Array {
return encode([INTEGRITY_BLOCK_MAGIC, VERSION_B2, this.attributes, []]);
}

private static parseSignatureAttributes(
attributes: SignatureAttributes
): [SignatureType, Uint8Array] {
assert(
Object.entries(attributes).length == 1,
'Invalid signature attributes'
);
const [maybeType, publicKey] = Object.entries(attributes)[0];
const type = SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING.get(maybeType);
assert(type != undefined, 'Invalid signature attributes');
return [type, publicKey];
}
}
Loading
Loading