Skip to content

Commit 30bf533

Browse files
rferenszgroza
andcommitted
New API - SignedWebBundle
Apply suggestions from code review Co-authored-by: Zgroza (Luke) Klimek <zgroza@google.com>
1 parent 13b6b52 commit 30bf533

13 files changed

Lines changed: 741 additions & 127 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)