Add WasmModuleBackend, a ContentAnalysisBackend implementation that serializes requests to the content_analysis SDK protobuf, and hands them to the in-process WASM DLP module via nsIContentAnalysisWasmRunner. Gated behind a pref that is off (and will eventually be set by policy) Differential Revision: https://phabricator.services.mozilla.com/D311326
141 lines
4.8 KiB
JavaScript
141 lines
4.8 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* Parent-process driver for the in-process WebAssembly DLP module. Called by the
|
|
* C++ WasmModuleBackend, which passes a serialized ContentAnalysisRequest and
|
|
* expects a serialized ContentAnalysisResponse back.
|
|
*
|
|
* This runs in the parent process so it cannot compile or run the module -
|
|
* that work is done by the ContentAnalysisWasm actor.
|
|
*/
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
|
ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
|
|
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
|
|
});
|
|
|
|
// keep in sync with mozilla::contentanalysis::kWasmModuleExtensionId
|
|
const EXTENSION_ID = "dlp-wasm-provider@mozilla.org";
|
|
const REQUIRE_SIGNATURE_PREF =
|
|
"browser.contentanalysis.wasm_module_extension_require_signature";
|
|
|
|
// Name the module is expected to have inside an extension's package.
|
|
const EXTENSION_MODULE_FILENAME = "content_analysis_wasm.wasm";
|
|
|
|
const ACTOR_NAME = "ContentAnalysisWasm";
|
|
|
|
/**
|
|
* Read a file packaged inside an installed extension (identified by ID) as raw
|
|
* bytes. Also checks the signature if the pref is set.
|
|
*
|
|
* @param {string} extensionId The extension ID to load
|
|
* @param {string} path The path of the file to load from the extension
|
|
* @returns {{moduleBytes: Uint8Array, extensionVersion: string}}
|
|
*/
|
|
function readExtensionBytes(extensionId, path) {
|
|
const extension =
|
|
lazy.ExtensionParent.GlobalManager.getExtension(extensionId);
|
|
if (!extension) {
|
|
return null;
|
|
}
|
|
|
|
const requireSignature = Services.prefs.getBoolPref(
|
|
REQUIRE_SIGNATURE_PREF,
|
|
true
|
|
);
|
|
if (requireSignature) {
|
|
const signedState = extension.addonData?.signedState;
|
|
if (!(signedState >= lazy.AddonManager.SIGNEDSTATE_SYSTEM)) {
|
|
throw Components.Exception(
|
|
`DLP wasm extension '${extensionId}' is not acceptably signed ` +
|
|
`(signedState=${signedState}); refusing to load its module`,
|
|
Cr.NS_ERROR_INVALID_SIGNATURE
|
|
);
|
|
}
|
|
}
|
|
|
|
const channel = lazy.NetUtil.newChannel({
|
|
uri: extension.getURL(path),
|
|
loadUsingSystemPrincipal: true,
|
|
});
|
|
const stream = channel.open();
|
|
const bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
|
|
Ci.nsIBinaryInputStream
|
|
);
|
|
bstream.setInputStream(stream);
|
|
const bytes = bstream.readByteArray(stream.available());
|
|
bstream.close();
|
|
stream.close();
|
|
return {
|
|
moduleBytes: Uint8Array.from(bytes),
|
|
extensionVersion: extension.version,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* nsIContentAnalysisWasmRunner implementation. See the module comment above.
|
|
*/
|
|
export class ContentAnalysisWasmRunner {
|
|
QueryInterface = ChromeUtils.generateQI(["nsIContentAnalysisWasmRunner"]);
|
|
|
|
async analyze(aRequestBytes, aContentBytes, aRules) {
|
|
// Reading + signature verification stay in the parent (the trust decision):
|
|
// WebExtensionPolicy/AddonManager are parent-process only.
|
|
const extensionInfo = readExtensionBytes(
|
|
EXTENSION_ID,
|
|
EXTENSION_MODULE_FILENAME
|
|
);
|
|
if (!extensionInfo) {
|
|
throw Components.Exception(
|
|
`DLP WASM extension '${EXTENSION_ID}' is not installed or not enabled`,
|
|
Cr.NS_ERROR_NOT_AVAILABLE
|
|
);
|
|
}
|
|
const { moduleBytes, extensionVersion } = extensionInfo;
|
|
|
|
const actor = await this.#getActor();
|
|
// Resolves with a Uint8Array, which the C++ caller reads in bulk.
|
|
return actor.sendQuery("Analyze", {
|
|
version: extensionVersion,
|
|
moduleBytes,
|
|
requestBytes: Uint8Array.from(aRequestBytes),
|
|
contentBytes: Uint8Array.from(aContentBytes || []),
|
|
rules: toPlainRules(aRules),
|
|
});
|
|
}
|
|
|
|
async #getActor() {
|
|
// We use the privilegedabout process for this because:
|
|
// - it's essentially always running, so it won't add any overhead
|
|
// - it has the ability to compile and run WASM
|
|
const keepAlive = await ChromeUtils.ensureHeadlessContentProcess(
|
|
lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
|
|
);
|
|
if (!keepAlive?.domProcess?.canSend) {
|
|
throw Components.Exception(
|
|
"could not start a content process for the DLP wasm module",
|
|
Cr.NS_ERROR_NOT_AVAILABLE
|
|
);
|
|
}
|
|
return keepAlive.domProcess.getActor(ACTOR_NAME);
|
|
}
|
|
}
|
|
|
|
// Convert the C++-supplied nsIContentAnalysisRule objects into plain,
|
|
// structured-cloneable objects to ship to the content process.
|
|
function toPlainRules(aRules) {
|
|
return Array.from(aRules || [], rule => ({
|
|
name: rule.name,
|
|
operations: Array.from(rule.operations),
|
|
domains: Array.from(rule.domains),
|
|
contentPatterns: Array.from(rule.contentPatterns),
|
|
ruleType: rule.verdict,
|
|
message: rule.message,
|
|
}));
|
|
}
|