summaryrefslogtreecommitdiffstats
path: root/devtools/shared/security/auth.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/security/auth.js')
-rw-r--r--devtools/shared/security/auth.js642
1 files changed, 642 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);
+ },
+};