summaryrefslogtreecommitdiffstats
path: root/devtools/shared/security
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/shared/security
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--devtools/shared/security/auth.js642
-rw-r--r--devtools/shared/security/cert.js62
-rw-r--r--devtools/shared/security/moz.build15
-rw-r--r--devtools/shared/security/prompt.js203
-rw-r--r--devtools/shared/security/socket.js888
-rw-r--r--devtools/shared/security/tests/chrome/.eslintrc.js6
-rw-r--r--devtools/shared/security/tests/chrome/chrome.ini4
-rw-r--r--devtools/shared/security/tests/chrome/test_websocket-transport.html74
-rw-r--r--devtools/shared/security/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/security/tests/xpcshell/head_dbg.js95
-rw-r--r--devtools/shared/security/tests/xpcshell/test_encryption.js106
-rw-r--r--devtools/shared/security/tests/xpcshell/test_oob_cert_auth.js267
-rw-r--r--devtools/shared/security/tests/xpcshell/testactors.js16
-rw-r--r--devtools/shared/security/tests/xpcshell/xpcshell.ini11
14 files changed, 2395 insertions, 0 deletions
diff --git a/devtools/shared/security/auth.js b/devtools/shared/security/auth.js
new file mode 100644
index 0000000000..375cf6101f
--- /dev/null
+++ b/devtools/shared/security/auth.js
@@ -0,0 +1,642 @@
+/* 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/. */
+
+"use strict";
+
+var { Ci, Cc } = require("chrome");
+var Services = require("Services");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { dumpn, dumpv } = DevToolsUtils;
+loader.lazyRequireGetter(this, "prompt", "devtools/shared/security/prompt");
+loader.lazyRequireGetter(this, "cert", "devtools/shared/security/cert");
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
+
+/**
+ * A simple enum-like object with keys mirrored to values.
+ * This makes comparison to a specfic value simpler without having to repeat and
+ * mis-type the value.
+ */
+function createEnum(obj) {
+ for (const key in obj) {
+ obj[key] = key;
+ }
+ return obj;
+}
+
+/**
+ * |allowConnection| implementations can return various values as their |result|
+ * field to indicate what action to take. By specifying these, we can
+ * centralize the common actions available, while still allowing embedders to
+ * present their UI in whatever way they choose.
+ */
+var AuthenticationResult = (exports.AuthenticationResult = createEnum({
+ /**
+ * Close all listening sockets, and disable them from opening again.
+ */
+ DISABLE_ALL: null,
+
+ /**
+ * Deny the current connection.
+ */
+ DENY: null,
+
+ /**
+ * Additional data needs to be exchanged before a result can be determined.
+ */
+ PENDING: null,
+
+ /**
+ * Allow the current connection.
+ */
+ ALLOW: null,
+
+ /**
+ * Allow the current connection, and persist this choice for future
+ * connections from the same client. This requires a trustable mechanism to
+ * identify the client in the future, such as the cert used during OOB_CERT.
+ */
+ ALLOW_PERSIST: null,
+}));
+
+/**
+ * An |Authenticator| implements an authentication mechanism via various hooks
+ * in the client and server debugger socket connection path (see socket.js).
+ *
+ * |Authenticator|s are stateless objects. Each hook method is passed the state
+ * it needs by the client / server code in socket.js.
+ *
+ * Separate instances of the |Authenticator| are created for each use (client
+ * connection, server listener) in case some methods are customized by the
+ * embedder for a given use case.
+ */
+var Authenticators = {};
+
+/**
+ * The Prompt authenticator displays a server-side user prompt that includes
+ * connection details, and asks the user to verify the connection. There are
+ * no cryptographic properties at work here, so it is up to the user to be sure
+ * that the client can be trusted.
+ */
+var Prompt = (Authenticators.Prompt = {});
+
+Prompt.mode = "PROMPT";
+
+Prompt.Client = function() {};
+Prompt.Client.prototype = {
+ mode: Prompt.mode,
+
+ /**
+ * When client is about to make a new connection, verify that the connection settings
+ * are compatible with this authenticator.
+ * @throws if validation requirements are not met
+ */
+ validateSettings() {},
+
+ /**
+ * When client has just made a new socket connection, validate the connection
+ * to ensure it meets the authenticator's policies.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ * @return boolean
+ * Whether the connection is valid.
+ */
+ validateConnection() {
+ return true;
+ },
+
+ /**
+ * Work with the server to complete any additional steps required by this
+ * authenticator's policies.
+ *
+ * Debugging commences after this hook completes successfully.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param transport DebuggerTransport
+ * A transport that can be used to communicate with the server.
+ * @return A promise can be used if there is async behavior.
+ */
+ authenticate() {},
+};
+
+Prompt.Server = function() {};
+Prompt.Server.prototype = {
+ mode: Prompt.mode,
+
+ /**
+ * Verify that listener settings are appropriate for this authentication mode.
+ *
+ * @param listener SocketListener
+ * The socket listener about to be opened.
+ * @throws if validation requirements are not met
+ */
+ validateOptions() {},
+
+ /**
+ * Augment options on the listening socket about to be opened.
+ *
+ * @param listener SocketListener
+ * The socket listener about to be opened.
+ * @param socket nsIServerSocket
+ * The socket that is about to start listening.
+ */
+ augmentSocketOptions() {},
+
+ /**
+ * Augment the service discovery advertisement with any additional data needed
+ * to support this authentication mode.
+ *
+ * @param listener SocketListener
+ * The socket listener that was just opened.
+ * @param advertisement object
+ * The advertisement being built.
+ */
+ augmentAdvertisement(listener, advertisement) {
+ advertisement.authentication = Prompt.mode;
+ },
+
+ /**
+ * Determine whether a connection the server should be allowed or not based on
+ * this authenticator's policies.
+ *
+ * @param session object
+ * In PROMPT mode, the |session| includes:
+ * {
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * },
+ * transport
+ * }
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ authenticate({ client, server }) {
+ if (!Services.prefs.getBoolPref("devtools.debugger.prompt-connection")) {
+ return AuthenticationResult.ALLOW;
+ }
+ return this.allowConnection({
+ authentication: this.mode,
+ client,
+ server,
+ });
+ },
+
+ /**
+ * Prompt the user to accept or decline the incoming connection. The default
+ * implementation is used unless this is overridden on a particular
+ * authenticator instance.
+ *
+ * It is expected that the implementation of |allowConnection| will show a
+ * prompt to the user so that they can allow or deny the connection.
+ *
+ * @param session object
+ * In PROMPT mode, the |session| includes:
+ * {
+ * authentication: "PROMPT",
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * }
+ * }
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ allowConnection: prompt.Server.defaultAllowConnection,
+};
+
+/**
+ * The out-of-band (OOB) cert authenticator is based on self-signed X.509 certs
+ * at both the client and server end.
+ *
+ * The user is first prompted to verify the connection, similar to the prompt
+ * method above. This prompt may display cert fingerprints if desired.
+ *
+ * Assuming the user approves the connection, further UI is used to assist the
+ * user in tranferring out-of-band (OOB) verification of the client's
+ * certificate. For example, this could take the form of a QR code that the
+ * client displays which is then scanned by a camera on the server.
+ *
+ * Since it is assumed that an attacker can't forge the client's X.509 cert, the
+ * user may also choose to always allow a client, which would permit immediate
+ * connections in the future with no user interaction needed.
+ *
+ * See docs/wifi.md for details of the authentication design.
+ */
+var OOBCert = (Authenticators.OOBCert = {});
+
+OOBCert.mode = "OOB_CERT";
+
+OOBCert.Client = function() {};
+OOBCert.Client.prototype = {
+ mode: OOBCert.mode,
+
+ /**
+ * When client is about to make a new connection, verify that the connection settings
+ * are compatible with this authenticator.
+ * @throws if validation requirements are not met
+ */
+ validateSettings({ encryption }) {
+ if (!encryption) {
+ throw new Error(`${OOBCert.mode} authentication requires encryption.`);
+ }
+ },
+
+ /**
+ * When client has just made a new socket connection, validate the connection
+ * to ensure it meets the authenticator's policies.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param socket nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ * @return boolean
+ * Whether the connection is valid.
+ */
+ // eslint-disable-next-line no-shadow
+ validateConnection({ cert, socket }) {
+ // Step B.7
+ // Client verifies that Server's cert matches hash(ServerCert) from the
+ // advertisement
+ dumpv("Validate server cert hash");
+ const serverCert = socket.securityInfo.QueryInterface(
+ Ci.nsITransportSecurityInfo
+ ).serverCert;
+ const advertisedCert = cert;
+ if (serverCert.sha256Fingerprint != advertisedCert.sha256) {
+ dumpn("Server cert hash doesn't match advertisement");
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Work with the server to complete any additional steps required by this
+ * authenticator's policies.
+ *
+ * Debugging commences after this hook completes successfully.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param transport DebuggerTransport
+ * A transport that can be used to communicate with the server.
+ * @return A promise can be used if there is async behavior.
+ */
+ // eslint-disable-next-line no-shadow
+ authenticate({ host, port, cert, transport }) {
+ return new Promise((resolve, reject) => {
+ let oobData;
+
+ let activeSendDialog;
+ const closeDialog = () => {
+ // Close any prompts the client may have been showing from previous
+ // authentication steps
+ if (activeSendDialog?.close) {
+ activeSendDialog.close();
+ activeSendDialog = null;
+ }
+ };
+
+ transport.hooks = {
+ onPacket: async packet => {
+ closeDialog();
+ const { authResult } = packet;
+ switch (authResult) {
+ case AuthenticationResult.PENDING:
+ // Step B.8
+ // Client creates hash(ClientCert) + K(random 128-bit number)
+ oobData = await this._createOOB();
+ activeSendDialog = this.sendOOB({
+ host,
+ port,
+ cert,
+ authResult,
+ oob: oobData,
+ });
+ break;
+ case AuthenticationResult.ALLOW:
+ // Step B.12
+ // Client verifies received value matches K
+ if (packet.k != oobData.k) {
+ transport.close(new Error("Auth secret mismatch"));
+ return;
+ }
+ // Step B.13
+ // Debugging begins
+ transport.hooks = null;
+ resolve(transport);
+ break;
+ case AuthenticationResult.ALLOW_PERSIST:
+ // Server previously persisted Client as allowed
+ // Step C.5
+ // Debugging begins
+ transport.hooks = null;
+ resolve(transport);
+ break;
+ default:
+ transport.close(new Error("Invalid auth result: " + authResult));
+ break;
+ }
+ },
+ onClosed(reason) {
+ closeDialog();
+ // Transport died before auth completed
+ transport.hooks = null;
+ reject(reason);
+ },
+ };
+ transport.ready();
+ });
+ },
+
+ /**
+ * Create the package of data that needs to be transferred across the OOB
+ * channel.
+ */
+ async _createOOB() {
+ const clientCert = await cert.local.getOrCreate();
+ return {
+ sha256: clientCert.sha256Fingerprint,
+ k: this._createRandom(),
+ };
+ },
+
+ _createRandom() {
+ // 16 bytes / 128 bits
+ const length = 16;
+ const rng = Cc["@mozilla.org/security/random-generator;1"].createInstance(
+ Ci.nsIRandomGenerator
+ );
+ const bytes = rng.generateRandomBytes(length);
+ return bytes.map(byte => byte.toString(16)).join("");
+ },
+
+ /**
+ * Send data across the OOB channel to the server to authenticate the devices.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param authResult AuthenticationResult
+ * Authentication result sent from the server.
+ * @param oob object (optional)
+ * The token data to be transferred during OOB_CERT step 8:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * @return object containing:
+ * * close: Function to hide the notification
+ */
+ sendOOB: prompt.Client.defaultSendOOB,
+};
+
+OOBCert.Server = function() {};
+OOBCert.Server.prototype = {
+ mode: OOBCert.mode,
+
+ /**
+ * Verify that listener settings are appropriate for this authentication mode.
+ *
+ * @param listener SocketListener
+ * The socket listener about to be opened.
+ * @throws if validation requirements are not met
+ */
+ validateOptions(listener) {
+ if (!listener.encryption) {
+ throw new Error(OOBCert.mode + " authentication requires encryption.");
+ }
+ },
+
+ /**
+ * Augment options on the listening socket about to be opened.
+ *
+ * @param listener SocketListener
+ * The socket listener about to be opened.
+ * @param socket nsIServerSocket
+ * The socket that is about to start listening.
+ */
+ augmentSocketOptions(listener, socket) {
+ const requestCert = Ci.nsITLSServerSocket.REQUIRE_ALWAYS;
+ socket.setRequestClientCertificate(requestCert);
+ },
+
+ /**
+ * Augment the service discovery advertisement with any additional data needed
+ * to support this authentication mode.
+ *
+ * @param listener SocketListener
+ * The socket listener that was just opened.
+ * @param advertisement object
+ * The advertisement being built.
+ */
+ augmentAdvertisement(listener, advertisement) {
+ advertisement.authentication = OOBCert.mode;
+ // Step A.4
+ // Server announces itself via service discovery
+ // Announcement contains hash(ServerCert) as additional data
+ advertisement.cert = listener.cert;
+ },
+
+ /**
+ * Determine whether a connection the server should be allowed or not based on
+ * this authenticator's policies.
+ *
+ * @param session object
+ * In OOB_CERT mode, the |session| includes:
+ * {
+ * client: {
+ * host,
+ * port,
+ * cert: {
+ * sha256
+ * },
+ * },
+ * server: {
+ * host,
+ * port,
+ * cert: {
+ * sha256
+ * }
+ * },
+ * transport
+ * }
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ async authenticate({ client, server, transport }) {
+ // Step B.3 / C.3
+ // TLS connection established, authentication begins
+ const storageKey = `devtools.auth.${this.mode}.approved-clients`;
+ const approvedClients = (await asyncStorage.getItem(storageKey)) || {};
+ // Step C.4
+ // Server sees that ClientCert is from a known client via hash(ClientCert)
+ if (approvedClients[client.cert.sha256]) {
+ const authResult = AuthenticationResult.ALLOW_PERSIST;
+ transport.send({ authResult });
+ // Step C.5
+ // Debugging begins
+ return authResult;
+ }
+
+ // Step B.4
+ // Server sees that ClientCert is from a unknown client
+ // Tell client they are unknown and should display OOB client UX
+ transport.send({
+ authResult: AuthenticationResult.PENDING,
+ });
+
+ // Step B.5
+ // User is shown a Allow / Deny / Always Allow prompt on the Server
+ // with Client name and hash(ClientCert)
+ const authResult = await this.allowConnection({
+ authentication: this.mode,
+ client,
+ server,
+ });
+
+ switch (authResult) {
+ case AuthenticationResult.ALLOW_PERSIST:
+ case AuthenticationResult.ALLOW:
+ // Further processing
+ break;
+ default:
+ // Abort for any negative results
+ return authResult;
+ }
+
+ // Examine additional data for authentication
+ const oob = await this.receiveOOB();
+ if (!oob) {
+ dumpn("Invalid OOB data received");
+ return AuthenticationResult.DENY;
+ }
+
+ const { sha256, k } = oob;
+ // The OOB auth prompt should have transferred:
+ // hash(ClientCert) + K(random 128-bit number)
+ // from the client.
+ if (!sha256 || !k) {
+ dumpn("Invalid OOB data received");
+ return AuthenticationResult.DENY;
+ }
+
+ // Step B.10
+ // Server verifies that Client's cert matches hash(ClientCert) from
+ // out-of-band channel
+ if (client.cert.sha256 != sha256) {
+ dumpn("Client cert hash doesn't match OOB data");
+ return AuthenticationResult.DENY;
+ }
+
+ // Step B.11
+ // Server sends K to Client over TLS connection
+ transport.send({ authResult, k });
+
+ // Persist Client if we want to always allow in the future
+ if (authResult === AuthenticationResult.ALLOW_PERSIST) {
+ approvedClients[client.cert.sha256] = true;
+ await asyncStorage.setItem(storageKey, approvedClients);
+ }
+
+ // Client may decide to abort if K does not match.
+ // Server's portion of authentication is now complete.
+
+ // Step B.13
+ // Debugging begins
+ return authResult;
+ },
+
+ /**
+ * Prompt the user to accept or decline the incoming connection. The default
+ * implementation is used unless this is overridden on a particular
+ * authenticator instance.
+ *
+ * It is expected that the implementation of |allowConnection| will show a
+ * prompt to the user so that they can allow or deny the connection.
+ *
+ * @param session object
+ * In OOB_CERT mode, the |session| includes:
+ * {
+ * authentication: "OOB_CERT",
+ * client: {
+ * host,
+ * port,
+ * cert: {
+ * sha256
+ * },
+ * },
+ * server: {
+ * host,
+ * port,
+ * cert: {
+ * sha256
+ * }
+ * }
+ * }
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ allowConnection: prompt.Server.defaultAllowConnection,
+
+ /**
+ * The user must transfer some data through some out of band mechanism from
+ * the client to the server to authenticate the devices.
+ *
+ * @return An object containing:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * A promise that will be resolved to the above is also allowed.
+ */
+ receiveOOB: prompt.Server.defaultReceiveOOB,
+};
+
+exports.Authenticators = {
+ get(mode) {
+ if (!mode) {
+ mode = Prompt.mode;
+ }
+ for (const key in Authenticators) {
+ const auth = Authenticators[key];
+ if (auth.mode === mode) {
+ return auth;
+ }
+ }
+ throw new Error("Unknown authenticator mode: " + mode);
+ },
+};
diff --git a/devtools/shared/security/cert.js b/devtools/shared/security/cert.js
new file mode 100644
index 0000000000..24e1d1012b
--- /dev/null
+++ b/devtools/shared/security/cert.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+"use strict";
+
+var { Ci, Cc } = require("chrome");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+DevToolsUtils.defineLazyGetter(this, "localCertService", () => {
+ // Ensure PSM is initialized to support TLS sockets
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+ return Cc["@mozilla.org/security/local-cert-service;1"].getService(
+ Ci.nsILocalCertService
+ );
+});
+
+const localCertName = "devtools";
+
+exports.local = {
+ /**
+ * Get or create a new self-signed X.509 cert to represent this device for
+ * DevTools purposes over a secure transport, like TLS.
+ *
+ * The cert is stored permanently in the profile's key store after first use,
+ * and is valid for 1 year. If an expired or otherwise invalid cert is found,
+ * it is removed and a new one is made.
+ *
+ * @return promise
+ */
+ getOrCreate() {
+ return new Promise((resolve, reject) => {
+ localCertService.getOrCreateCert(localCertName, {
+ handleCert: function(cert, rv) {
+ if (rv) {
+ reject(rv);
+ return;
+ }
+ resolve(cert);
+ },
+ });
+ });
+ },
+
+ /**
+ * Remove the DevTools self-signed X.509 cert for this device.
+ *
+ * @return promise
+ */
+ remove() {
+ return new Promise((resolve, reject) => {
+ localCertService.removeCert(localCertName, {
+ handleCert: function(rv) {
+ if (rv) {
+ reject(rv);
+ return;
+ }
+ resolve();
+ },
+ });
+ });
+ },
+};
diff --git a/devtools/shared/security/moz.build b/devtools/shared/security/moz.build
new file mode 100644
index 0000000000..5c364d7efa
--- /dev/null
+++ b/devtools/shared/security/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+
+DevToolsModules(
+ "auth.js",
+ "cert.js",
+ "prompt.js",
+ "socket.js",
+)
diff --git a/devtools/shared/security/prompt.js b/devtools/shared/security/prompt.js
new file mode 100644
index 0000000000..e46311ed4e
--- /dev/null
+++ b/devtools/shared/security/prompt.js
@@ -0,0 +1,203 @@
+/* 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/. */
+
+"use strict";
+
+var Services = require("Services");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+loader.lazyRequireGetter(
+ this,
+ "AuthenticationResult",
+ "devtools/shared/security/auth",
+ true
+);
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+ "devtools/shared/locales/debugger.properties"
+);
+
+var Client = (exports.Client = {});
+var Server = (exports.Server = {});
+
+/**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually. For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param authResult AuthenticationResult
+ * Authentication result sent from the server.
+ * @param oob object (optional)
+ * The token data to be transferred during OOB_CERT step 8:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * @return object containing:
+ * * close: Function to hide the notification
+ */
+Client.defaultSendOOB = ({ authResult, oob }) => {
+ // Only show in the PENDING state
+ if (authResult != AuthenticationResult.PENDING) {
+ throw new Error("Expected PENDING result, got " + authResult);
+ }
+ const title = L10N.getStr("clientSendOOBTitle");
+ const header = L10N.getStr("clientSendOOBHeader");
+ const hashMsg = L10N.getFormatStr("clientSendOOBHash", oob.sha256);
+ const token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
+ const tokenMsg = L10N.getFormatStr("clientSendOOBToken", token);
+ const msg = `${header}\n\n${hashMsg}\n${tokenMsg}`;
+ const prompt = Services.prompt;
+ const flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_CANCEL;
+
+ // Listen for the window our prompt opens, so we can close it programatically
+ let promptWindow;
+ const windowListener = {
+ onOpenWindow(xulWindow) {
+ const win = xulWindow.docShell.domWindow;
+ win.addEventListener(
+ "load",
+ function() {
+ if (
+ win.document.documentElement.getAttribute("id") != "commonDialog"
+ ) {
+ return;
+ }
+ // Found the window
+ promptWindow = win;
+ Services.wm.removeListener(windowListener);
+ },
+ { once: true }
+ );
+ },
+ onCloseWindow() {},
+ };
+ Services.wm.addListener(windowListener);
+
+ // nsIPrompt is typically a blocking API, so |executeSoon| to get around this
+ DevToolsUtils.executeSoon(() => {
+ prompt.confirmEx(null, title, msg, flags, null, null, null, null, {
+ value: false,
+ });
+ });
+
+ return {
+ close() {
+ if (!promptWindow) {
+ return;
+ }
+ promptWindow.document.documentElement.acceptDialog();
+ promptWindow = null;
+ },
+ };
+};
+
+/**
+ * Prompt the user to accept or decline the incoming connection. This is the
+ * default implementation that products embedding the devtools server may
+ * choose to override. This can be overridden via |allowConnection| on the
+ * socket's authenticator instance.
+ *
+ * @param session object
+ * The session object will contain at least the following fields:
+ * {
+ * authentication,
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * }
+ * }
+ * Specific authentication modes may include additional fields. Check
+ * the different |allowConnection| methods in ./auth.js.
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+Server.defaultAllowConnection = ({ client, server }) => {
+ const title = L10N.getStr("remoteIncomingPromptTitle");
+ const header = L10N.getStr("remoteIncomingPromptHeader");
+ const clientEndpoint = `${client.host}:${client.port}`;
+ const clientMsg = L10N.getFormatStr(
+ "remoteIncomingPromptClientEndpoint",
+ clientEndpoint
+ );
+ const serverEndpoint = `${server.host}:${server.port}`;
+ const serverMsg = L10N.getFormatStr(
+ "remoteIncomingPromptServerEndpoint",
+ serverEndpoint
+ );
+ const footer = L10N.getStr("remoteIncomingPromptFooter");
+ const msg = `${header}\n\n${clientMsg}\n${serverMsg}\n\n${footer}`;
+ const disableButton = L10N.getStr("remoteIncomingPromptDisable");
+ const prompt = Services.prompt;
+ const flags =
+ prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK +
+ prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL +
+ prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING +
+ prompt.BUTTON_POS_1_DEFAULT;
+ const result = prompt.confirmEx(
+ null,
+ title,
+ msg,
+ flags,
+ null,
+ null,
+ disableButton,
+ null,
+ { value: false }
+ );
+ if (result === 0) {
+ return AuthenticationResult.ALLOW;
+ }
+ if (result === 2) {
+ return AuthenticationResult.DISABLE_ALL;
+ }
+ return AuthenticationResult.DENY;
+};
+
+/**
+ * During OOB_CERT authentication, the user must transfer some data through some
+ * out of band mechanism from the client to the server to authenticate the
+ * devices.
+ *
+ * This implementation prompts the user for a token as constructed by
+ * |Client.defaultSendOOB| that the user needs to transfer manually. For a
+ * mobile device, you should override this implementation with something more
+ * convenient, such as reading a QR code.
+ *
+ * @return An object containing:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * A promise that will be resolved to the above is also allowed.
+ */
+Server.defaultReceiveOOB = () => {
+ const title = L10N.getStr("serverReceiveOOBTitle");
+ const msg = L10N.getStr("serverReceiveOOBBody");
+ let input = { value: null };
+ const prompt = Services.prompt;
+ const result = prompt.prompt(null, title, msg, input, null, { value: false });
+ if (!result) {
+ return null;
+ }
+ // Re-create original object from token
+ input = input.value.trim();
+ let sha256 = input.substring(0, 64);
+ sha256 = sha256
+ .replace(/\w{2}/g, "$&:")
+ .slice(0, -1)
+ .toUpperCase();
+ const k = input.substring(64);
+ return { sha256, k };
+};
diff --git a/devtools/shared/security/socket.js b/devtools/shared/security/socket.js
new file mode 100644
index 0000000000..52c6e5249a
--- /dev/null
+++ b/devtools/shared/security/socket.js
@@ -0,0 +1,888 @@
+/* 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/. */
+
+"use strict";
+
+var { Ci, Cc, CC, Cr } = require("chrome");
+
+// Ensure PSM is initialized to support TLS sockets
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+var Services = require("Services");
+var promise = require("promise");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { dumpn, dumpv } = DevToolsUtils;
+loader.lazyRequireGetter(
+ this,
+ "WebSocketServer",
+ "devtools/server/socket/websocket-server"
+);
+loader.lazyRequireGetter(
+ this,
+ "DebuggerTransport",
+ "devtools/shared/transport/transport",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WebSocketDebuggerTransport",
+ "devtools/shared/transport/websocket-transport"
+);
+loader.lazyRequireGetter(
+ this,
+ "discovery",
+ "devtools/shared/discovery/discovery"
+);
+loader.lazyRequireGetter(this, "cert", "devtools/shared/security/cert");
+loader.lazyRequireGetter(
+ this,
+ "Authenticators",
+ "devtools/shared/security/auth",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "AuthenticationResult",
+ "devtools/shared/security/auth",
+ true
+);
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
+ return CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
+});
+
+DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
+ return Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+});
+
+DevToolsUtils.defineLazyGetter(this, "certOverrideService", () => {
+ return Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+ );
+});
+
+DevToolsUtils.defineLazyGetter(this, "nssErrorsService", () => {
+ return Cc["@mozilla.org/nss_errors_service;1"].getService(
+ Ci.nsINSSErrorsService
+ );
+});
+
+var DebuggerSocket = {};
+
+/**
+ * Connects to a devtools server socket.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param webSocket boolean (optional)
+ * Whether to use WebSocket protocol to connect. Defaults to false.
+ * @param authenticator Authenticator (optional)
+ * |Authenticator| instance matching the mode in use by the server.
+ * Defaults to a PROMPT instance if not supplied.
+ * @param cert object (optional)
+ * The server's cert details. Used with OOB_CERT authentication.
+ * @return promise
+ * Resolved to a DebuggerTransport instance.
+ */
+DebuggerSocket.connect = async function(settings) {
+ // Default to PROMPT |Authenticator| instance if not supplied
+ if (!settings.authenticator) {
+ settings.authenticator = new (Authenticators.get().Client)();
+ }
+ _validateSettings(settings);
+ // eslint-disable-next-line no-shadow
+ const { host, port, encryption, authenticator, cert } = settings;
+ const transport = await _getTransport(settings);
+ await authenticator.authenticate({
+ host,
+ port,
+ encryption,
+ cert,
+ transport,
+ });
+ transport.connectionSettings = settings;
+ return transport;
+};
+
+/**
+ * Validate that the connection settings have been set to a supported configuration.
+ */
+function _validateSettings(settings) {
+ const { encryption, webSocket, authenticator } = settings;
+
+ if (webSocket && encryption) {
+ throw new Error("Encryption not supported on WebSocket transport");
+ }
+ authenticator.validateSettings(settings);
+}
+
+/**
+ * Try very hard to create a DevTools transport, potentially making several
+ * connect attempts in the process.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param webSocket boolean (optional)
+ * Whether to use WebSocket protocol to connect to the server. Defaults to false.
+ * @param authenticator Authenticator
+ * |Authenticator| instance matching the mode in use by the server.
+ * Defaults to a PROMPT instance if not supplied.
+ * @param cert object (optional)
+ * The server's cert details. Used with OOB_CERT authentication.
+ * @return transport DebuggerTransport
+ * A possible DevTools transport (if connection succeeded and streams
+ * are actually alive and working)
+ */
+var _getTransport = async function(settings) {
+ const { host, port, encryption, webSocket } = settings;
+
+ if (webSocket) {
+ // Establish a connection and wait until the WebSocket is ready to send and receive
+ const socket = await new Promise((resolve, reject) => {
+ const s = new WebSocket(`ws://${host}:${port}`);
+ s.onopen = () => resolve(s);
+ s.onerror = err => reject(err);
+ });
+
+ return new WebSocketDebuggerTransport(socket);
+ }
+
+ let attempt = await _attemptTransport(settings);
+ if (attempt.transport) {
+ // Success
+ return attempt.transport;
+ }
+
+ // If the server cert failed validation, store a temporary override and make
+ // a second attempt.
+ if (encryption && attempt.certError) {
+ _storeCertOverride(attempt.s, host, port);
+ } else {
+ throw new Error("Connection failed");
+ }
+
+ attempt = await _attemptTransport(settings);
+ if (attempt.transport) {
+ // Success
+ return attempt.transport;
+ }
+
+ throw new Error("Connection failed even after cert override");
+};
+
+/**
+ * Make a single attempt to connect and create a DevTools transport. This could
+ * fail if the remote host is unreachable, for example. If there is security
+ * error due to the use of self-signed certs, you should make another attempt
+ * after storing a cert override.
+ *
+ * @param host string
+ * The host name or IP address of the devtools server.
+ * @param port number
+ * The port number of the devtools server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @param authenticator Authenticator
+ * |Authenticator| instance matching the mode in use by the server.
+ * Defaults to a PROMPT instance if not supplied.
+ * @param cert object (optional)
+ * The server's cert details. Used with OOB_CERT authentication.
+ * @return transport DebuggerTransport
+ * A possible DevTools transport (if connection succeeded and streams
+ * are actually alive and working)
+ * @return certError boolean
+ * Flag noting if cert trouble caused the streams to fail
+ * @return s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ */
+var _attemptTransport = async function(settings) {
+ const { authenticator } = settings;
+ // _attemptConnect only opens the streams. Any failures at that stage
+ // aborts the connection process immedidately.
+ const { s, input, output } = await _attemptConnect(settings);
+
+ // Check if the input stream is alive. If encryption is enabled, we need to
+ // watch out for cert errors by testing the input stream.
+ let alive, certError;
+ try {
+ const results = await _isInputAlive(input);
+ alive = results.alive;
+ certError = results.certError;
+ } catch (e) {
+ // For other unexpected errors, like NS_ERROR_CONNECTION_REFUSED, we reach
+ // this block.
+ input.close();
+ output.close();
+ throw e;
+ }
+ dumpv("Server cert accepted? " + !certError);
+
+ // The |Authenticator| examines the connection as well and may determine it
+ // should be dropped.
+ alive =
+ alive &&
+ authenticator.validateConnection({
+ host: settings.host,
+ port: settings.port,
+ encryption: settings.encryption,
+ cert: settings.cert,
+ socket: s,
+ });
+
+ let transport;
+ if (alive) {
+ transport = new DebuggerTransport(input, output);
+ } else {
+ // Something went wrong, close the streams.
+ input.close();
+ output.close();
+ }
+
+ return { transport, certError, s };
+};
+
+/**
+ * Try to connect to a remote server socket.
+ *
+ * If successsful, the socket transport and its opened streams are returned.
+ * Typically, this will only fail if the host / port is unreachable. Other
+ * problems, such as security errors, will allow this stage to succeed, but then
+ * fail later when the streams are actually used.
+ * @return s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ * @return input nsIAsyncInputStream
+ * The socket's input stream.
+ * @return output nsIAsyncOutputStream
+ * The socket's output stream.
+ */
+var _attemptConnect = async function({ host, port, encryption }) {
+ let s;
+ if (encryption) {
+ s = socketTransportService.createTransport(["ssl"], host, port, null);
+ } else {
+ s = socketTransportService.createTransport([], host, port, null);
+ }
+
+ // Force disabling IPV6 if we aren't explicitely connecting to an IPv6 address
+ // It fails intermitently on MacOS when opening the Browser Toolbox (bug 1615412)
+ if (!host.includes(":")) {
+ s.connectionFlags |= Ci.nsISocketTransport.DISABLE_IPV6;
+ }
+
+ // By default the CONNECT socket timeout is very long, 65535 seconds,
+ // so that if we race to be in CONNECT state while the server socket is still
+ // initializing, the connection is stuck in connecting state for 18.20 hours!
+ s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
+
+ // If encrypting, load the client cert now, so we can deliver it at just the
+ // right time.
+ let clientCert;
+ if (encryption) {
+ clientCert = await cert.local.getOrCreate();
+ }
+
+ let input;
+ let output;
+ return new Promise((resolve, reject) => {
+ // Delay opening the input stream until the transport has fully connected.
+ // The goal is to avoid showing the user a client cert UI prompt when
+ // encryption is used. This prompt is shown when the client opens the input
+ // stream and does not know which client cert to present to the server. To
+ // specify a client cert programmatically, we need to access the transport's
+ // nsISSLSocketControl interface, which is not accessible until the transport
+ // has connected.
+ s.setEventSink(
+ {
+ onTransportStatus(transport, status) {
+ if (status != Ci.nsISocketTransport.STATUS_CONNECTING_TO) {
+ return;
+ }
+ if (encryption) {
+ const sslSocketControl = transport.securityInfo.QueryInterface(
+ Ci.nsISSLSocketControl
+ );
+ sslSocketControl.clientCert = clientCert;
+ }
+ try {
+ input = s.openInputStream(0, 0, 0);
+ } catch (e) {
+ reject(e);
+ }
+ resolve({ s, input, output });
+ },
+ },
+ Services.tm.currentThread
+ );
+
+ // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
+ // where the nsISocketTransport gets shutdown in between its instantiation and
+ // the call to this method.
+ try {
+ output = s.openOutputStream(0, 0, 0);
+ } catch (e) {
+ reject(e);
+ }
+ }).catch(e => {
+ if (input) {
+ input.close();
+ }
+ if (output) {
+ output.close();
+ }
+ DevToolsUtils.reportException("_attemptConnect", e);
+ });
+};
+
+/**
+ * Check if the input stream is alive. For an encrypted connection, it may not
+ * be if the client refuses the server's cert. A cert error is expected on
+ * first connection to a new host because the cert is self-signed.
+ */
+function _isInputAlive(input) {
+ return new Promise((resolve, reject) => {
+ input.asyncWait(
+ {
+ onInputStreamReady(stream) {
+ try {
+ stream.available();
+ resolve({ alive: true });
+ } catch (e) {
+ try {
+ // getErrorClass may throw if you pass a non-NSS error
+ const errorClass = nssErrorsService.getErrorClass(e.result);
+ if (errorClass === Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ resolve({ certError: true });
+ } else {
+ reject(e);
+ }
+ } catch (nssErr) {
+ reject(e);
+ }
+ }
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ });
+}
+
+/**
+ * To allow the connection to proceed with self-signed cert, we store a cert
+ * override. This implies that we take on the burden of authentication for
+ * these connections.
+ */
+function _storeCertOverride(s, host, port) {
+ // eslint-disable-next-line no-shadow
+ const cert = s.securityInfo.QueryInterface(Ci.nsITransportSecurityInfo)
+ .serverCert;
+ const overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ host,
+ port,
+ cert,
+ overrideBits,
+ true /* temporary */
+ );
+}
+
+/**
+ * Creates a new socket listener for remote connections to the DevToolsServer.
+ * This helps contain and organize the parts of the server that may differ or
+ * are particular to one given listener mechanism vs. another.
+ * This can be closed at any later time by calling |close|.
+ * If remote connections are disabled, an error is thrown.
+ *
+ * @param {DevToolsServer} devToolsServer
+ * @param {Object} socketOptions
+ * options of socket as follows
+ * {
+ * authenticator:
+ * Controls the |Authenticator| used, which hooks various socket steps to
+ * implement an authentication policy. It is expected that different use
+ * cases may override pieces of the |Authenticator|. See auth.js.
+ * We set the default |Authenticator|, which is |Prompt|.
+ * discoverable:
+ * Controls whether this listener is announced via the service discovery
+ * mechanism. Defaults is false.
+ * encryption:
+ * Controls whether this listener's transport uses encryption.
+ * Defaults is false.
+ * portOrPath:
+ * The port or path to listen on.
+ * If given an integer, the port to listen on. Use -1 to choose any available
+ * port. Otherwise, the path to the unix socket domain file to listen on.
+ * Defaults is null.
+ * webSocket:
+ * Whether to use WebSocket protocol. Defaults is false.
+ * }
+ */
+function SocketListener(devToolsServer, socketOptions) {
+ this._devToolsServer = devToolsServer;
+
+ // Set socket options with default value
+ this._socketOptions = {
+ authenticator:
+ socketOptions.authenticator || new (Authenticators.get().Server)(),
+ discoverable: !!socketOptions.discoverable,
+ encryption: !!socketOptions.encryption,
+ portOrPath: socketOptions.portOrPath || null,
+ webSocket: !!socketOptions.webSocket,
+ };
+
+ EventEmitter.decorate(this);
+}
+
+SocketListener.prototype = {
+ get authenticator() {
+ return this._socketOptions.authenticator;
+ },
+
+ get discoverable() {
+ return this._socketOptions.discoverable;
+ },
+
+ get encryption() {
+ return this._socketOptions.encryption;
+ },
+
+ get portOrPath() {
+ return this._socketOptions.portOrPath;
+ },
+
+ get webSocket() {
+ return this._socketOptions.webSocket;
+ },
+
+ /**
+ * Validate that all options have been set to a supported configuration.
+ */
+ _validateOptions: function() {
+ if (this.portOrPath === null) {
+ throw new Error("Must set a port / path to listen on.");
+ }
+ if (this.discoverable && !Number(this.portOrPath)) {
+ throw new Error("Discovery only supported for TCP sockets.");
+ }
+ if (this.encryption && this.webSocket) {
+ throw new Error("Encryption not supported on WebSocket transport");
+ }
+ this.authenticator.validateOptions(this);
+ },
+
+ /**
+ * Listens on the given port or socket file for remote debugger connections.
+ */
+ open: function() {
+ this._validateOptions();
+ this._devToolsServer.addSocketListener(this);
+
+ let flags = Ci.nsIServerSocket.KeepWhenOffline;
+ // A preference setting can force binding on the loopback interface.
+ if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
+ flags |= Ci.nsIServerSocket.LoopbackOnly;
+ }
+
+ const self = this;
+ return (async function() {
+ const backlog = 4;
+ self._socket = self._createSocketInstance();
+ if (self.isPortBased) {
+ const port = Number(self.portOrPath);
+ self._socket.initSpecialConnection(port, flags, backlog);
+ } else if (self.portOrPath.startsWith("/")) {
+ const file = nsFile(self.portOrPath);
+ if (file.exists()) {
+ file.remove(false);
+ }
+ self._socket.initWithFilename(file, parseInt("666", 8), backlog);
+ } else {
+ // Path isn't absolute path, so we use abstract socket address
+ self._socket.initWithAbstractAddress(self.portOrPath, backlog);
+ }
+ await self._setAdditionalSocketOptions();
+ self._socket.asyncListen(self);
+ dumpn("Socket listening on: " + (self.port || self.portOrPath));
+ })()
+ .then(() => {
+ this._advertise();
+ })
+ .catch(e => {
+ dumpn(
+ "Could not start debugging listener on '" +
+ this.portOrPath +
+ "': " +
+ e
+ );
+ this.close();
+ });
+ },
+
+ _advertise: function() {
+ if (!this.discoverable || !this.port) {
+ return;
+ }
+
+ const advertisement = {
+ port: this.port,
+ encryption: this.encryption,
+ };
+
+ this.authenticator.augmentAdvertisement(this, advertisement);
+
+ discovery.addService("devtools", advertisement);
+ },
+
+ _createSocketInstance: function() {
+ if (this.encryption) {
+ return Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ }
+ return Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ },
+
+ async _setAdditionalSocketOptions() {
+ if (this.encryption) {
+ this._socket.serverCert = await cert.local.getOrCreate();
+ this._socket.setSessionTickets(false);
+ const requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
+ this._socket.setRequestClientCertificate(requestCert);
+ }
+ this.authenticator.augmentSocketOptions(this, this._socket);
+ },
+
+ /**
+ * Closes the SocketListener. Notifies the server to remove the listener from
+ * the set of active SocketListeners.
+ */
+ close: function() {
+ if (this.discoverable && this.port) {
+ discovery.removeService("devtools");
+ }
+ if (this._socket) {
+ this._socket.close();
+ this._socket = null;
+ }
+ this._devToolsServer.removeSocketListener(this);
+ },
+
+ get host() {
+ if (!this._socket) {
+ return null;
+ }
+ if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
+ return "127.0.0.1";
+ }
+ return "0.0.0.0";
+ },
+
+ /**
+ * Gets whether this listener uses a port number vs. a path.
+ */
+ get isPortBased() {
+ return !!Number(this.portOrPath);
+ },
+
+ /**
+ * Gets the port that a TCP socket listener is listening on, or null if this
+ * is not a TCP socket (so there is no port).
+ */
+ get port() {
+ if (!this.isPortBased || !this._socket) {
+ return null;
+ }
+ return this._socket.port;
+ },
+
+ get cert() {
+ if (!this._socket || !this._socket.serverCert) {
+ return null;
+ }
+ return {
+ sha256: this._socket.serverCert.sha256Fingerprint,
+ };
+ },
+
+ onAllowedConnection(transport) {
+ dumpn("onAllowedConnection, transport: " + transport);
+ this.emit("accepted", transport, this);
+ },
+
+ // nsIServerSocketListener implementation
+
+ onSocketAccepted: DevToolsUtils.makeInfallible(function(
+ socket,
+ socketTransport
+ ) {
+ const connection = new ServerSocketConnection(this, socketTransport);
+ connection.once("allowed", this.onAllowedConnection.bind(this));
+ },
+ "SocketListener.onSocketAccepted"),
+
+ onStopListening: function(socket, status) {
+ dumpn("onStopListening, status: " + status);
+ },
+};
+
+// Client must complete TLS handshake within this window (ms)
+loader.lazyGetter(this, "HANDSHAKE_TIMEOUT", () => {
+ return Services.prefs.getIntPref("devtools.remote.tls-handshake-timeout");
+});
+
+/**
+ * A |ServerSocketConnection| is created by a |SocketListener| for each accepted
+ * incoming socket. This is a short-lived object used to implement
+ * authentication and verify encryption prior to handing off the connection to
+ * the |DevToolsServer|.
+ */
+function ServerSocketConnection(listener, socketTransport) {
+ this._listener = listener;
+ this._socketTransport = socketTransport;
+ this._handle();
+ EventEmitter.decorate(this);
+}
+
+ServerSocketConnection.prototype = {
+ get authentication() {
+ return this._listener.authenticator.mode;
+ },
+
+ get host() {
+ return this._socketTransport.host;
+ },
+
+ get port() {
+ return this._socketTransport.port;
+ },
+
+ get cert() {
+ if (!this._clientCert) {
+ return null;
+ }
+ return {
+ sha256: this._clientCert.sha256Fingerprint,
+ };
+ },
+
+ get address() {
+ return this.host + ":" + this.port;
+ },
+
+ get client() {
+ const client = {
+ host: this.host,
+ port: this.port,
+ };
+ if (this.cert) {
+ client.cert = this.cert;
+ }
+ return client;
+ },
+
+ get server() {
+ const server = {
+ host: this._listener.host,
+ port: this._listener.port,
+ };
+ if (this._listener.cert) {
+ server.cert = this._listener.cert;
+ }
+ return server;
+ },
+
+ /**
+ * This is the main authentication workflow. If any pieces reject a promise,
+ * the connection is denied. If the entire process resolves successfully,
+ * the connection is finally handed off to the |DevToolsServer|.
+ */
+ async _handle() {
+ dumpn("Debugging connection starting authentication on " + this.address);
+ try {
+ this._listenForTLSHandshake();
+ await this._createTransport();
+ await this._awaitTLSHandshake();
+ await this._authenticate();
+ this.allow();
+ } catch (e) {
+ this.deny(e);
+ }
+ },
+
+ /**
+ * We need to open the streams early on, as that is required in the case of
+ * TLS sockets to keep the handshake moving.
+ */
+ async _createTransport() {
+ const input = this._socketTransport.openInputStream(0, 0, 0);
+ const output = this._socketTransport.openOutputStream(0, 0, 0);
+
+ if (this._listener.webSocket) {
+ const socket = await WebSocketServer.accept(
+ this._socketTransport,
+ input,
+ output
+ );
+ this._transport = new WebSocketDebuggerTransport(socket);
+ } else {
+ this._transport = new DebuggerTransport(input, output);
+ }
+
+ // Start up the transport to observe the streams in case they are closed
+ // early. This allows us to clean up our state as well.
+ this._transport.hooks = {
+ onClosed: reason => {
+ this.deny(reason);
+ },
+ };
+ this._transport.ready();
+ },
+
+ /**
+ * Set the socket's security observer, which receives an event via the
+ * |onHandshakeDone| callback when the TLS handshake completes.
+ */
+ _setSecurityObserver(observer) {
+ if (!this._socketTransport || !this._socketTransport.securityInfo) {
+ return;
+ }
+ const connectionInfo = this._socketTransport.securityInfo.QueryInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ connectionInfo.setSecurityObserver(observer);
+ },
+
+ /**
+ * When encryption is used, we wait for the client to complete the TLS
+ * handshake before proceeding. The handshake details are validated in
+ * |onHandshakeDone|.
+ */
+ _listenForTLSHandshake() {
+ if (!this._listener.encryption) {
+ this._handshakePromise = Promise.resolve();
+ return;
+ }
+
+ this._handshakePromise = new Promise((resolve, reject) => {
+ this._observer = {
+ // nsITLSServerSecurityObserver implementation
+ onHandshakeDone: (socket, clientStatus) => {
+ clearTimeout(this._handshakeTimeout);
+ this._setSecurityObserver(null);
+ dumpv("TLS version: " + clientStatus.tlsVersionUsed.toString(16));
+ dumpv("TLS cipher: " + clientStatus.cipherName);
+ dumpv("TLS key length: " + clientStatus.keyLength);
+ dumpv("TLS MAC length: " + clientStatus.macLength);
+ this._clientCert = clientStatus.peerCert;
+ /*
+ * TODO: These rules should be really be set on the TLS socket directly, but
+ * this would need more platform work to expose it via XPCOM.
+ *
+ * Enforcing cipher suites here would be a bad idea, as we want TLS
+ * cipher negotiation to work correctly. The server already allows only
+ * Gecko's normal set of cipher suites.
+ */
+ if (
+ clientStatus.tlsVersionUsed < Ci.nsITLSClientStatus.TLS_VERSION_1_2
+ ) {
+ reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+ return;
+ }
+
+ resolve();
+ },
+ };
+ this._setSecurityObserver(this._observer);
+ this._handshakeTimeout = setTimeout(() => {
+ dumpv("Client failed to complete TLS handshake");
+ reject(Cr.NS_ERROR_NET_TIMEOUT);
+ }, HANDSHAKE_TIMEOUT);
+ });
+ },
+
+ _awaitTLSHandshake() {
+ return this._handshakePromise;
+ },
+
+ async _authenticate() {
+ const result = await this._listener.authenticator.authenticate({
+ client: this.client,
+ server: this.server,
+ transport: this._transport,
+ });
+ switch (result) {
+ case AuthenticationResult.DISABLE_ALL:
+ this._listener._devToolsServer.closeAllSocketListeners();
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
+ return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+ case AuthenticationResult.DENY:
+ return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+ case AuthenticationResult.ALLOW:
+ case AuthenticationResult.ALLOW_PERSIST:
+ return promise.resolve();
+ default:
+ return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+ }
+ },
+
+ deny(result) {
+ if (this._destroyed) {
+ return;
+ }
+ let errorName = result;
+ for (const name in Cr) {
+ if (Cr[name] === result) {
+ errorName = name;
+ break;
+ }
+ }
+ dumpn(
+ "Debugging connection denied on " + this.address + " (" + errorName + ")"
+ );
+ if (this._transport) {
+ this._transport.hooks = null;
+ this._transport.close(result);
+ }
+ this._socketTransport.close(result);
+ this.destroy();
+ },
+
+ allow() {
+ if (this._destroyed) {
+ return;
+ }
+ dumpn("Debugging connection allowed on " + this.address);
+ this.emit("allowed", this._transport);
+ this.destroy();
+ },
+
+ destroy() {
+ this._destroyed = true;
+ clearTimeout(this._handshakeTimeout);
+ this._setSecurityObserver(null);
+ this._listener = null;
+ this._socketTransport = null;
+ this._transport = null;
+ this._clientCert = null;
+ },
+};
+
+exports.DebuggerSocket = DebuggerSocket;
+exports.SocketListener = SocketListener;
diff --git a/devtools/shared/security/tests/chrome/.eslintrc.js b/devtools/shared/security/tests/chrome/.eslintrc.js
new file mode 100644
index 0000000000..660a3bf0a9
--- /dev/null
+++ b/devtools/shared/security/tests/chrome/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "plugin:mozilla/chrome-test",
+};
diff --git a/devtools/shared/security/tests/chrome/chrome.ini b/devtools/shared/security/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..5812511032
--- /dev/null
+++ b/devtools/shared/security/tests/chrome/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+tags = devtools
+
+[test_websocket-transport.html]
diff --git a/devtools/shared/security/tests/chrome/test_websocket-transport.html b/devtools/shared/security/tests/chrome/test_websocket-transport.html
new file mode 100644
index 0000000000..25174a7174
--- /dev/null
+++ b/devtools/shared/security/tests/chrome/test_websocket-transport.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test the WebSocket debugger transport</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+
+window.onload = function() {
+ const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+ const Services = require("Services");
+ // eslint-disable-next-line mozilla/reject-some-requires
+ const {DevToolsClient} = require("devtools/client/devtools-client");
+ const {DevToolsServer} = require("devtools/server/devtools-server");
+ const { SocketListener } = require("devtools/shared/security/socket");
+
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+ Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
+
+ SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+ Services.prefs.clearUserPref("devtools.debugger.prompt-connection");
+ });
+
+ add_task(async function() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+
+ is(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ const socketOptions = {
+ portOrPath: -1,
+ webSocket: true,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ is(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ const transport = await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ webSocket: true,
+ });
+ ok(transport, "Client transport created");
+
+ const client = new DevToolsClient(transport);
+ const onUnexpectedClose = () => {
+ ok(false, "Closed unexpectedly");
+ };
+ client.on("closed", onUnexpectedClose);
+
+ await client.connect();
+
+ // Send a message the server that will echo back
+ const message = "message";
+ const reply = await client.mainRoot.echo({ message });
+ is(reply.message, message, "Echo message matches");
+
+ client.off("closed", onUnexpectedClose);
+ transport.close();
+ listener.close();
+ is(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ DevToolsServer.destroy();
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/shared/security/tests/xpcshell/.eslintrc.js b/devtools/shared/security/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/security/tests/xpcshell/head_dbg.js b/devtools/shared/security/tests/xpcshell/head_dbg.js
new file mode 100644
index 0000000000..6adf9bdd1b
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/head_dbg.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* exported defer, DevToolsClient, initTestDevToolsServer */
+
+const { loader, require } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+);
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const xpcInspector = require("xpcInspector");
+const { DevToolsServer } = require("devtools/server/devtools-server");
+const { DevToolsClient } = require("devtools/client/devtools-client");
+// We need to require lazily since will be crashed if we load SocketListener too early
+// in xpc shell test due to SocketListener loads PSM module.
+loader.lazyRequireGetter(
+ this,
+ "SocketListener",
+ "devtools/shared/security/socket",
+ true
+);
+
+// We do not want to log packets by default, because in some tests,
+// we can be sending large amounts of data. The test harness has
+// trouble dealing with logging all the data, and we end up with
+// intermittent time outs (e.g. bug 775924).
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+// Services.prefs.setBoolPref("devtools.debugger.log.verbose", true);
+// Enable remote debugging for the relevant tests.
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+// Fast timeout for TLS tests
+Services.prefs.setIntPref("devtools.remote.tls-handshake-timeout", 1000);
+
+// Convert an nsIScriptError 'logLevel' value into an appropriate string.
+function scriptErrorLogLevel(message) {
+ switch (message.logLevel) {
+ case Ci.nsIConsoleMessage.info:
+ return "info";
+ case Ci.nsIConsoleMessage.warn:
+ return "warning";
+ default:
+ Assert.equal(message.logLevel, Ci.nsIConsoleMessage.error);
+ return "error";
+ }
+}
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+var listener = {
+ observe: function(message) {
+ let string;
+ try {
+ message.QueryInterface(Ci.nsIScriptError);
+ dump(
+ message.sourceName +
+ ":" +
+ message.lineNumber +
+ ": " +
+ scriptErrorLogLevel(message) +
+ ": " +
+ message.errorMessage +
+ "\n"
+ );
+ string = message.errorMessage;
+ } catch (ex) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ string = "" + message.message;
+ } catch (e) {
+ string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (xpcInspector.eventLoopNestLevel > 0) {
+ xpcInspector.exitNestedEventLoop();
+ }
+
+ info("head_dbg.js got console message: " + string + "\n");
+ },
+};
+
+Services.console.registerListener(listener);
+
+/**
+ * Initialize the testing devtools server.
+ */
+function initTestDevToolsServer() {
+ const { createRootActor } = require("xpcshell-test/testactors");
+ DevToolsServer.setRootActor(createRootActor);
+ DevToolsServer.init();
+}
diff --git a/devtools/shared/security/tests/xpcshell/test_encryption.js b/devtools/shared/security/tests/xpcshell/test_encryption.js
new file mode 100644
index 0000000000..1721418a5b
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/test_encryption.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test basic functionality of DevTools client and server TLS encryption mode
+function run_test() {
+ // Need profile dir to store the key / cert
+ do_get_profile();
+ // Ensure PSM is initialized
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+ run_next_test();
+}
+
+function connectClient(client) {
+ return client.connect();
+}
+
+add_task(async function() {
+ initTestDevToolsServer();
+});
+
+// Client w/ encryption connects successfully to server w/ encryption
+add_task(async function() {
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ const AuthenticatorType = DevToolsServer.Authenticators.get("PROMPT");
+ const authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+
+ const socketOptions = {
+ authenticator,
+ encryption: true,
+ portOrPath: -1,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ equal(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ const transport = await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ encryption: true,
+ });
+ ok(transport, "Client transport created");
+
+ const client = new DevToolsClient(transport);
+ const onUnexpectedClose = () => {
+ do_throw("Closed unexpectedly");
+ };
+ client.on("closed", onUnexpectedClose);
+ await connectClient(client);
+
+ // Send a message the server will echo back
+ const message = "secrets";
+ const reply = await client.mainRoot.echo({ message });
+ equal(reply.message, message, "Encrypted echo matches");
+
+ client.off("closed", onUnexpectedClose);
+ transport.close();
+ listener.close();
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+});
+
+// Client w/o encryption fails to connect to server w/ encryption
+add_task(async function() {
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ const AuthenticatorType = DevToolsServer.Authenticators.get("PROMPT");
+ const authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+
+ const socketOptions = {
+ authenticator,
+ encryption: true,
+ portOrPath: -1,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ equal(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ try {
+ await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ // encryption: false is the default
+ });
+ } catch (e) {
+ ok(true, "Client failed to connect as expected");
+ listener.close();
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+ return;
+ }
+
+ do_throw("Connection unexpectedly succeeded");
+});
+
+add_task(async function() {
+ DevToolsServer.destroy();
+});
diff --git a/devtools/shared/security/tests/xpcshell/test_oob_cert_auth.js b/devtools/shared/security/tests/xpcshell/test_oob_cert_auth.js
new file mode 100644
index 0000000000..229db1e62f
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/test_oob_cert_auth.js
@@ -0,0 +1,267 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const cert = require("devtools/shared/security/cert");
+
+// Test basic functionality of DevTools client and server OOB_CERT auth (used
+// with WiFi debugging)
+function run_test() {
+ // Need profile dir to store the key / cert
+ do_get_profile();
+ // Ensure PSM is initialized
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+ run_next_test();
+}
+
+function connectClient(client) {
+ return client.connect();
+}
+
+add_task(async function() {
+ initTestDevToolsServer();
+});
+
+// Client w/ OOB_CERT auth connects successfully to server w/ OOB_CERT auth
+add_task(async function() {
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ // Grab our cert, instead of relying on a discovery advertisement
+ const serverCert = await cert.local.getOrCreate();
+
+ const oobData = defer();
+ const AuthenticatorType = DevToolsServer.Authenticators.get("OOB_CERT");
+ const serverAuth = new AuthenticatorType.Server();
+ serverAuth.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+ // Skip prompt for tests
+ serverAuth.receiveOOB = () => oobData.promise;
+
+ const socketOptions = {
+ authenticator: serverAuth,
+ encryption: true,
+ portOrPath: -1,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ equal(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ const clientAuth = new AuthenticatorType.Client();
+ clientAuth.sendOOB = ({ oob }) => {
+ info(oob);
+ // Pass to server, skipping prompt for tests
+ oobData.resolve(oob);
+ };
+
+ const transport = await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ encryption: true,
+ authenticator: clientAuth,
+ cert: {
+ sha256: serverCert.sha256Fingerprint,
+ },
+ });
+ ok(transport, "Client transport created");
+
+ const client = new DevToolsClient(transport);
+ const onUnexpectedClose = () => {
+ do_throw("Closed unexpectedly");
+ };
+ client.on("closed", onUnexpectedClose);
+ await connectClient(client);
+
+ // Send a message the server will echo back
+ const message = "secrets";
+ const reply = await client.mainRoot.echo({ message });
+ equal(reply.message, message, "Encrypted echo matches");
+
+ client.off("closed", onUnexpectedClose);
+ transport.close();
+ listener.close();
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+});
+
+// Client w/o OOB_CERT auth fails to connect to server w/ OOB_CERT auth
+add_task(async function() {
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ const oobData = defer();
+ const AuthenticatorType = DevToolsServer.Authenticators.get("OOB_CERT");
+ const serverAuth = new AuthenticatorType.Server();
+ serverAuth.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+ // Skip prompt for tests
+ serverAuth.receiveOOB = () => oobData.promise;
+
+ const socketOptions = {
+ authenticator: serverAuth,
+ encryption: true,
+ portOrPath: -1,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ equal(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ // This will succeed, but leaves the client in confused state, and no data is
+ // actually accessible
+ const transport = await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ encryption: true,
+ // authenticator: PROMPT is the default
+ });
+
+ // Attempt to use the transport
+ const deferred = defer();
+ const client = new DevToolsClient(transport);
+ client.onPacket = packet => {
+ // Client did not authenticate, so it ends up seeing the server's auth data
+ // which is effectively malformed data from the client's perspective
+ ok(!packet.from && packet.authResult, "Got auth packet instead of data");
+ deferred.resolve();
+ };
+ client.connect();
+ await deferred.promise;
+
+ // Try to send a message the server will echo back
+ const message = "secrets";
+ try {
+ await client.request({
+ to: "root",
+ type: "echo",
+ message,
+ });
+ } catch (e) {
+ ok(true, "Sending a message failed");
+ transport.close();
+ listener.close();
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+ return;
+ }
+
+ do_throw("Connection unexpectedly succeeded");
+});
+
+// Client w/ invalid K value fails to connect
+add_task(async function() {
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ // Grab our cert, instead of relying on a discovery advertisement
+ const serverCert = await cert.local.getOrCreate();
+
+ const oobData = defer();
+ const AuthenticatorType = DevToolsServer.Authenticators.get("OOB_CERT");
+ const serverAuth = new AuthenticatorType.Server();
+ serverAuth.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+ // Skip prompt for tests
+ serverAuth.receiveOOB = () => oobData.promise;
+
+ const clientAuth = new AuthenticatorType.Client();
+ clientAuth.sendOOB = ({ oob }) => {
+ info(oob);
+ info("Modifying K value, should fail");
+ // Pass to server, skipping prompt for tests
+ oobData.resolve({
+ k: oob.k + 1,
+ sha256: oob.sha256,
+ });
+ };
+
+ const socketOptions = {
+ authenticator: serverAuth,
+ encryption: true,
+ portOrPath: -1,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ equal(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ try {
+ await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ encryption: true,
+ authenticator: clientAuth,
+ cert: {
+ sha256: serverCert.sha256Fingerprint,
+ },
+ });
+ } catch (e) {
+ ok(true, "Client failed to connect as expected");
+ listener.close();
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+ return;
+ }
+
+ do_throw("Connection unexpectedly succeeded");
+});
+
+// Client w/ invalid cert hash fails to connect
+add_task(async function() {
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+
+ // Grab our cert, instead of relying on a discovery advertisement
+ const serverCert = await cert.local.getOrCreate();
+
+ const oobData = defer();
+ const AuthenticatorType = DevToolsServer.Authenticators.get("OOB_CERT");
+ const serverAuth = new AuthenticatorType.Server();
+ serverAuth.allowConnection = () => {
+ return DevToolsServer.AuthenticationResult.ALLOW;
+ };
+ // Skip prompt for tests
+ serverAuth.receiveOOB = () => oobData.promise;
+
+ const clientAuth = new AuthenticatorType.Client();
+ clientAuth.sendOOB = ({ oob }) => {
+ info(oob);
+ info("Modifying cert hash, should fail");
+ // Pass to server, skipping prompt for tests
+ oobData.resolve({
+ k: oob.k,
+ sha256: oob.sha256 + 1,
+ });
+ };
+
+ const socketOptions = {
+ authenticator: serverAuth,
+ encryption: true,
+ portOrPath: -1,
+ };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ ok(listener, "Socket listener created");
+ await listener.open();
+ equal(DevToolsServer.listeningSockets, 1, "1 listening socket");
+
+ try {
+ await DevToolsClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ encryption: true,
+ authenticator: clientAuth,
+ cert: {
+ sha256: serverCert.sha256Fingerprint,
+ },
+ });
+ } catch (e) {
+ ok(true, "Client failed to connect as expected");
+ listener.close();
+ equal(DevToolsServer.listeningSockets, 0, "0 listening sockets");
+ return;
+ }
+
+ do_throw("Connection unexpectedly succeeded");
+});
+
+add_task(async function() {
+ DevToolsServer.destroy();
+});
diff --git a/devtools/shared/security/tests/xpcshell/testactors.js b/devtools/shared/security/tests/xpcshell/testactors.js
new file mode 100644
index 0000000000..e5f849a431
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/testactors.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { RootActor } = require("devtools/server/actors/root");
+const {
+ ActorRegistry,
+} = require("devtools/server/actors/utils/actor-registry");
+
+exports.createRootActor = function createRootActor(connection) {
+ const root = new RootActor(connection, {
+ globalActorFactories: ActorRegistry.globalActorFactories,
+ });
+ root.applicationType = "xpcshell-tests";
+ return root;
+};
diff --git a/devtools/shared/security/tests/xpcshell/xpcshell.ini b/devtools/shared/security/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..c1a669c228
--- /dev/null
+++ b/devtools/shared/security/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = devtools
+head = head_dbg.js
+skip-if = toolkit == 'android' || socketprocess_networking
+firefox-appdir = browser
+
+support-files=
+ testactors.js
+
+[test_encryption.js]
+[test_oob_cert_auth.js]