diff options
Diffstat (limited to 'devtools/shared/security/auth.js')
-rw-r--r-- | devtools/shared/security/auth.js | 653 |
1 files changed, 653 insertions, 0 deletions
diff --git a/devtools/shared/security/auth.js b/devtools/shared/security/auth.js new file mode 100644 index 000000000..9272f602e --- /dev/null +++ b/devtools/shared/security/auth.js @@ -0,0 +1,653 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 promise = require("promise"); +var defer = require("devtools/shared/defer"); +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"); +const { Task } = require("devtools/shared/task"); + +/** + * 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 (let 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 debugger server. + * @param port number + * The port number of the debugger 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 debugger server. + * @param port number + * The port number of the debugger 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 debugger server. + * @param port number + * The port number of the debugger 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. + */ + validateConnection({ cert, socket }) { + // Step B.7 + // Client verifies that Server's cert matches hash(ServerCert) from the + // advertisement + dumpv("Validate server cert hash"); + let serverCert = socket.securityInfo.QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus.serverCert; + let 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 debugger server. + * @param port number + * The port number of the debugger 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. + */ + authenticate({ host, port, cert, transport }) { + let deferred = defer(); + let oobData; + + let activeSendDialog; + let closeDialog = () => { + // Close any prompts the client may have been showing from previous + // authentication steps + if (activeSendDialog && activeSendDialog.close) { + activeSendDialog.close(); + activeSendDialog = null; + } + }; + + transport.hooks = { + onPacket: Task.async(function* (packet) { + closeDialog(); + let { authResult } = packet; + switch (authResult) { + case AuthenticationResult.PENDING: + // Step B.8 + // Client creates hash(ClientCert) + K(random 128-bit number) + oobData = yield 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; + deferred.resolve(transport); + break; + case AuthenticationResult.ALLOW_PERSIST: + // Server previously persisted Client as allowed + // Step C.5 + // Debugging begins + transport.hooks = null; + deferred.resolve(transport); + break; + default: + transport.close(new Error("Invalid auth result: " + authResult)); + return; + } + }.bind(this)), + onClosed(reason) { + closeDialog(); + // Transport died before auth completed + transport.hooks = null; + deferred.reject(reason); + } + }; + transport.ready(); + return deferred.promise; + }, + + /** + * Create the package of data that needs to be transferred across the OOB + * channel. + */ + _createOOB: Task.async(function* () { + let clientCert = yield cert.local.getOrCreate(); + return { + sha256: clientCert.sha256Fingerprint, + k: this._createRandom() + }; + }), + + _createRandom() { + const length = 16; // 16 bytes / 128 bits + let rng = Cc["@mozilla.org/security/random-generator;1"] + .createInstance(Ci.nsIRandomGenerator); + let 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 debugger server. + * @param port number + * The port number of the debugger 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) { + let 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. + */ + authenticate: Task.async(function* ({ client, server, transport }) { + // Step B.3 / C.3 + // TLS connection established, authentication begins + const storageKey = `devtools.auth.${this.mode}.approved-clients`; + let approvedClients = (yield asyncStorage.getItem(storageKey)) || {}; + // Step C.4 + // Server sees that ClientCert is from a known client via hash(ClientCert) + if (approvedClients[client.cert.sha256]) { + let 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) + let authResult = yield this.allowConnection({ + authentication: this.mode, + client, + server + }); + + switch (authResult) { + case AuthenticationResult.ALLOW_PERSIST: + case AuthenticationResult.ALLOW: + break; // Further processing + default: + return authResult; // Abort for any negative results + } + + // Examine additional data for authentication + let oob = yield this.receiveOOB(); + if (!oob) { + dumpn("Invalid OOB data received"); + return AuthenticationResult.DENY; + } + + let { 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; + yield 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 (let key in Authenticators) { + let auth = Authenticators[key]; + if (auth.mode === mode) { + return auth; + } + } + throw new Error("Unknown authenticator mode: " + mode); + } +}; |