@freedomofpress/webcat 0.0.0-stage → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/Readme.md +45 -0
  2. package/data/block.json +101 -0
  3. package/data/list.json +80 -0
  4. package/dist/hooks/content.js +1 -0
  5. package/dist/lib/browser/chrome.d.ts +86 -0
  6. package/dist/lib/browser/chrome.js +138 -0
  7. package/dist/lib/browser/kvstore.d.ts +70 -0
  8. package/dist/lib/browser/kvstore.js +241 -0
  9. package/dist/lib/browser/permissions.d.ts +113 -0
  10. package/dist/lib/browser/permissions.js +182 -0
  11. package/dist/lib/browser/requests.d.ts +145 -0
  12. package/dist/lib/browser/requests.js +292 -0
  13. package/dist/lib/browser/scripting.d.ts +19 -0
  14. package/dist/lib/browser/scripting.js +92 -0
  15. package/dist/lib/browser/sync.d.ts +68 -0
  16. package/dist/lib/browser/sync.js +114 -0
  17. package/dist/lib/browser/utils.d.ts +5 -0
  18. package/dist/lib/browser/utils.js +11 -0
  19. package/dist/lib/config.d.ts +45 -0
  20. package/dist/lib/config.js +45 -0
  21. package/dist/lib/validator_set.d.ts +4 -0
  22. package/dist/lib/validator_set.js +20 -0
  23. package/dist/lib/webcat/cache.d.ts +200 -0
  24. package/dist/lib/webcat/cache.js +308 -0
  25. package/dist/lib/webcat/canonicalize.d.ts +1 -0
  26. package/dist/lib/webcat/canonicalize.js +58 -0
  27. package/dist/lib/webcat/db.d.ts +30 -0
  28. package/dist/lib/webcat/db.js +131 -0
  29. package/dist/lib/webcat/encoding.d.ts +8 -0
  30. package/dist/lib/webcat/encoding.js +57 -0
  31. package/dist/lib/webcat/handler.d.ts +26 -0
  32. package/dist/lib/webcat/handler.js +419 -0
  33. package/dist/lib/webcat/hookbuilder.d.ts +47 -0
  34. package/dist/lib/webcat/hookbuilder.js +170 -0
  35. package/dist/lib/webcat/interfaces/bundle.d.ts +46 -0
  36. package/dist/lib/webcat/interfaces/bundle.js +8 -0
  37. package/dist/lib/webcat/interfaces/database.d.ts +68 -0
  38. package/dist/lib/webcat/interfaces/database.js +1 -0
  39. package/dist/lib/webcat/interfaces/errors.d.ts +451 -0
  40. package/dist/lib/webcat/interfaces/errors.js +109 -0
  41. package/dist/lib/webcat/interfaces/log.d.ts +14 -0
  42. package/dist/lib/webcat/interfaces/log.js +10 -0
  43. package/dist/lib/webcat/interfaces/originstate.d.ts +40 -0
  44. package/dist/lib/webcat/interfaces/originstate.js +1 -0
  45. package/dist/lib/webcat/interfaces/requeststate.d.ts +10 -0
  46. package/dist/lib/webcat/interfaces/requeststate.js +1 -0
  47. package/dist/lib/webcat/logger.d.ts +64 -0
  48. package/dist/lib/webcat/logger.js +112 -0
  49. package/dist/lib/webcat/originstate.d.ts +45 -0
  50. package/dist/lib/webcat/originstate.js +302 -0
  51. package/dist/lib/webcat/parsers.d.ts +21 -0
  52. package/dist/lib/webcat/parsers.js +36 -0
  53. package/dist/lib/webcat/request.d.ts +11 -0
  54. package/dist/lib/webcat/request.js +78 -0
  55. package/dist/lib/webcat/resources.d.ts +3 -0
  56. package/dist/lib/webcat/resources.js +41 -0
  57. package/dist/lib/webcat/response.d.ts +39 -0
  58. package/dist/lib/webcat/response.js +394 -0
  59. package/dist/lib/webcat/ui.d.ts +70 -0
  60. package/dist/lib/webcat/ui.js +282 -0
  61. package/dist/lib/webcat/updater.d.ts +115 -0
  62. package/dist/lib/webcat/updater.js +296 -0
  63. package/dist/lib/webcat/utils.d.ts +8 -0
  64. package/dist/lib/webcat/utils.js +74 -0
  65. package/dist/lib/webcat/validators.d.ts +16 -0
  66. package/dist/lib/webcat/validators.js +457 -0
  67. package/dist/lib/webcat.d.ts +11 -0
  68. package/dist/lib/webcat.js +130 -0
  69. package/icons/dark/webcat-error.SVG +1 -0
  70. package/icons/dark/webcat-ok.SVG +1 -0
  71. package/icons/dark/webcat.SVG +1 -0
  72. package/icons/light/webcat-error.SVG +1 -0
  73. package/icons/light/webcat-ok.SVG +1 -0
  74. package/icons/light/webcat.SVG +1 -0
  75. package/package.json +72 -4
  76. package/pages/error.css +9 -0
  77. package/pages/error.html +70 -0
  78. package/pages/error.js +51 -0
  79. package/pages/popup.css +16 -0
  80. package/pages/popup.html +14 -0
  81. package/pages/popup.js +48 -0
  82. package/README.md +0 -3
@@ -0,0 +1,8 @@
1
+ export declare function getFQDN(url: string): string;
2
+ export declare function getFQDNSafe(url: string): string;
3
+ export declare function isSameOriginURL(target: string, base: string): boolean;
4
+ export declare function isExtensionRequest(details: browser.webRequest._OnBeforeRequestDetails): boolean;
5
+ export declare function SHA256(data: ArrayBuffer | Uint8Array | string): Promise<ArrayBuffer>;
6
+ export declare function arraysEqual(a: Uint8Array, b: Uint8Array): boolean;
7
+ export declare function isNewerSemver(a: string, b: string): boolean;
8
+ export declare function clearBrowserCaches(fqdns: string[]): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import permissions from "../browser/permissions.js";
2
+ function getFQDN(url) {
3
+ const urlobj = new URL(url);
4
+ return urlobj.hostname;
5
+ }
6
+ function getFQDNSafe(url) {
7
+ if (!/^https?:\/\//i.test(url)) {
8
+ url = `https://${url}`;
9
+ }
10
+ return getFQDN(url);
11
+ }
12
+ function isSameOriginURL(target, base) {
13
+ try {
14
+ const baseURL = new URL(base);
15
+ const targetURL = new URL(target, baseURL);
16
+ return targetURL.origin === baseURL.origin;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function isExtensionRequest(details) {
22
+ return details.originUrl !== void 0 && details.documentUrl !== void 0 && details.originUrl.substring(0, 16) === "moz-extension://" && details.documentUrl.substring(0, 16) === "moz-extension://" && details.tabId === -1;
23
+ }
24
+ async function SHA256(data) {
25
+ let input;
26
+ if (typeof data === "string") {
27
+ input = new TextEncoder().encode(data).buffer;
28
+ } else if (data instanceof Uint8Array) {
29
+ input = data.slice().buffer;
30
+ } else {
31
+ input = data;
32
+ }
33
+ return crypto.subtle.digest("SHA-256", input);
34
+ }
35
+ function arraysEqual(a, b) {
36
+ if (a.length !== b.length) return false;
37
+ for (let i = 0; i < a.length; i++) {
38
+ if (a[i] !== b[i]) return false;
39
+ }
40
+ return true;
41
+ }
42
+ function isNewerSemver(a, b) {
43
+ const pa = a.split(".").map(Number);
44
+ const pb = b.split(".").map(Number);
45
+ const len = Math.max(pa.length, pb.length);
46
+ for (let i = 0; i < len; i++) {
47
+ const na = pa[i] ?? 0;
48
+ const nb = pb[i] ?? 0;
49
+ if (na > nb) return true;
50
+ if (na < nb) return false;
51
+ }
52
+ return false;
53
+ }
54
+ permissions.require("browsingData")(clearBrowserCaches);
55
+ async function clearBrowserCaches(fqdns) {
56
+ if (fqdns.length === 0) {
57
+ return;
58
+ }
59
+ await browser.browsingData.remove(
60
+ { hostnames: fqdns },
61
+ { serviceWorkers: true }
62
+ );
63
+ await browser.browsingData.remove({}, { cache: true });
64
+ }
65
+ export {
66
+ SHA256,
67
+ arraysEqual,
68
+ clearBrowserCaches,
69
+ getFQDN,
70
+ getFQDNSafe,
71
+ isExtensionRequest,
72
+ isNewerSemver,
73
+ isSameOriginURL
74
+ };
@@ -0,0 +1,16 @@
1
+ import { Manifest, SigstoreEnrollment, SigstoreSignatures, SigsumEnrollment, SigsumSignatures } from "./interfaces/bundle";
2
+ import { WebcatError } from "./interfaces/errors";
3
+ /**
4
+ * Validates a Content Security Policy. Enforces the restrictions outlined in
5
+ * {@link https://docs.webcat.tech/webapp-developers/CSP.html | the CSP docs}.
6
+ * Host sources are never allowed, so no enrollment lookup is needed.
7
+ *
8
+ * @param csp The policy string to validate.
9
+ */
10
+ export declare function validateCSP(csp: string): void;
11
+ export declare function witnessTimestampsFromCosignedTreeHead(compiledPolicy: Uint8Array, treeHead: string): Promise<number[]>;
12
+ export declare function validateSigsumEnrollment(enrollment: SigsumEnrollment): WebcatError | null;
13
+ export declare function validateSigstoreEnrollment(enrollment: SigstoreEnrollment): WebcatError | null;
14
+ export declare function validateManifest(manifest: Manifest): WebcatError | null;
15
+ export declare function verifySigsumManifest(enrollment: SigsumEnrollment, manifest: Manifest, signatures: SigsumSignatures): Promise<WebcatError | number>;
16
+ export declare function verifySigstoreManifest(enrollment: SigstoreEnrollment, manifest: Manifest, signatures: SigstoreSignatures): Promise<WebcatError | number>;
@@ -0,0 +1,457 @@
1
+ import { SigstoreVerifier, AllOf, PolicyError, EXTENSION_OID_OTHERNAME } from "@freedomofpress/sigstore-browser";
2
+ import { verifyMessageWithCompiledPolicy } from "@freedomofpress/sigsum";
3
+ import { parseCompiledPolicy, importAndHashAll, evalQuorumBytecode } from "@freedomofpress/sigsum/dist/compiledPolicy";
4
+ import { verifySignedTreeHead, verifyCosignedTreeHead } from "@freedomofpress/sigsum/dist/crypto";
5
+ import { parseCosignedTreeHead } from "@freedomofpress/sigsum/dist/proof";
6
+ import { RawPublicKey, Base64KeyHash } from "@freedomofpress/sigsum/dist/types";
7
+ import { canonicalize } from "./canonicalize.js";
8
+ import { stringToUint8Array, base64UrlToUint8Array } from "./encoding.js";
9
+ import { WebcatError, WebcatErrorCode } from "./interfaces/errors.js";
10
+ import { parseContentSecurityPolicy } from "./parsers.js";
11
+ function validateCSP(csp) {
12
+ let directives;
13
+ ((directives2) => {
14
+ directives2["DefaultSrc"] = "default-src";
15
+ directives2["ScriptSrc"] = "script-src";
16
+ directives2["ScriptSrcElem"] = "script-src-elem";
17
+ directives2["ScriptSrcAttr"] = "script-src-attr";
18
+ directives2["StyleSrc"] = "style-src";
19
+ directives2["StyleSrcElem"] = "style-src-elem";
20
+ directives2["StyleSrcAttr"] = "style-src-attr";
21
+ directives2["ObjectSrc"] = "object-src";
22
+ directives2["ChildSrc"] = "child-src";
23
+ directives2["FrameSrc"] = "frame-src";
24
+ directives2["WorkerSrc"] = "worker-src";
25
+ })(directives || (directives = {}));
26
+ let source_keywords;
27
+ ((source_keywords2) => {
28
+ source_keywords2["None"] = "'none'";
29
+ source_keywords2["Self"] = "'self'";
30
+ source_keywords2["WasmUnsafeEval"] = "'wasm-unsafe-eval'";
31
+ source_keywords2["UnsafeInline"] = "'unsafe-inline'";
32
+ source_keywords2["UnsafeEval"] = "'unsafe-eval'";
33
+ source_keywords2["UnsafeHashes"] = "'unsafe-hashes'";
34
+ source_keywords2["StrictDynamic"] = "'strict-dynamic'";
35
+ })(source_keywords || (source_keywords = {}));
36
+ const script_keywords = [
37
+ "'none'",
38
+ "'self'",
39
+ "'wasm-unsafe-eval'"
40
+ /* WasmUnsafeEval */
41
+ ];
42
+ const style_keywords = [
43
+ "'none'",
44
+ "'self'",
45
+ "'unsafe-inline'",
46
+ "'unsafe-hashes'"
47
+ /* UnsafeHashes */
48
+ ];
49
+ if (csp.includes(",")) {
50
+ throw new Error(`CSP contains a comma: ${csp}`);
51
+ }
52
+ const parsedCSP = parseContentSecurityPolicy(csp);
53
+ const default_src = parsedCSP.get(
54
+ "default-src"
55
+ /* DefaultSrc */
56
+ ) ?? [];
57
+ for (const src of default_src) {
58
+ if (src !== "'none'" && src !== "'self'") {
59
+ throw new Error(
60
+ `Unexpected or non-allowed default-src directive: ${src}`
61
+ );
62
+ }
63
+ }
64
+ const default_src_is_none = default_src.length === 1 && default_src[0] === "'none'";
65
+ const hash_source = /^'sha(256|384|512)-[A-Za-z0-9+/_-]+={0,2}'$/i;
66
+ function validateDirective(directive, allowed_keywords, hashes = false, optional = default_src_is_none) {
67
+ const list = parsedCSP.get(directive);
68
+ if (!list?.length && !optional) {
69
+ throw new Error(
70
+ `${"default-src"} is not none, and ${directive} is not defined.`
71
+ );
72
+ }
73
+ for (const src of list ?? []) {
74
+ const lower = src.toLowerCase();
75
+ if (!allowed_keywords.includes(lower) && !(hashes && hash_source.test(src))) {
76
+ throw new Error(
77
+ `${directive} cannot contain ${src} which is unsupported.`
78
+ );
79
+ }
80
+ }
81
+ }
82
+ validateDirective("object-src", [
83
+ "'none'"
84
+ /* None */
85
+ ]);
86
+ validateDirective("script-src", script_keywords, true);
87
+ validateDirective(
88
+ "script-src-attr",
89
+ [
90
+ "'none'"
91
+ /* None */
92
+ ],
93
+ false,
94
+ true
95
+ );
96
+ validateDirective(
97
+ "script-src-elem",
98
+ script_keywords,
99
+ true,
100
+ default_src_is_none || parsedCSP.has(
101
+ "script-src"
102
+ /* ScriptSrc */
103
+ )
104
+ );
105
+ validateDirective("style-src", style_keywords, true);
106
+ validateDirective(
107
+ "style-src-elem",
108
+ style_keywords,
109
+ true,
110
+ default_src_is_none || parsedCSP.has(
111
+ "style-src"
112
+ /* StyleSrc */
113
+ )
114
+ );
115
+ validateDirective("style-src-attr", style_keywords, true, true);
116
+ validateDirective(
117
+ "worker-src",
118
+ [
119
+ "'none'",
120
+ "'self'"
121
+ /* Self */
122
+ ],
123
+ false,
124
+ default_src_is_none && !parsedCSP.has(
125
+ "child-src"
126
+ /* ChildSrc */
127
+ )
128
+ );
129
+ }
130
+ async function witnessTimestampsFromCosignedTreeHead(compiledPolicy, treeHead) {
131
+ const compiled = parseCompiledPolicy(compiledPolicy);
132
+ const logs = await importAndHashAll(compiled.logsRaw);
133
+ const witnesses = await importAndHashAll(compiled.witnessesRaw);
134
+ const cosignedTreeHead = await parseCosignedTreeHead(
135
+ treeHead.split("\n")
136
+ );
137
+ let logKeyHash = null;
138
+ for (const log of logs) {
139
+ if (await verifySignedTreeHead(
140
+ cosignedTreeHead.SignedTreeHead,
141
+ log.pub,
142
+ log.hash
143
+ )) {
144
+ logKeyHash = log.hash;
145
+ break;
146
+ }
147
+ }
148
+ if (!logKeyHash) {
149
+ throw new Error("no log key in policy verified the tree head");
150
+ }
151
+ const present = new Uint8Array(witnesses.length);
152
+ const timestamps = [];
153
+ for (const [i, witness] of witnesses.entries()) {
154
+ const cosignature = Base64KeyHash.lookup(
155
+ cosignedTreeHead.Cosignatures,
156
+ witness.b64
157
+ );
158
+ if (!cosignature) continue;
159
+ if (await verifyCosignedTreeHead(
160
+ cosignedTreeHead.SignedTreeHead.TreeHead,
161
+ witness.pub,
162
+ logKeyHash,
163
+ cosignature
164
+ )) {
165
+ present[i] = 1;
166
+ timestamps.push(cosignature.Timestamp);
167
+ }
168
+ }
169
+ if (!evalQuorumBytecode(compiled.quorum, witnesses.length, present)) {
170
+ throw new Error("cosignature quorum not satisfied");
171
+ }
172
+ return timestamps;
173
+ }
174
+ function validateSigsumEnrollment(enrollment) {
175
+ if (typeof enrollment.policy !== "string") {
176
+ return new WebcatError(WebcatErrorCode.Enrollment.POLICY_MALFORMED);
177
+ }
178
+ if (enrollment.policy.length === 0 || enrollment.policy.length > 8192) {
179
+ return new WebcatError(WebcatErrorCode.Enrollment.POLICY_LENGTH);
180
+ }
181
+ if (!Array.isArray(enrollment.signers)) {
182
+ return new WebcatError(WebcatErrorCode.Enrollment.SIGNERS_MALFORMED);
183
+ }
184
+ if (enrollment.signers.length === 0) {
185
+ return new WebcatError(WebcatErrorCode.Enrollment.SIGNERS_EMPTY);
186
+ }
187
+ for (const key of enrollment.signers) {
188
+ if (typeof key !== "string") {
189
+ return new WebcatError(WebcatErrorCode.Enrollment.SIGNERS_KEY_MALFORMED, [
190
+ String(key)
191
+ ]);
192
+ }
193
+ }
194
+ if (typeof enrollment.threshold !== "number" || !Number.isInteger(enrollment.threshold) || enrollment.threshold < 1) {
195
+ return new WebcatError(WebcatErrorCode.Enrollment.THRESHOLD_MALFORMED);
196
+ }
197
+ if (enrollment.threshold > enrollment.signers.length) {
198
+ return new WebcatError(WebcatErrorCode.Enrollment.THRESHOLD_IMPOSSIBLE);
199
+ }
200
+ if (typeof enrollment.max_age !== "number" || !Number.isFinite(enrollment.max_age)) {
201
+ return new WebcatError(WebcatErrorCode.Enrollment.MAX_AGE_MALFORMED);
202
+ }
203
+ if (typeof enrollment.logs !== "object" || enrollment.logs === null || Object.keys(enrollment.logs).length === 0) {
204
+ return new WebcatError(WebcatErrorCode.Enrollment.LOGS_MALFORMED);
205
+ }
206
+ for (const [pubkey, url] of Object.entries(enrollment.logs)) {
207
+ if (typeof pubkey !== "string" || typeof url !== "string") {
208
+ return new WebcatError(WebcatErrorCode.Enrollment.LOGS_MALFORMED);
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+ function validateSigstoreEnrollment(enrollment) {
214
+ if (!enrollment.trusted_root) {
215
+ return new WebcatError(WebcatErrorCode.Enrollment.TRUSTED_ROOT_MISSING);
216
+ }
217
+ if (typeof enrollment.claims !== "object" || enrollment.claims === null || Array.isArray(enrollment.claims)) {
218
+ return new WebcatError(WebcatErrorCode.Enrollment.CLAIMS_MISSING);
219
+ }
220
+ if (Object.keys(enrollment.claims).length < 1) {
221
+ return new WebcatError(WebcatErrorCode.Enrollment.CLAIMS_EMPTY);
222
+ }
223
+ for (const [oid, value] of Object.entries(enrollment.claims)) {
224
+ if (typeof oid !== "string" || oid.length < 1) {
225
+ return new WebcatError(WebcatErrorCode.Enrollment.CLAIMS_MALFORMED);
226
+ }
227
+ if (typeof value !== "string" || value.length < 1) {
228
+ return new WebcatError(WebcatErrorCode.Enrollment.CLAIMS_MALFORMED, [
229
+ String(oid)
230
+ ]);
231
+ }
232
+ }
233
+ if (typeof enrollment.max_age !== "number" || !Number.isFinite(enrollment.max_age)) {
234
+ return new WebcatError(WebcatErrorCode.Enrollment.MAX_AGE_MALFORMED);
235
+ }
236
+ return null;
237
+ }
238
+ const SHA256_BASE64URL = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
239
+ function validateManifest(manifest) {
240
+ if (!manifest.files || Object.keys(manifest.files).length < 1) {
241
+ return new WebcatError(WebcatErrorCode.Manifest.FILES_MISSING);
242
+ }
243
+ for (const [path, hash] of Object.entries(manifest.files)) {
244
+ if (!SHA256_BASE64URL.test(hash)) {
245
+ return new WebcatError(WebcatErrorCode.Manifest.FILES_MALFORMED, [path]);
246
+ }
247
+ }
248
+ if (!manifest.default_csp) {
249
+ return new WebcatError(WebcatErrorCode.Manifest.DEFAULT_CSP_MISSING);
250
+ }
251
+ if (!manifest.default_index) {
252
+ return new WebcatError(WebcatErrorCode.Manifest.DEFAULT_INDEX_MISSING);
253
+ }
254
+ if (!manifest.default_fallback) {
255
+ return new WebcatError(WebcatErrorCode.Manifest.DEFAULT_FALLBACK_MISSING);
256
+ }
257
+ if (!manifest.files["/" + manifest.default_index]) {
258
+ return new WebcatError(WebcatErrorCode.Manifest.DEFAULT_INDEX_MISSING_FILE);
259
+ }
260
+ if (!manifest.files[manifest.default_fallback]) {
261
+ return new WebcatError(
262
+ WebcatErrorCode.Manifest.DEFAULT_FALLBACK_MISSING_FILE
263
+ );
264
+ }
265
+ if (!manifest.wasm) {
266
+ return new WebcatError(WebcatErrorCode.Manifest.WASM_MISSING);
267
+ }
268
+ return null;
269
+ }
270
+ async function verifySigsumManifest(enrollment, manifest, signatures) {
271
+ const untrusted = signatures;
272
+ if (typeof untrusted !== "object" || untrusted === null || Array.isArray(untrusted)) {
273
+ return new WebcatError(WebcatErrorCode.Bundle.SIGNATURES_MISSING);
274
+ }
275
+ const canonical = canonicalize(manifest);
276
+ if (canonical === null) {
277
+ return new WebcatError(WebcatErrorCode.Manifest.VERIFY_FAILED);
278
+ }
279
+ const canonicalized = stringToUint8Array(canonical);
280
+ const remainingSigners = new Set(enrollment.signers);
281
+ let validCount = 0;
282
+ for (const pubKey of Object.keys(signatures)) {
283
+ if (!remainingSigners.has(pubKey)) {
284
+ continue;
285
+ }
286
+ try {
287
+ await verifyMessageWithCompiledPolicy(
288
+ canonicalized,
289
+ new RawPublicKey(base64UrlToUint8Array(pubKey)),
290
+ base64UrlToUint8Array(enrollment.policy),
291
+ signatures[pubKey]
292
+ );
293
+ } catch (e) {
294
+ return new WebcatError(WebcatErrorCode.Manifest.VERIFY_FAILED, [
295
+ String(e)
296
+ ]);
297
+ }
298
+ remainingSigners.delete(pubKey);
299
+ validCount++;
300
+ }
301
+ if (validCount < enrollment.threshold) {
302
+ return new WebcatError(WebcatErrorCode.Manifest.THRESHOLD_UNSATISFIED, [
303
+ String(validCount),
304
+ String(enrollment.threshold)
305
+ ]);
306
+ }
307
+ if (!manifest.timestamp) {
308
+ return new WebcatError(WebcatErrorCode.Manifest.TIMESTAMP_MISSING);
309
+ }
310
+ let timestamps;
311
+ try {
312
+ timestamps = await witnessTimestampsFromCosignedTreeHead(
313
+ base64UrlToUint8Array(enrollment.policy),
314
+ manifest.timestamp
315
+ );
316
+ } catch (e) {
317
+ return new WebcatError(WebcatErrorCode.Manifest.TIMESTAMP_VERIFY_FAILED, [
318
+ String(e)
319
+ ]);
320
+ }
321
+ const timestamp = timestamps.sort((a, b) => a - b)[Math.floor(timestamps.length / 2)];
322
+ const now = Math.floor(Date.now() / 1e3);
323
+ if (now - timestamp > enrollment.max_age) {
324
+ return new WebcatError(WebcatErrorCode.Manifest.EXPIRED, [
325
+ String(enrollment.max_age),
326
+ String(timestamp)
327
+ ]);
328
+ }
329
+ return timestamp + enrollment.max_age;
330
+ }
331
+ class ClaimPolicy {
332
+ constructor(oid, expected) {
333
+ this.oid = oid;
334
+ this.expected = expected;
335
+ }
336
+ claimMatches(got) {
337
+ if (this.expected.startsWith("^")) {
338
+ const expectedPrefix = this.expected.slice(1);
339
+ return got.startsWith(expectedPrefix);
340
+ }
341
+ return got === this.expected;
342
+ }
343
+ verify(cert) {
344
+ if (this.oid === "2.5.29.17") {
345
+ const sanExt = cert.extSubjectAltName;
346
+ if (!sanExt) {
347
+ throw new PolicyError("Certificate missing SubjectAlternativeName");
348
+ }
349
+ const allSans = /* @__PURE__ */ new Set();
350
+ if (sanExt.rfc822Name) allSans.add(sanExt.rfc822Name);
351
+ if (sanExt.uri) allSans.add(sanExt.uri);
352
+ const other = sanExt.otherName(EXTENSION_OID_OTHERNAME);
353
+ if (other) allSans.add(other);
354
+ const sanMatches = [...allSans].some((san) => this.claimMatches(san));
355
+ if (!sanMatches) {
356
+ throw new PolicyError(
357
+ `SAN mismatch for 2.5.29.17: expected '${this.expected}'`
358
+ );
359
+ }
360
+ return;
361
+ }
362
+ const ext = cert.extension(this.oid);
363
+ if (!ext) {
364
+ throw new PolicyError(`Certificate missing extension ${this.oid}`);
365
+ }
366
+ let got;
367
+ try {
368
+ const valueObj = ext.valueObj;
369
+ if (valueObj.subs && valueObj.subs.length > 0) {
370
+ const inner = valueObj.subs[0];
371
+ if (inner?.value) {
372
+ got = new TextDecoder().decode(inner.value);
373
+ }
374
+ }
375
+ if (!got) {
376
+ got = new TextDecoder().decode(ext.value);
377
+ }
378
+ } catch {
379
+ throw new PolicyError(`Unable to decode extension ${this.oid}`);
380
+ }
381
+ if (!this.claimMatches(got)) {
382
+ throw new PolicyError(
383
+ `Extension ${this.oid} mismatch: got '${got}', expected '${this.expected}'`
384
+ );
385
+ }
386
+ }
387
+ }
388
+ class CertFreshnessPolicy {
389
+ constructor(maxAgeSeconds) {
390
+ this.maxAgeSeconds = maxAgeSeconds;
391
+ this.validUntil = 0;
392
+ }
393
+ verify(cert) {
394
+ const now = Math.floor(Date.now() / 1e3);
395
+ const issued = Math.floor(cert.notBefore.getTime() / 1e3);
396
+ const validUntil = issued + this.maxAgeSeconds;
397
+ if (now > validUntil) {
398
+ this.expired = new WebcatError(WebcatErrorCode.Manifest.EXPIRED, [
399
+ String(this.maxAgeSeconds),
400
+ String(issued)
401
+ ]);
402
+ throw new PolicyError(
403
+ `Signing certificate is too old: issued at ${issued}, max age ${this.maxAgeSeconds}s`
404
+ );
405
+ }
406
+ if (!this.validUntil || this.validUntil > validUntil) {
407
+ this.validUntil = validUntil;
408
+ }
409
+ }
410
+ }
411
+ async function verifySigstoreManifest(enrollment, manifest, signatures) {
412
+ const verifier = new SigstoreVerifier();
413
+ await verifier.loadSigstoreRoot(enrollment.trusted_root);
414
+ const claimPolicies = [];
415
+ for (const [oid, expected] of Object.entries(enrollment.claims)) {
416
+ claimPolicies.push(new ClaimPolicy(oid, expected));
417
+ }
418
+ const freshnessPolicy = new CertFreshnessPolicy(enrollment.max_age);
419
+ claimPolicies.push(freshnessPolicy);
420
+ const policy = new AllOf(claimPolicies);
421
+ if (!Array.isArray(signatures)) {
422
+ return new WebcatError(WebcatErrorCode.Bundle.SIGNATURES_MISSING);
423
+ }
424
+ const canonical = canonicalize(manifest);
425
+ if (canonical === null) {
426
+ return new WebcatError(WebcatErrorCode.Manifest.VERIFY_FAILED);
427
+ }
428
+ const canonicalized = stringToUint8Array(canonical);
429
+ let verified = false;
430
+ for (const bundle of signatures) {
431
+ try {
432
+ verified = await verifier.verifyArtifactPolicy(
433
+ policy,
434
+ bundle,
435
+ canonicalized
436
+ );
437
+ if (verified) {
438
+ break;
439
+ }
440
+ } catch (e) {
441
+ console.log(e);
442
+ }
443
+ }
444
+ if (!verified) {
445
+ return freshnessPolicy.expired ?? new WebcatError(WebcatErrorCode.Manifest.VERIFY_FAILED);
446
+ }
447
+ return freshnessPolicy.validUntil;
448
+ }
449
+ export {
450
+ validateCSP,
451
+ validateManifest,
452
+ validateSigstoreEnrollment,
453
+ validateSigsumEnrollment,
454
+ verifySigstoreManifest,
455
+ verifySigsumManifest,
456
+ witnessTimestampsFromCosignedTreeHead
457
+ };
@@ -0,0 +1,11 @@
1
+ import { WebcatConfig } from "./config";
2
+ declare const _default: {
3
+ /**
4
+ * Sets up the full unmodified WEBCAT experience as it is designed to operate
5
+ * in Firefox-based browsers.
6
+ *
7
+ * @param options Configuration options. Defaults to {@link defaults}.
8
+ */
9
+ start(options?: Partial<WebcatConfig>): void;
10
+ };
11
+ export default _default;
@@ -0,0 +1,130 @@
1
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
2
+ var __typeError = (msg) => {
3
+ throw TypeError(msg);
4
+ };
5
+ var __using = (stack, value, async) => {
6
+ if (value != null) {
7
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
8
+ var dispose, inner;
9
+ if (async) dispose = value[__knownSymbol("asyncDispose")];
10
+ if (dispose === void 0) {
11
+ dispose = value[__knownSymbol("dispose")];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") __typeError("Object not disposable");
15
+ if (inner) dispose = function() {
16
+ try {
17
+ inner.call(this);
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ };
22
+ stack.push([async, dispose, value]);
23
+ } else if (async) {
24
+ stack.push([async]);
25
+ }
26
+ return value;
27
+ };
28
+ var __callDispose = (stack, error, hasError) => {
29
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
30
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
31
+ };
32
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
33
+ var next = (it) => {
34
+ while (it = stack.pop()) {
35
+ try {
36
+ var result = it[1] && it[1].call(it[2]);
37
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
38
+ } catch (e) {
39
+ fail(e);
40
+ }
41
+ }
42
+ if (hasError) throw error;
43
+ };
44
+ return next();
45
+ };
46
+ import { defaults } from "./config.js";
47
+ import { WebcatDatabase } from "./webcat/db.js";
48
+ import { WebcatRequestHandler } from "./webcat/handler.js";
49
+ import { WebcatUI } from "./webcat/ui.js";
50
+ import { EnrollmentUpdater } from "./webcat/updater.js";
51
+ import "./validator_set.js";
52
+ import "./browser/kvstore.js";
53
+ import "./browser/permissions.js";
54
+ import "./browser/sync.js";
55
+ import "./webcat/cache.js";
56
+ import "./webcat/originstate.js";
57
+ import "./webcat/canonicalize.js";
58
+ import "./webcat/encoding.js";
59
+ import "./webcat/interfaces/bundle.js";
60
+ import "./webcat/interfaces/errors.js";
61
+ import "./webcat/utils.js";
62
+ import "./webcat/validators.js";
63
+ import "@freedomofpress/sigstore-browser";
64
+ import "@freedomofpress/sigsum";
65
+ import "@freedomofpress/sigsum/dist/compiledPolicy";
66
+ import "@freedomofpress/sigsum/dist/crypto";
67
+ import "@freedomofpress/sigsum/dist/proof";
68
+ import "@freedomofpress/sigsum/dist/types";
69
+ import "./webcat/parsers.js";
70
+ import "./browser/requests.js";
71
+ import "./browser/utils.js";
72
+ import "./browser/scripting.js";
73
+ import "./webcat/hookbuilder.js";
74
+ import "./webcat/logger.js";
75
+ import "./webcat/request.js";
76
+ import "./webcat/resources.js";
77
+ import "./webcat/response.js";
78
+ import "./browser/chrome.js";
79
+ import "@freedomofpress/cometbft/dist/commit";
80
+ import "@freedomofpress/cometbft/dist/lightclient";
81
+ import "@freedomofpress/cometbft/dist/validators";
82
+ import "@freedomofpress/ics23/dist/webcat";
83
+ const webcat = {
84
+ /**
85
+ * Sets up the full unmodified WEBCAT experience as it is designed to operate
86
+ * in Firefox-based browsers.
87
+ *
88
+ * @param options Configuration options. Defaults to {@link defaults}.
89
+ */
90
+ start(options) {
91
+ const cfg = Object.assign(Object.assign({}, defaults), options);
92
+ const db = new WebcatDatabase(cfg);
93
+ const ui = new WebcatUI(db, db.namespace("ui"), cfg);
94
+ const requestHandler = new WebcatRequestHandler(db, ui, cfg);
95
+ const updater = new EnrollmentUpdater(Object.assign({ database: db }, cfg));
96
+ requestHandler.bindAll();
97
+ requestHandler.addEventListener(
98
+ "beforeframeload",
99
+ async (event) => {
100
+ var _stack = [];
101
+ try {
102
+ const blockingResponse = __using(_stack, event.createBlockingResponse());
103
+ await updater.retryIfFailed();
104
+ blockingResponse.cancel = false;
105
+ } catch (_) {
106
+ var _error = _, _hasError = true;
107
+ } finally {
108
+ __callDispose(_stack, _error, _hasError);
109
+ }
110
+ }
111
+ );
112
+ let firstUpdate = true;
113
+ updater.addEventListener("updated", async (event) => {
114
+ if (!event.success && !firstUpdate) {
115
+ return;
116
+ }
117
+ try {
118
+ const fqdns = await db.listAllFQDNs();
119
+ await requestHandler.bind(fqdns);
120
+ firstUpdate = false;
121
+ } catch (error) {
122
+ console.error("[webcat] Bundled list import failed:", error);
123
+ }
124
+ });
125
+ updater.start();
126
+ }
127
+ };
128
+ export {
129
+ webcat as default
130
+ };