Skip to content

Commit 19a856d

Browse files
authored
Add/remove/replace/info commands added to wbn-sign CLI
Summary This change refactors the core signing logic into a new high-level SignedWebBundle API, introduces support for multiple signatures Key Changes New SignedWebBundle API: Introduced a central class in src/core/signed-web-bundle.ts that provides a simplified, high-level interface for creating, loading, and modifying signed web bundles. It supports: Signing an unsigned bundle with one or more strategies. Adding or removing signatures from an existing signed bundle. Retrieving the Web Bundle ID (App ID). Loading signed bundles directly from bytes. Enhanced Integrity Block Management: Moved IntegrityBlock to a dedicated core module and added the ability to parse existing integrity blocks from CBOR. Refactored IntegrityBlockSigner to be internal, deprecating several methods in favor of the new SignedWebBundle and utility functions. Infrastructure & Utilities: Added new utility functions for identifying pure vs. signed bundles and verifying signatures. Updated tsconfig.json to strip internal declarations from the generated types.
1 parent 13b6b52 commit 19a856d

13 files changed

Lines changed: 741 additions & 124 deletions

js/sign/README.md

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ This is a Node.js module for signing
44
[Web Bundles](https://wpack-wg.github.io/bundled-responses/draft-ietf-wpack-bundled-responses.html)
55
with [integrityblock](../../explainers/integrity-signature.md).
66

7-
The module takes an existing bundle file and an ed25519 private key, and emits a
8-
new bundle file with cryptographic signature added to the integrity block.
7+
The module takes an existing bundle file and an ed25519 or ecdsa-p256 private key, and emits a
8+
new bundle file with cryptographic signature(s) added to the integrity block.
9+
10+
The module also support other operations on Integrity block like adding/removing/replacing signatures.
911

1012
## Installation
1113

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

27-
Signing a web bundle file:
29+
### Signing a Web Bundle
30+
31+
The recommended way to sign a web bundle is using the `SignedWebBundle` class.
2832

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

40-
// Option 1: With the default (`NodeCryptoSigningStrategy`) signing strategy.
41-
const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(webBundle, {
42-
key: privateKey,
43-
}).sign();
44+
// Sign with a single key.
45+
const signedWebBundle = await wbnSign.SignedWebBundle.fromWebBundle(
46+
webBundle,
47+
[new wbnSign.NodeCryptoSigningStrategy(privateKey)]
48+
);
49+
50+
// Get the signed bytes to save to a file.
51+
const signedBytes = signedWebBundle.getSignedWebBundleBytes();
52+
fs.writeFileSync('./path/to/signed.swbn', signedBytes);
53+
54+
// Get the Web Bundle ID (App ID).
55+
console.log('Web Bundle ID:', signedWebBundle.getWebBundleId());
56+
```
57+
58+
### Advanced Usage: Multiple Signatures
59+
60+
You can sign a bundle with multiple keys at once, or add signatures to an already signed bundle.
4461

45-
// Option 2: With specified signing strategy.
46-
const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
62+
```javascript
63+
// Sign with multiple keys at once.
64+
const multiSigned = await wbnSign.SignedWebBundle.fromWebBundle(
4765
webBundle,
48-
new wbnSign.NodeCryptoSigningStrategy(privateKey)
49-
).sign();
66+
[strategy1, strategy2]
67+
);
68+
69+
// Or add a signature to an existing SignedWebBundle instance.
70+
await multiSigned.addSignature(strategy3);
5071

51-
// Option 3: With ones own CustomSigningStrategy class implementing
52-
// ISigningStrategy.
53-
const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
72+
// You can also load an existing signed web bundle from bytes.
73+
const existingSigned = wbnSign.SignedWebBundle.fromBytes(
74+
fs.readFileSync('./path/to/already_signed.swbn')
75+
);
76+
77+
// And remove a signature by providing the public key.
78+
existingSigned.removeSignature(publicKeyBytes);
79+
```
80+
81+
### Custom Signing Strategies
82+
83+
You can implement your own signing strategy by implementing the `ISigningStrategy` interface.
84+
85+
```javascript
86+
const customStrategy = new (class {
87+
async sign(data: Uint8Array): Promise<Uint8Array> {
88+
// Connect to an external signing service.
89+
}
90+
async getPublicKey(): Promise<KeyObject> {
91+
// Return the public key from the service.
92+
}
93+
})();
94+
95+
const signedWebBundle = await wbnSign.SignedWebBundle.fromWebBundle(
5496
webBundle,
55-
new (class {
56-
async sign(data: Uint8Array): Promise<Uint8Array> {
57-
// E.g. connect to one's external signing service that signs the payload.
58-
}
59-
async getPublicKey(): Promise<KeyObject> {
60-
/** E.g. connect to one's external signing service that returns the public
61-
* key.*/
62-
}
63-
})()
64-
).sign();
65-
66-
fs.writeFileSync(signedWebBundle);
97+
[customStrategy]
98+
);
6799
```
68100

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

74-
Calculating the web bundle ID for Isolated Web Apps:
75-
76108
```javascript
77109
import * as fs from 'fs';
78110
import * as wbnSign from 'wbn-sign';
@@ -211,6 +243,12 @@ environment variable named `WEB_BUNDLE_SIGNING_PASSPHRASE`.
211243

212244
## Release Notes
213245

246+
### v0.3.0
247+
- **Major architectural update**: Introduced `SignedWebBundle` as the primary interface for managing signed bundles.
248+
- Support for **multi-signatures**: Add, remove, and replace signatures in already signed web bundles.
249+
- **NOTICE**: Upcoming breaking change. `IntegrityBlockSigner` is deprecated and will be removed in a future version. Migrate to `SignedWebBundle`.
250+
251+
214252
### v0.2.7
215253
- The new command line interface that supports commands introduced for wbn-sign tool
216254

js/sign/package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/sign/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wbn-sign",
3-
"version": "0.2.7",
3+
"version": "0.3.0",
44
"description": "Tool to sign web bundles and manage signatures of signed web bundles.",
55
"homepage": "https://github.com/WICG/webpackage/tree/main/js/sign",
66
"main": "./lib/wbn-sign.cjs",
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import assert from 'assert';
2+
3+
import { decode, encode } from 'cborg';
4+
5+
import {
6+
INTEGRITY_BLOCK_MAGIC,
7+
SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING,
8+
VERSION_B2,
9+
WEB_BUNDLE_ID_ATTRIBUTE_NAME,
10+
type SignatureType,
11+
} from '../utils/constants.js';
12+
13+
export type SignatureAttributes = {
14+
[SignatureAttributeKey: string]: Uint8Array;
15+
};
16+
17+
export type IntegritySignature = {
18+
signatureAttributes: SignatureAttributes;
19+
signature: Uint8Array;
20+
};
21+
22+
export class IntegrityBlock {
23+
private attributes: Map<string, string> = new Map();
24+
private signatureStack: IntegritySignature[] = [];
25+
26+
/** @internal */
27+
constructor() {}
28+
29+
static fromCbor(integrityBlockBytes: Uint8Array): IntegrityBlock {
30+
const integrityBlock = new IntegrityBlock();
31+
try {
32+
const [magic, version, attributes, signatureStack] = decode(
33+
integrityBlockBytes,
34+
{ useMaps: true }
35+
);
36+
37+
assert(magic instanceof Uint8Array, 'Invalid magic bytes');
38+
assert.deepStrictEqual(
39+
magic,
40+
INTEGRITY_BLOCK_MAGIC,
41+
'Invalid magic bytes'
42+
);
43+
44+
assert(version instanceof Uint8Array, 'Invalid version');
45+
assert.deepStrictEqual(version, VERSION_B2, 'Invalid version');
46+
47+
assert(attributes instanceof Map, 'Invalid attributes');
48+
assert(
49+
attributes.has(WEB_BUNDLE_ID_ATTRIBUTE_NAME),
50+
`Missing ${WEB_BUNDLE_ID_ATTRIBUTE_NAME} attribute`
51+
);
52+
integrityBlock.setWebBundleId(
53+
attributes.get(WEB_BUNDLE_ID_ATTRIBUTE_NAME)!
54+
);
55+
56+
assert(signatureStack instanceof Array, 'Invalid signature stack');
57+
assert(signatureStack.length > 0, 'Invalid signature stack');
58+
59+
for (const signatureBlock of signatureStack) {
60+
assert(signatureBlock instanceof Array, 'Invalid signature');
61+
assert.strictEqual(signatureBlock.length, 2, 'Invalid signature');
62+
63+
const [attributes, signature] = signatureBlock;
64+
assert(attributes instanceof Map, 'Invalid signature attributes');
65+
assert(signature instanceof Uint8Array, 'Invalid signature');
66+
assert.equal(attributes.size, 1, 'Invalid signature attributes');
67+
68+
const [keyType, publicKey] = [...attributes][0];
69+
assert(
70+
SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING.has(keyType),
71+
'Invalid signature attribute key type'
72+
);
73+
assert(
74+
publicKey instanceof Uint8Array,
75+
'Invalid signature attribute key'
76+
);
77+
78+
integrityBlock.addIntegritySignature({
79+
signatureAttributes: { [keyType]: publicKey },
80+
signature: Buffer.from(signature),
81+
});
82+
}
83+
return integrityBlock;
84+
} catch (err) {
85+
throw new Error(`Invalid integrity block: ${(err as Error).message}`, {
86+
cause: err,
87+
});
88+
}
89+
}
90+
91+
getWebBundleId(): string {
92+
return this.attributes.get(WEB_BUNDLE_ID_ATTRIBUTE_NAME)!;
93+
}
94+
95+
setWebBundleId(webBundleId: string) {
96+
this.attributes.set(WEB_BUNDLE_ID_ATTRIBUTE_NAME, webBundleId);
97+
}
98+
99+
addIntegritySignature(is: IntegritySignature) {
100+
this.signatureStack.push(is);
101+
}
102+
103+
removeIntegritySignature(publicKey: Uint8Array) {
104+
this.signatureStack = this.signatureStack.filter((integritySignature) => {
105+
// Uint8Arrays cannot be directly compared, but Buffer can
106+
return !Buffer.from(
107+
Object.values(integritySignature.signatureAttributes)[0]
108+
).equals(publicKey);
109+
});
110+
}
111+
112+
getSignatureStack(): IntegritySignature[] {
113+
return this.signatureStack;
114+
}
115+
116+
toCbor(): Uint8Array {
117+
return encode([
118+
INTEGRITY_BLOCK_MAGIC,
119+
VERSION_B2,
120+
this.attributes,
121+
this.signatureStack.map((integritySig) => {
122+
// The CBOR must have an array of length 2 containing the following:
123+
// (0) attributes and (1) signature. The order is important.
124+
return [integritySig.signatureAttributes, integritySig.signature];
125+
}),
126+
]);
127+
}
128+
129+
// Stripped CBOR does not include signatures and is a part of data which hash is signed
130+
/** @internal */
131+
toStrippedCbor(): Uint8Array {
132+
return encode([INTEGRITY_BLOCK_MAGIC, VERSION_B2, this.attributes, []]);
133+
}
134+
135+
private static parseSignatureAttributes(
136+
attributes: SignatureAttributes
137+
): [SignatureType, Uint8Array] {
138+
assert(
139+
Object.entries(attributes).length == 1,
140+
'Invalid signature attributes'
141+
);
142+
const [maybeType, publicKey] = Object.entries(attributes)[0];
143+
const type = SIGNATURE_ATTRIBUTE_TO_TYPE_MAPPING.get(maybeType);
144+
assert(type != undefined, 'Invalid signature attributes');
145+
return [type, publicKey];
146+
}
147+
}

0 commit comments

Comments
 (0)