@freedomofpress/webcat 0.0.0-stage → 3.0.0
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.
- package/Readme.md +45 -0
- package/data/block.json +101 -0
- package/data/list.json +80 -0
- package/dist/hooks/content.js +1 -0
- package/dist/lib/browser/chrome.d.ts +86 -0
- package/dist/lib/browser/chrome.js +138 -0
- package/dist/lib/browser/kvstore.d.ts +70 -0
- package/dist/lib/browser/kvstore.js +241 -0
- package/dist/lib/browser/permissions.d.ts +113 -0
- package/dist/lib/browser/permissions.js +182 -0
- package/dist/lib/browser/requests.d.ts +145 -0
- package/dist/lib/browser/requests.js +292 -0
- package/dist/lib/browser/scripting.d.ts +19 -0
- package/dist/lib/browser/scripting.js +92 -0
- package/dist/lib/browser/sync.d.ts +68 -0
- package/dist/lib/browser/sync.js +114 -0
- package/dist/lib/browser/utils.d.ts +5 -0
- package/dist/lib/browser/utils.js +11 -0
- package/dist/lib/config.d.ts +45 -0
- package/dist/lib/config.js +45 -0
- package/dist/lib/validator_set.d.ts +4 -0
- package/dist/lib/validator_set.js +20 -0
- package/dist/lib/webcat/cache.d.ts +200 -0
- package/dist/lib/webcat/cache.js +308 -0
- package/dist/lib/webcat/canonicalize.d.ts +1 -0
- package/dist/lib/webcat/canonicalize.js +58 -0
- package/dist/lib/webcat/db.d.ts +30 -0
- package/dist/lib/webcat/db.js +131 -0
- package/dist/lib/webcat/encoding.d.ts +8 -0
- package/dist/lib/webcat/encoding.js +57 -0
- package/dist/lib/webcat/handler.d.ts +26 -0
- package/dist/lib/webcat/handler.js +419 -0
- package/dist/lib/webcat/hookbuilder.d.ts +47 -0
- package/dist/lib/webcat/hookbuilder.js +170 -0
- package/dist/lib/webcat/interfaces/bundle.d.ts +46 -0
- package/dist/lib/webcat/interfaces/bundle.js +8 -0
- package/dist/lib/webcat/interfaces/database.d.ts +68 -0
- package/dist/lib/webcat/interfaces/database.js +1 -0
- package/dist/lib/webcat/interfaces/errors.d.ts +451 -0
- package/dist/lib/webcat/interfaces/errors.js +109 -0
- package/dist/lib/webcat/interfaces/log.d.ts +14 -0
- package/dist/lib/webcat/interfaces/log.js +10 -0
- package/dist/lib/webcat/interfaces/originstate.d.ts +40 -0
- package/dist/lib/webcat/interfaces/originstate.js +1 -0
- package/dist/lib/webcat/interfaces/requeststate.d.ts +10 -0
- package/dist/lib/webcat/interfaces/requeststate.js +1 -0
- package/dist/lib/webcat/logger.d.ts +64 -0
- package/dist/lib/webcat/logger.js +112 -0
- package/dist/lib/webcat/originstate.d.ts +45 -0
- package/dist/lib/webcat/originstate.js +302 -0
- package/dist/lib/webcat/parsers.d.ts +21 -0
- package/dist/lib/webcat/parsers.js +36 -0
- package/dist/lib/webcat/request.d.ts +11 -0
- package/dist/lib/webcat/request.js +78 -0
- package/dist/lib/webcat/resources.d.ts +3 -0
- package/dist/lib/webcat/resources.js +41 -0
- package/dist/lib/webcat/response.d.ts +39 -0
- package/dist/lib/webcat/response.js +394 -0
- package/dist/lib/webcat/ui.d.ts +70 -0
- package/dist/lib/webcat/ui.js +282 -0
- package/dist/lib/webcat/updater.d.ts +115 -0
- package/dist/lib/webcat/updater.js +296 -0
- package/dist/lib/webcat/utils.d.ts +8 -0
- package/dist/lib/webcat/utils.js +74 -0
- package/dist/lib/webcat/validators.d.ts +16 -0
- package/dist/lib/webcat/validators.js +457 -0
- package/dist/lib/webcat.d.ts +11 -0
- package/dist/lib/webcat.js +130 -0
- package/icons/dark/webcat-error.SVG +1 -0
- package/icons/dark/webcat-ok.SVG +1 -0
- package/icons/dark/webcat.SVG +1 -0
- package/icons/light/webcat-error.SVG +1 -0
- package/icons/light/webcat-ok.SVG +1 -0
- package/icons/light/webcat.SVG +1 -0
- package/package.json +72 -4
- package/pages/error.css +9 -0
- package/pages/error.html +70 -0
- package/pages/error.js +51 -0
- package/pages/popup.css +16 -0
- package/pages/popup.html +14 -0
- package/pages/popup.js +48 -0
- package/README.md +0 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RequestDetails } from "../browser/requests";
|
|
2
|
+
import { LogEntry } from "./interfaces/log";
|
|
3
|
+
import { Stateful } from "./interfaces/requeststate";
|
|
4
|
+
type ConsoleLevel = "log" | "warn" | "error" | "info" | "debug";
|
|
5
|
+
declare const globalLogs: LogEntry[];
|
|
6
|
+
declare class Logger {
|
|
7
|
+
private debugMode;
|
|
8
|
+
/**
|
|
9
|
+
* Enable or disable debug mode.
|
|
10
|
+
* @param enabled - Whether debug mode is enabled.
|
|
11
|
+
*/
|
|
12
|
+
setDebugMode(enabled: boolean): void;
|
|
13
|
+
/**
|
|
14
|
+
* Add a log entry.
|
|
15
|
+
* @param level - Log level (debug, info, warn, error).
|
|
16
|
+
* @param message - The log message.
|
|
17
|
+
* @param tabId - The tab ID associated with the log.
|
|
18
|
+
* @param origin - The origin of the log (e.g., URL or script).
|
|
19
|
+
* @param stack - Optional stack trace.
|
|
20
|
+
*/
|
|
21
|
+
addLog(level: ConsoleLevel, message: string, tabId: number, origin: string, stack?: string): void;
|
|
22
|
+
/** Adds a log entry with the default level. */
|
|
23
|
+
log(message: string, details: Stateful<RequestDetails>, stack?: string): void;
|
|
24
|
+
/** Adds a warning log entry. */
|
|
25
|
+
warn(message: string, details: Stateful<RequestDetails>, stack?: string): void;
|
|
26
|
+
/** Adds an error log entry. */
|
|
27
|
+
error(message: string, details: Stateful<RequestDetails>, stack?: string): void;
|
|
28
|
+
/** Adds an informational log entry. */
|
|
29
|
+
info(message: string, details: Stateful<RequestDetails>, stack?: string): void;
|
|
30
|
+
/** Add a debug log entry. */
|
|
31
|
+
debug(message: string, details: Stateful<RequestDetails>, stack?: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Determine if a log should be printed based on the debug mode and log level.
|
|
34
|
+
* @param level - Log level to check.
|
|
35
|
+
*/
|
|
36
|
+
private shouldPrint;
|
|
37
|
+
/**
|
|
38
|
+
* Get all log entries.
|
|
39
|
+
*/
|
|
40
|
+
getLogs(): LogEntry[];
|
|
41
|
+
/**
|
|
42
|
+
* Get logs filtered by level.
|
|
43
|
+
* @param level - Log level to filter by (debug, info, warn, error).
|
|
44
|
+
*/
|
|
45
|
+
getLogsByLevel(level: ConsoleLevel): LogEntry[];
|
|
46
|
+
/**
|
|
47
|
+
* Get logs filtered by tab ID.
|
|
48
|
+
* @param tabId - Tab ID to filter by.
|
|
49
|
+
*/
|
|
50
|
+
getLogsByTab(tabId: number): LogEntry[];
|
|
51
|
+
/**
|
|
52
|
+
* Get logs filtered by origin.
|
|
53
|
+
* @param origin - Origin (e.g., URL or script) to filter by.
|
|
54
|
+
*/
|
|
55
|
+
getLogsByOrigin(origin: string): LogEntry[];
|
|
56
|
+
/**
|
|
57
|
+
* Clear all logs.
|
|
58
|
+
*/
|
|
59
|
+
clearLogs(): void;
|
|
60
|
+
}
|
|
61
|
+
/** @internal */
|
|
62
|
+
export declare const logger: Logger;
|
|
63
|
+
/** @internal */
|
|
64
|
+
export { globalLogs };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const globalLogs = [];
|
|
2
|
+
class Logger {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.debugMode = true;
|
|
5
|
+
}
|
|
6
|
+
// Controls if debug/info logs are printed
|
|
7
|
+
/**
|
|
8
|
+
* Enable or disable debug mode.
|
|
9
|
+
* @param enabled - Whether debug mode is enabled.
|
|
10
|
+
*/
|
|
11
|
+
setDebugMode(enabled) {
|
|
12
|
+
this.debugMode = enabled;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Add a log entry.
|
|
16
|
+
* @param level - Log level (debug, info, warn, error).
|
|
17
|
+
* @param message - The log message.
|
|
18
|
+
* @param tabId - The tab ID associated with the log.
|
|
19
|
+
* @param origin - The origin of the log (e.g., URL or script).
|
|
20
|
+
* @param stack - Optional stack trace.
|
|
21
|
+
*/
|
|
22
|
+
addLog(level, message, tabId, origin, stack) {
|
|
23
|
+
const logEntry = {
|
|
24
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
25
|
+
tabId,
|
|
26
|
+
origin,
|
|
27
|
+
level,
|
|
28
|
+
message,
|
|
29
|
+
stack
|
|
30
|
+
};
|
|
31
|
+
globalLogs.push(logEntry);
|
|
32
|
+
if (this.shouldPrint(level)) {
|
|
33
|
+
let tab;
|
|
34
|
+
if (logEntry.tabId > 0) {
|
|
35
|
+
tab = `Tab ${logEntry.tabId}`;
|
|
36
|
+
} else {
|
|
37
|
+
tab = "worker";
|
|
38
|
+
}
|
|
39
|
+
console[level](
|
|
40
|
+
`[${logEntry.timestamp.toISOString()}] [${tab}] [${logEntry.origin}] ${logEntry.message}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Adds a log entry with the default level. */
|
|
45
|
+
log(message, details, stack) {
|
|
46
|
+
this.addLog("log", message, details.tabId, details.state.fqdn, stack);
|
|
47
|
+
}
|
|
48
|
+
/** Adds a warning log entry. */
|
|
49
|
+
warn(message, details, stack) {
|
|
50
|
+
this.addLog("warn", message, details.tabId, details.state.fqdn, stack);
|
|
51
|
+
}
|
|
52
|
+
/** Adds an error log entry. */
|
|
53
|
+
error(message, details, stack) {
|
|
54
|
+
this.addLog("error", message, details.tabId, details.state.fqdn, stack);
|
|
55
|
+
}
|
|
56
|
+
/** Adds an informational log entry. */
|
|
57
|
+
info(message, details, stack) {
|
|
58
|
+
this.addLog("info", message, details.tabId, details.state.fqdn, stack);
|
|
59
|
+
}
|
|
60
|
+
/** Add a debug log entry. */
|
|
61
|
+
debug(message, details, stack) {
|
|
62
|
+
this.addLog("debug", message, details.tabId, details.state.fqdn, stack);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Determine if a log should be printed based on the debug mode and log level.
|
|
66
|
+
* @param level - Log level to check.
|
|
67
|
+
*/
|
|
68
|
+
shouldPrint(level) {
|
|
69
|
+
if (level === "warn" || level === "error") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return this.debugMode;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get all log entries.
|
|
76
|
+
*/
|
|
77
|
+
getLogs() {
|
|
78
|
+
return [...globalLogs];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get logs filtered by level.
|
|
82
|
+
* @param level - Log level to filter by (debug, info, warn, error).
|
|
83
|
+
*/
|
|
84
|
+
getLogsByLevel(level) {
|
|
85
|
+
return globalLogs.filter((log) => log.level === level);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get logs filtered by tab ID.
|
|
89
|
+
* @param tabId - Tab ID to filter by.
|
|
90
|
+
*/
|
|
91
|
+
getLogsByTab(tabId) {
|
|
92
|
+
return globalLogs.filter((log) => log.tabId === tabId);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get logs filtered by origin.
|
|
96
|
+
* @param origin - Origin (e.g., URL or script) to filter by.
|
|
97
|
+
*/
|
|
98
|
+
getLogsByOrigin(origin) {
|
|
99
|
+
return globalLogs.filter((log) => log.origin === origin);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Clear all logs.
|
|
103
|
+
*/
|
|
104
|
+
clearLogs() {
|
|
105
|
+
globalLogs.length = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const logger = new Logger();
|
|
109
|
+
export {
|
|
110
|
+
globalLogs,
|
|
111
|
+
logger
|
|
112
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Bundle, Enrollment, Manifest, SigstoreSignatures, SigsumSignatures } from "./interfaces/bundle";
|
|
2
|
+
import { Database } from "./interfaces/database";
|
|
3
|
+
import { WebcatError } from "./interfaces/errors";
|
|
4
|
+
import { CachePartition, OriginState as IOriginState, OriginStateFailed, OriginStateObject, OriginStateVerifiedEnrollment, OriginStateVerifiedManifest } from "./interfaces/originstate";
|
|
5
|
+
type BundleFetch = {
|
|
6
|
+
promise: Promise<Response>;
|
|
7
|
+
error?: WebcatError;
|
|
8
|
+
value?: Bundle;
|
|
9
|
+
};
|
|
10
|
+
export type BundleFetcherConfig = {
|
|
11
|
+
/** The path to `bundle.json`. */
|
|
12
|
+
bundlePath: string;
|
|
13
|
+
/** The path to `bundle-prev.json`. */
|
|
14
|
+
bundlePrevPath: string;
|
|
15
|
+
};
|
|
16
|
+
/** @internal */
|
|
17
|
+
export declare class BundleFetcher {
|
|
18
|
+
readonly current: BundleFetch;
|
|
19
|
+
readonly previous: BundleFetch;
|
|
20
|
+
constructor(base: string, config: BundleFetcherConfig);
|
|
21
|
+
awaitAll(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare class OriginState implements IOriginState {
|
|
24
|
+
#private;
|
|
25
|
+
status: "request_sent" | "verified_enrollment" | "verified_manifest" | "failed";
|
|
26
|
+
readonly enrollment_hash: Uint8Array;
|
|
27
|
+
fetcher: BundleFetcher;
|
|
28
|
+
enrollment?: Enrollment;
|
|
29
|
+
manifest?: Manifest;
|
|
30
|
+
delegation?: string;
|
|
31
|
+
error?: WebcatError;
|
|
32
|
+
validUntil?: number;
|
|
33
|
+
stale: boolean;
|
|
34
|
+
constructor(db: Database, fetcher: BundleFetcher, enrollment_hash: Uint8Array, cachePartition: CachePartition, delegation?: string);
|
|
35
|
+
isRequestSent(): boolean;
|
|
36
|
+
isFailed(): this is OriginStateFailed;
|
|
37
|
+
isEnrollmentVerified(): this is OriginStateVerifiedEnrollment;
|
|
38
|
+
isManifestVerified(): this is OriginStateVerifiedManifest;
|
|
39
|
+
verifyEnrollment(enrollment?: Enrollment, delegation?: string): Promise<void>;
|
|
40
|
+
verifyManifest(manifest?: Manifest, signatures?: SigsumSignatures | SigstoreSignatures): Promise<void>;
|
|
41
|
+
verifyCSP(csp: string, pathname: string): boolean;
|
|
42
|
+
toPOJO(): OriginStateObject;
|
|
43
|
+
static fromPOJO(pojo: OriginStateObject): OriginState;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
9
|
+
var _db, _cachePartition, _bundle, _OriginState_instances, fail_fn, setVerifiedEnrollment_fn, setVerifiedManifest_fn, verifyDelegation_fn, enrollmentHash_fn;
|
|
10
|
+
import { canonicalize } from "./canonicalize.js";
|
|
11
|
+
import { stringToUint8Array } from "./encoding.js";
|
|
12
|
+
import { EnrollmentTypes } from "./interfaces/bundle.js";
|
|
13
|
+
import { WebcatError, WebcatErrorCode } from "./interfaces/errors.js";
|
|
14
|
+
import { arraysEqual, SHA256 } from "./utils.js";
|
|
15
|
+
import { validateSigsumEnrollment, validateSigstoreEnrollment, verifySigstoreManifest, verifySigsumManifest, validateManifest, validateCSP } from "./validators.js";
|
|
16
|
+
import "../browser/permissions.js";
|
|
17
|
+
import "@freedomofpress/sigstore-browser";
|
|
18
|
+
import "@freedomofpress/sigsum";
|
|
19
|
+
import "@freedomofpress/sigsum/dist/compiledPolicy";
|
|
20
|
+
import "@freedomofpress/sigsum/dist/crypto";
|
|
21
|
+
import "@freedomofpress/sigsum/dist/proof";
|
|
22
|
+
import "@freedomofpress/sigsum/dist/types";
|
|
23
|
+
import "./parsers.js";
|
|
24
|
+
class BundleFetcher {
|
|
25
|
+
constructor(base, config) {
|
|
26
|
+
this.current = {
|
|
27
|
+
promise: fetch(`${base}${config.bundlePath}`, {
|
|
28
|
+
cache: "no-store"
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
this.previous = {
|
|
32
|
+
promise: fetch(`${base}${config.bundlePrevPath}`, {
|
|
33
|
+
cache: "no-store"
|
|
34
|
+
})
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async awaitAll() {
|
|
38
|
+
for (const slot of [this.current, this.previous]) {
|
|
39
|
+
if (slot.value || slot.error) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await slot.promise;
|
|
45
|
+
} catch {
|
|
46
|
+
slot.error = new WebcatError(WebcatErrorCode.Fetch.FETCH_ERROR);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
slot.error = new WebcatError(WebcatErrorCode.Fetch.FETCH_ERROR);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let bundle;
|
|
54
|
+
try {
|
|
55
|
+
bundle = await response.json();
|
|
56
|
+
} catch {
|
|
57
|
+
slot.error = new WebcatError(WebcatErrorCode.Bundle.MALFORMED);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!bundle.enrollment) {
|
|
61
|
+
slot.error = new WebcatError(WebcatErrorCode.Bundle.ENROLLMENT_MISSING);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!bundle.manifest) {
|
|
65
|
+
slot.error = new WebcatError(WebcatErrorCode.Bundle.MANIFEST_MISSING);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!bundle.signatures) {
|
|
69
|
+
slot.error = new WebcatError(WebcatErrorCode.Bundle.SIGNATURES_MISSING);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
slot.value = bundle;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const _OriginState = class _OriginState {
|
|
77
|
+
// Due to list logic, we support only one app per domain, and that should be a privileged one
|
|
78
|
+
// But that is enforced in request.ts
|
|
79
|
+
constructor(db, fetcher, enrollment_hash, cachePartition, delegation) {
|
|
80
|
+
__privateAdd(this, _OriginState_instances);
|
|
81
|
+
__privateAdd(this, _db);
|
|
82
|
+
__privateAdd(this, _cachePartition);
|
|
83
|
+
__privateAdd(this, _bundle);
|
|
84
|
+
this.stale = false;
|
|
85
|
+
__privateSet(this, _db, db);
|
|
86
|
+
this.fetcher = fetcher;
|
|
87
|
+
this.enrollment_hash = enrollment_hash;
|
|
88
|
+
__privateSet(this, _cachePartition, cachePartition);
|
|
89
|
+
this.delegation = delegation;
|
|
90
|
+
this.status = "request_sent";
|
|
91
|
+
}
|
|
92
|
+
isRequestSent() {
|
|
93
|
+
return this.status === "request_sent" && this.enrollment === void 0 && this.manifest === void 0 && this.error === void 0;
|
|
94
|
+
}
|
|
95
|
+
isFailed() {
|
|
96
|
+
return this.status === "failed";
|
|
97
|
+
}
|
|
98
|
+
isEnrollmentVerified() {
|
|
99
|
+
return this.status === "verified_enrollment" && this.enrollment !== void 0;
|
|
100
|
+
}
|
|
101
|
+
isManifestVerified() {
|
|
102
|
+
return this.status === "verified_manifest" && this.manifest !== void 0 && this.validUntil !== void 0;
|
|
103
|
+
}
|
|
104
|
+
// This functiont ries to verify the enrollment information against the value in the local list
|
|
105
|
+
// It will also return errors while fetching the bundles (though they are generated in awaitBundles)
|
|
106
|
+
// and tell the next stages whether they should use the current or the previous bundle
|
|
107
|
+
async verifyEnrollment(enrollment, delegation) {
|
|
108
|
+
let verified_delegation;
|
|
109
|
+
if (delegation && await __privateMethod(this, _OriginState_instances, verifyDelegation_fn).call(this, delegation)) {
|
|
110
|
+
verified_delegation = delegation;
|
|
111
|
+
}
|
|
112
|
+
if (!enrollment) {
|
|
113
|
+
if (!this.fetcher.current.value) {
|
|
114
|
+
if (!this.fetcher.current.error) {
|
|
115
|
+
throw new Error("verifyEnrollment called before fetcher is ready");
|
|
116
|
+
}
|
|
117
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, this.fetcher.current.error);
|
|
118
|
+
}
|
|
119
|
+
enrollment = this.fetcher.current.value.enrollment;
|
|
120
|
+
}
|
|
121
|
+
const canonicalized_hash = await __privateMethod(this, _OriginState_instances, enrollmentHash_fn).call(this, enrollment);
|
|
122
|
+
const match = canonicalized_hash !== null && arraysEqual(this.enrollment_hash, canonicalized_hash);
|
|
123
|
+
if (match) {
|
|
124
|
+
__privateSet(this, _bundle, this.fetcher.current.value);
|
|
125
|
+
} else {
|
|
126
|
+
if (!this.fetcher.previous.value) {
|
|
127
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, new WebcatError(WebcatErrorCode.Enrollment.MISMATCH));
|
|
128
|
+
}
|
|
129
|
+
enrollment = this.fetcher.previous.value.enrollment;
|
|
130
|
+
const canonicalized_hash_prev = await __privateMethod(this, _OriginState_instances, enrollmentHash_fn).call(this, enrollment);
|
|
131
|
+
if (canonicalized_hash_prev === null || !arraysEqual(this.enrollment_hash, canonicalized_hash_prev)) {
|
|
132
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, new WebcatError(WebcatErrorCode.Enrollment.MISMATCH));
|
|
133
|
+
}
|
|
134
|
+
__privateSet(this, _bundle, this.fetcher.previous.value);
|
|
135
|
+
}
|
|
136
|
+
let err = null;
|
|
137
|
+
if (enrollment.type === EnrollmentTypes.Sigsum) {
|
|
138
|
+
err = validateSigsumEnrollment(enrollment);
|
|
139
|
+
} else if (enrollment.type === EnrollmentTypes.Sigstore) {
|
|
140
|
+
err = validateSigstoreEnrollment(enrollment);
|
|
141
|
+
} else {
|
|
142
|
+
err = new WebcatError(WebcatErrorCode.Enrollment.TYPE_INVALID);
|
|
143
|
+
}
|
|
144
|
+
if (err) {
|
|
145
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, err);
|
|
146
|
+
}
|
|
147
|
+
return __privateMethod(this, _OriginState_instances, setVerifiedEnrollment_fn).call(this, enrollment, verified_delegation);
|
|
148
|
+
}
|
|
149
|
+
async verifyManifest(manifest, signatures) {
|
|
150
|
+
if (!this.isEnrollmentVerified()) {
|
|
151
|
+
throw new Error("verifyManifest called before verifyEnrollment");
|
|
152
|
+
}
|
|
153
|
+
if (!manifest || !signatures) {
|
|
154
|
+
if (!__privateGet(this, _bundle)) {
|
|
155
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, new WebcatError(WebcatErrorCode.Bundle.MANIFEST_MISSING));
|
|
156
|
+
}
|
|
157
|
+
manifest = __privateGet(this, _bundle).manifest;
|
|
158
|
+
signatures = __privateGet(this, _bundle).signatures;
|
|
159
|
+
}
|
|
160
|
+
let verify_result;
|
|
161
|
+
switch (this.enrollment.type) {
|
|
162
|
+
case EnrollmentTypes.Sigsum:
|
|
163
|
+
verify_result = await verifySigsumManifest(
|
|
164
|
+
this.enrollment,
|
|
165
|
+
manifest,
|
|
166
|
+
signatures
|
|
167
|
+
);
|
|
168
|
+
break;
|
|
169
|
+
case EnrollmentTypes.Sigstore:
|
|
170
|
+
verify_result = await verifySigstoreManifest(
|
|
171
|
+
this.enrollment,
|
|
172
|
+
manifest,
|
|
173
|
+
signatures
|
|
174
|
+
);
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
verify_result = new WebcatError(
|
|
178
|
+
WebcatErrorCode.Enrollment.TYPE_INVALID
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (verify_result instanceof WebcatError) {
|
|
182
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, verify_result);
|
|
183
|
+
}
|
|
184
|
+
const format_error = validateManifest(manifest);
|
|
185
|
+
if (format_error) {
|
|
186
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, format_error);
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
validateCSP(manifest.default_csp);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, new WebcatError(WebcatErrorCode.Manifest.DEFAULT_CSP_INVALID, [
|
|
192
|
+
String(e)
|
|
193
|
+
]));
|
|
194
|
+
}
|
|
195
|
+
for (const path in manifest.extra_csp) {
|
|
196
|
+
if (manifest.extra_csp.hasOwnProperty(path)) {
|
|
197
|
+
const csp = manifest.extra_csp[path];
|
|
198
|
+
try {
|
|
199
|
+
validateCSP(csp);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, new WebcatError(WebcatErrorCode.Manifest.EXTRA_CSP_INVALID, [
|
|
202
|
+
String(e)
|
|
203
|
+
]));
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
return __privateMethod(this, _OriginState_instances, fail_fn).call(this, new WebcatError(WebcatErrorCode.Manifest.EXTRA_CSP_MALFORMED, [
|
|
207
|
+
String(path)
|
|
208
|
+
]));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return __privateMethod(this, _OriginState_instances, setVerifiedManifest_fn).call(this, manifest, verify_result);
|
|
212
|
+
}
|
|
213
|
+
verifyCSP(csp, pathname) {
|
|
214
|
+
if (!this.isManifestVerified()) {
|
|
215
|
+
throw new Error("verifyCSP called before verifyManifest");
|
|
216
|
+
}
|
|
217
|
+
csp = csp.split(",")[0];
|
|
218
|
+
const extraCSP = this.manifest.extra_csp || {};
|
|
219
|
+
const defaultCSP = this.manifest.default_csp;
|
|
220
|
+
const effectivePath = pathname === "/" ? this.manifest.default_index : pathname;
|
|
221
|
+
if (extraCSP[effectivePath]) {
|
|
222
|
+
return csp === extraCSP[effectivePath];
|
|
223
|
+
}
|
|
224
|
+
let bestMatch = null;
|
|
225
|
+
let bestMatchLength = 0;
|
|
226
|
+
for (const prefix in extraCSP) {
|
|
227
|
+
if (effectivePath.startsWith(prefix) && prefix.length > bestMatchLength) {
|
|
228
|
+
bestMatch = prefix;
|
|
229
|
+
bestMatchLength = prefix.length;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const correctCSP = bestMatch ? extraCSP[bestMatch] : defaultCSP;
|
|
233
|
+
return csp === correctCSP;
|
|
234
|
+
}
|
|
235
|
+
toPOJO() {
|
|
236
|
+
if (!this.isManifestVerified()) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"cannot serialize OriginState before verifying the manifest"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
status: "verified_manifest",
|
|
243
|
+
enrollment_hash: this.enrollment_hash.toBase64(),
|
|
244
|
+
manifest: this.manifest,
|
|
245
|
+
delegation: this.delegation,
|
|
246
|
+
validUntil: this.validUntil
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
static fromPOJO(pojo) {
|
|
250
|
+
if (pojo.status !== "verified_manifest") {
|
|
251
|
+
throw new Error("cannot deserialize an unverified OriginState");
|
|
252
|
+
}
|
|
253
|
+
const restored = new _OriginState(
|
|
254
|
+
{},
|
|
255
|
+
{},
|
|
256
|
+
Uint8Array.fromBase64(pojo.enrollment_hash),
|
|
257
|
+
{}
|
|
258
|
+
);
|
|
259
|
+
restored.status = pojo.status;
|
|
260
|
+
restored.manifest = pojo.manifest;
|
|
261
|
+
restored.delegation = pojo.delegation;
|
|
262
|
+
restored.validUntil = pojo.validUntil;
|
|
263
|
+
return restored;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
_db = new WeakMap();
|
|
267
|
+
_cachePartition = new WeakMap();
|
|
268
|
+
_bundle = new WeakMap();
|
|
269
|
+
_OriginState_instances = new WeakSet();
|
|
270
|
+
fail_fn = function(error) {
|
|
271
|
+
this.status = "failed";
|
|
272
|
+
this.error = error;
|
|
273
|
+
};
|
|
274
|
+
setVerifiedEnrollment_fn = function(enrollment, delegation) {
|
|
275
|
+
this.status = "verified_enrollment";
|
|
276
|
+
this.enrollment = enrollment;
|
|
277
|
+
this.delegation = delegation;
|
|
278
|
+
};
|
|
279
|
+
setVerifiedManifest_fn = function(manifest, validUntil) {
|
|
280
|
+
this.status = "verified_manifest";
|
|
281
|
+
this.manifest = manifest;
|
|
282
|
+
this.validUntil = validUntil;
|
|
283
|
+
};
|
|
284
|
+
verifyDelegation_fn = async function(delegation) {
|
|
285
|
+
const delegation_hash = await __privateGet(this, _db).getFQDNEnrollment(
|
|
286
|
+
delegation,
|
|
287
|
+
__privateGet(this, _cachePartition)
|
|
288
|
+
);
|
|
289
|
+
return delegation_hash && arraysEqual(delegation_hash, this.enrollment_hash);
|
|
290
|
+
};
|
|
291
|
+
enrollmentHash_fn = async function(enrollment) {
|
|
292
|
+
const canonicalized = canonicalize(enrollment);
|
|
293
|
+
if (canonicalized === null) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return new Uint8Array(await SHA256(stringToUint8Array(canonicalized)));
|
|
297
|
+
};
|
|
298
|
+
let OriginState = _OriginState;
|
|
299
|
+
export {
|
|
300
|
+
BundleFetcher,
|
|
301
|
+
OriginState
|
|
302
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a serialized Content Security Policy via [the spec][0].
|
|
3
|
+
*
|
|
4
|
+
* [0]: https://w3c.github.io/webappsec-csp/#parse-serialized-policy
|
|
5
|
+
*
|
|
6
|
+
* @param policy The serialized Content Security Policy to parse.
|
|
7
|
+
* @returns A Map of Content Security Policy directives.
|
|
8
|
+
* @example
|
|
9
|
+
* parseContentSecurityPolicy(
|
|
10
|
+
* "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example",
|
|
11
|
+
* );
|
|
12
|
+
* // => Map(4) {
|
|
13
|
+
* // "default-src" => ["'self'"],
|
|
14
|
+
* // "script-src" => ["'unsafe-eval'", "scripts.example"],
|
|
15
|
+
* // "object-src" => [],
|
|
16
|
+
* // "style-src" => ["styles.example"],
|
|
17
|
+
* // }
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseContentSecurityPolicy(policy: string): Map<string, string[]>;
|
|
20
|
+
export declare function extractHostname(key: string): string;
|
|
21
|
+
export declare function extractRawHash(hexValue: string): Uint8Array;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { hexToUint8Array } from "./encoding.js";
|
|
2
|
+
const ASCII_WHITESPACE_CHARS = " \n\f\r ";
|
|
3
|
+
const ASCII_WHITESPACE = RegExp(`[${ASCII_WHITESPACE_CHARS}]+`);
|
|
4
|
+
const ASCII_WHITESPACE_AT_START = RegExp(`^[${ASCII_WHITESPACE_CHARS}]+`);
|
|
5
|
+
const ASCII_WHITESPACE_AT_END = RegExp(`[${ASCII_WHITESPACE_CHARS}]+$`);
|
|
6
|
+
const ASCII = /^[\x00-\x7f]*$/;
|
|
7
|
+
function parseContentSecurityPolicy(policy) {
|
|
8
|
+
const result = /* @__PURE__ */ new Map();
|
|
9
|
+
for (let token of policy.split(";")) {
|
|
10
|
+
token = token.replace(ASCII_WHITESPACE_AT_START, "").replace(ASCII_WHITESPACE_AT_END, "");
|
|
11
|
+
if (!token || !ASCII.test(token)) continue;
|
|
12
|
+
const [rawDirectiveName, ...directiveValue] = token.split(ASCII_WHITESPACE);
|
|
13
|
+
const directiveName = rawDirectiveName.toLowerCase();
|
|
14
|
+
if (result.has(directiveName)) continue;
|
|
15
|
+
result.set(directiveName, directiveValue);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
function extractHostname(key) {
|
|
20
|
+
let host = key.replace(/^canonical\//, "");
|
|
21
|
+
host = host.replace(/^\./, "");
|
|
22
|
+
const labels = host.split(".");
|
|
23
|
+
return labels.reverse().join(".");
|
|
24
|
+
}
|
|
25
|
+
function extractRawHash(hexValue) {
|
|
26
|
+
const bytes = hexToUint8Array(hexValue);
|
|
27
|
+
if (bytes[0] !== 10) throw new Error("Unexpected ICS23 prefix");
|
|
28
|
+
const len = bytes[1];
|
|
29
|
+
const valueBytes = bytes.slice(2, 2 + len);
|
|
30
|
+
return valueBytes;
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
extractHostname,
|
|
34
|
+
extractRawHash,
|
|
35
|
+
parseContentSecurityPolicy
|
|
36
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BeforeRequestDetails } from "../browser/requests";
|
|
2
|
+
import { Database } from "./interfaces/database";
|
|
3
|
+
import { WebcatError } from "./interfaces/errors";
|
|
4
|
+
import { Stateful } from "./interfaces/requeststate";
|
|
5
|
+
import { BundleFetcherConfig } from "./originstate";
|
|
6
|
+
import { WebcatUI } from "./ui";
|
|
7
|
+
export declare function validateProtocolAndPort(urlobj: URL): boolean;
|
|
8
|
+
export declare function enforceHTTPS(urlobj: URL): string | undefined;
|
|
9
|
+
export declare function validateOrigin(db: Database, ui: WebcatUI, details: Stateful<BeforeRequestDetails>, config: BundleFetcherConfig): Promise<WebcatError | {
|
|
10
|
+
redirectUrl: string;
|
|
11
|
+
} | undefined>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { CacheKey } from "./cache.js";
|
|
2
|
+
import { WebcatError, WebcatErrorCode } from "./interfaces/errors.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { BundleFetcher, OriginState } from "./originstate.js";
|
|
5
|
+
import "../browser/sync.js";
|
|
6
|
+
import "./canonicalize.js";
|
|
7
|
+
import "./encoding.js";
|
|
8
|
+
import "./interfaces/bundle.js";
|
|
9
|
+
import "./utils.js";
|
|
10
|
+
import "../browser/permissions.js";
|
|
11
|
+
import "./validators.js";
|
|
12
|
+
import "@freedomofpress/sigstore-browser";
|
|
13
|
+
import "@freedomofpress/sigsum";
|
|
14
|
+
import "@freedomofpress/sigsum/dist/compiledPolicy";
|
|
15
|
+
import "@freedomofpress/sigsum/dist/crypto";
|
|
16
|
+
import "@freedomofpress/sigsum/dist/proof";
|
|
17
|
+
import "@freedomofpress/sigsum/dist/types";
|
|
18
|
+
import "./parsers.js";
|
|
19
|
+
const allowedPorts = ["80", "443", ""];
|
|
20
|
+
function validateProtocolAndPort(urlobj) {
|
|
21
|
+
if (!allowedPorts.includes(urlobj.port) || !["http:", "https:"].includes(urlobj.protocol)) {
|
|
22
|
+
return false;
|
|
23
|
+
} else {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function enforceHTTPS(urlobj) {
|
|
28
|
+
if (urlobj.protocol !== "https:" && urlobj.hostname.substring(urlobj.hostname.lastIndexOf(".")) !== ".onion") {
|
|
29
|
+
urlobj.protocol = "https:";
|
|
30
|
+
return urlobj.toString();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function validateOrigin(db, ui, details, config) {
|
|
34
|
+
const { fqdn, cachePartition, isFrame } = details.state;
|
|
35
|
+
const enrollment_hash = await db.getFQDNEnrollment(fqdn, cachePartition);
|
|
36
|
+
if (enrollment_hash.length === 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (isFrame) {
|
|
40
|
+
ui.showIcon(details.tabId);
|
|
41
|
+
}
|
|
42
|
+
const urlobj = new URL(details.url);
|
|
43
|
+
if (!validateProtocolAndPort(urlobj)) {
|
|
44
|
+
return new WebcatError(WebcatErrorCode.URL.UNSUPPORTED, [
|
|
45
|
+
String(urlobj.protocol),
|
|
46
|
+
String(urlobj.port || "default")
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
const redirect = enforceHTTPS(urlobj);
|
|
50
|
+
if (redirect) {
|
|
51
|
+
return { redirectUrl: redirect };
|
|
52
|
+
}
|
|
53
|
+
const cached = await db.origins.get(CacheKey(fqdn, cachePartition));
|
|
54
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
55
|
+
if (cached && cached.validUntil && cached.validUntil > now) {
|
|
56
|
+
details.state.pendingOrigin = cached;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
logger.info(`${fqdn} is enrolled, but we do not have metadata yet.`, details);
|
|
60
|
+
const newFetcher = new BundleFetcher(
|
|
61
|
+
`${urlobj.protocol}//${fqdn}:${urlobj.port}`,
|
|
62
|
+
config
|
|
63
|
+
);
|
|
64
|
+
const newOriginState = new OriginState(
|
|
65
|
+
db,
|
|
66
|
+
newFetcher,
|
|
67
|
+
enrollment_hash,
|
|
68
|
+
cachePartition
|
|
69
|
+
);
|
|
70
|
+
details.state.pendingOrigin = newOriginState;
|
|
71
|
+
await newOriginState.fetcher.awaitAll();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
enforceHTTPS,
|
|
76
|
+
validateOrigin,
|
|
77
|
+
validateProtocolAndPort
|
|
78
|
+
};
|