forked from WICG/webpackage
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsigned-web-bundle.ts
More file actions
168 lines (144 loc) · 5.43 KB
/
Copy pathsigned-web-bundle.ts
File metadata and controls
168 lines (144 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { assert } from 'console';
import { encode } from 'cborg';
import { IntegrityBlockSigner } from '../signers/integrity-block-signer.js';
import { ISigningStrategy } from '../signers/signing-strategy-interface.js';
import { warnLog } from '../utils/cli-utils.js';
import {
calcWebBundleHash,
generateDataToBeSigned,
isSignedWebBundle,
parseRawPublicKey,
verifySignature,
} from '../utils/utils.js';
import { WebBundleId } from '../web-bundle-id.js';
import { IntegrityBlock } from './integrity-block.js';
export type SignatureValidationResult =
| { status: 'success'; derivedBundleId: string; isValid: boolean }
| { status: 'error'; error: string };
export class SignedWebBundle {
private constructor(
private integrityBlock: IntegrityBlock,
private pureWebBundle: Uint8Array
) {}
// Use with raw Singed Web Bundle data, for example read from file with fs.readFile('bundle.swbn')
static fromBytes(signedWebBundle: Uint8Array): SignedWebBundle {
if (!isSignedWebBundle(signedWebBundle)) {
throw new Error('Not a signed web bundle.');
}
const offset = this.obtainIntegrityOffset(signedWebBundle);
const integrityBlockBytes = signedWebBundle.slice(0, offset);
const integrityBlock = IntegrityBlock.fromCbor(integrityBlockBytes);
const bundle = signedWebBundle.slice(offset);
return new SignedWebBundle(integrityBlock, bundle);
}
// Web bundle ID is derived from the first key if not provided
static async fromWebBundle(
webBundle: Uint8Array,
signingStrategies: Array<ISigningStrategy>,
options?: { webBundleId?: string }
): Promise<SignedWebBundle> {
assert(signingStrategies.length > 0, 'No signing strategies provided');
const publicKey = await signingStrategies[0].getPublicKey();
const webBundleId =
options?.webBundleId ?? new WebBundleId(publicKey).serialize();
const { signedWebBundle } = await new IntegrityBlockSigner(
webBundle,
webBundleId,
signingStrategies
).sign();
return SignedWebBundle.fromBytes(signedWebBundle);
}
async addSignature(signingStrategy: ISigningStrategy): Promise<this> {
this.integrityBlock = await new IntegrityBlockSigner(
this.pureWebBundle,
this.integrityBlock,
[signingStrategy]
).signAndGetIntegrityBlock();
return this;
}
removeSignature(publicKey: Uint8Array): this {
this.integrityBlock.removeIntegritySignature(publicKey);
return this;
}
printInfo(): void {
warnLog(
'This feature is experimental, the form of output will change in the future. Do not use with scripts.'
);
this.integrityBlock.printInfo();
}
getIntegrityBlock(): IntegrityBlock {
return this.integrityBlock;
}
validateSignatures(): SignatureValidationResult[] {
const webBundleHash = calcWebBundleHash(this.pureWebBundle);
const ibCbor = this.integrityBlock.toStrippedCbor();
return this.integrityBlock.getSignatureStack().map((signature) => {
try {
const [keyType, publicKey] = IntegrityBlock.parseSignatureAttributes(
signature.signatureAttributes
);
const keyObject = parseRawPublicKey(keyType, publicKey);
const derivedBundleId = new WebBundleId(keyObject).serialize();
const attrCbor = encode(signature.signatureAttributes);
const dataToBeSigned = generateDataToBeSigned(
webBundleHash,
ibCbor,
attrCbor
);
const isValid = verifySignature(
dataToBeSigned,
signature.signature,
keyObject
);
return { status: 'success', isValid, derivedBundleId };
} catch (err) {
return {
status: 'error',
error: err instanceof Error ? err.message : String(err),
};
}
});
}
getWebBundleId(): string {
return this.integrityBlock.getWebBundleId();
}
getSignedWebBundleBytes(): Uint8Array {
if (this.integrityBlock.getSignatureStack().length == 0) {
throw new Error(
'Signed Web Bundle instance is in invalid state (no signatures).'
);
}
if (!this.integrityBlock.getWebBundleId()) {
throw new Error(
'Signed Web Bundle instance is in invalid state (bundle id not set)'
);
}
return Buffer.concat([this.integrityBlock.toCbor(), this.pureWebBundle]);
}
overrideBundleId(bundleId: string): this {
this.integrityBlock.setWebBundleId(bundleId);
return this;
}
// private method, static to emphasize pure functional character
private static obtainIntegrityOffset(bundle: Uint8Array): number {
const bundleLengthFromInternalData =
SignedWebBundle.readWebBundleLength(bundle);
const offset = bundle.length - bundleLengthFromInternalData;
if (bundleLengthFromInternalData < 0 || offset <= 0) {
throw new Error(
'SignedWebBundle::fromBytes: The provided bytes do not represent a signed web bundle.'
);
}
return offset;
}
// private method, static to emphasize pure functional character
private static readWebBundleLength(bundle: Uint8Array): number {
// The length of the web bundle is contained in the last 8 bytes of the web
// bundle, represented as BigEndian.
const buffer = Buffer.from(bundle.slice(-8));
// Number is big enough to represent 4GB which is the limit for the web
// bundle size which can be contained in a Buffer, which is the format
// in which it's passed back to e.g. Webpack.
return Number(buffer.readBigUint64BE());
}
}