1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var { Ci, Cc } = require("chrome");
8 var Services = require("Services");
9 var defer = require("devtools/shared/defer");
10 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
11 var { dumpn, dumpv } = DevToolsUtils;
12 loader.lazyRequireGetter(this, "prompt", "devtools/shared/security/prompt");
13 loader.lazyRequireGetter(this, "cert", "devtools/shared/security/cert");
14 loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
17 * A simple enum-like object with keys mirrored to values.
18 * This makes comparison to a specfic value simpler without having to repeat and
21 function createEnum(obj) {
22 for (const key in obj) {
29 * |allowConnection| implementations can return various values as their |result|
30 * field to indicate what action to take. By specifying these, we can
31 * centralize the common actions available, while still allowing embedders to
32 * present their UI in whatever way they choose.
34 var AuthenticationResult = (exports.AuthenticationResult = createEnum({
36 * Close all listening sockets, and disable them from opening again.
41 * Deny the current connection.
46 * Additional data needs to be exchanged before a result can be determined.
51 * Allow the current connection.
56 * Allow the current connection, and persist this choice for future
57 * connections from the same client. This requires a trustable mechanism to
58 * identify the client in the future, such as the cert used during OOB_CERT.
64 * An |Authenticator| implements an authentication mechanism via various hooks
65 * in the client and server debugger socket connection path (see socket.js).
67 * |Authenticator|s are stateless objects. Each hook method is passed the state
68 * it needs by the client / server code in socket.js.
70 * Separate instances of the |Authenticator| are created for each use (client
71 * connection, server listener) in case some methods are customized by the
72 * embedder for a given use case.
74 var Authenticators = {};
77 * The Prompt authenticator displays a server-side user prompt that includes
78 * connection details, and asks the user to verify the connection. There are
79 * no cryptographic properties at work here, so it is up to the user to be sure
80 * that the client can be trusted.
82 var Prompt = (Authenticators.Prompt = {});
84 Prompt.mode = "PROMPT";
86 Prompt.Client = function() {};
87 Prompt.Client.prototype = {
91 * When client is about to make a new connection, verify that the connection settings
92 * are compatible with this authenticator.
93 * @throws if validation requirements are not met
95 validateSettings() {},
98 * When client has just made a new socket connection, validate the connection
99 * to ensure it meets the authenticator's policies.
102 * The host name or IP address of the devtools server.
104 * The port number of the devtools server.
105 * @param encryption boolean (optional)
106 * Whether the server requires encryption. Defaults to false.
107 * @param cert object (optional)
108 * The server's cert details.
109 * @param s nsISocketTransport
110 * Underlying socket transport, in case more details are needed.
112 * Whether the connection is valid.
114 validateConnection() {
119 * Work with the server to complete any additional steps required by this
120 * authenticator's policies.
122 * Debugging commences after this hook completes successfully.
125 * The host name or IP address of the devtools server.
127 * The port number of the devtools server.
128 * @param encryption boolean (optional)
129 * Whether the server requires encryption. Defaults to false.
130 * @param transport DebuggerTransport
131 * A transport that can be used to communicate with the server.
132 * @return A promise can be used if there is async behavior.
137 Prompt.Server = function() {};
138 Prompt.Server.prototype = {
142 * Verify that listener settings are appropriate for this authentication mode.
144 * @param listener SocketListener
145 * The socket listener about to be opened.
146 * @throws if validation requirements are not met
148 validateOptions() {},
151 * Augment options on the listening socket about to be opened.
153 * @param listener SocketListener
154 * The socket listener about to be opened.
155 * @param socket nsIServerSocket
156 * The socket that is about to start listening.
158 augmentSocketOptions() {},
161 * Augment the service discovery advertisement with any additional data needed
162 * to support this authentication mode.
164 * @param listener SocketListener
165 * The socket listener that was just opened.
166 * @param advertisement object
167 * The advertisement being built.
169 augmentAdvertisement(listener, advertisement) {
170 advertisement.authentication = Prompt.mode;
174 * Determine whether a connection the server should be allowed or not based on
175 * this authenticator's policies.
177 * @param session object
178 * In PROMPT mode, the |session| includes:
190 * @return An AuthenticationResult value.
191 * A promise that will be resolved to the above is also allowed.
193 authenticate({ client, server }) {
194 if (!Services.prefs.getBoolPref("devtools.debugger.prompt-connection")) {
195 return AuthenticationResult.ALLOW;
197 return this.allowConnection({
198 authentication: this.mode,
205 * Prompt the user to accept or decline the incoming connection. The default
206 * implementation is used unless this is overridden on a particular
207 * authenticator instance.
209 * It is expected that the implementation of |allowConnection| will show a
210 * prompt to the user so that they can allow or deny the connection.
212 * @param session object
213 * In PROMPT mode, the |session| includes:
215 * authentication: "PROMPT",
225 * @return An AuthenticationResult value.
226 * A promise that will be resolved to the above is also allowed.
228 allowConnection: prompt.Server.defaultAllowConnection,
232 * The out-of-band (OOB) cert authenticator is based on self-signed X.509 certs
233 * at both the client and server end.
235 * The user is first prompted to verify the connection, similar to the prompt
236 * method above. This prompt may display cert fingerprints if desired.
238 * Assuming the user approves the connection, further UI is used to assist the
239 * user in tranferring out-of-band (OOB) verification of the client's
240 * certificate. For example, this could take the form of a QR code that the
241 * client displays which is then scanned by a camera on the server.
243 * Since it is assumed that an attacker can't forge the client's X.509 cert, the
244 * user may also choose to always allow a client, which would permit immediate
245 * connections in the future with no user interaction needed.
247 * See docs/wifi.md for details of the authentication design.
249 var OOBCert = (Authenticators.OOBCert = {});
251 OOBCert.mode = "OOB_CERT";
253 OOBCert.Client = function() {};
254 OOBCert.Client.prototype = {
258 * When client is about to make a new connection, verify that the connection settings
259 * are compatible with this authenticator.
260 * @throws if validation requirements are not met
262 validateSettings({ encryption }) {
264 throw new Error(`${OOBCert.mode} authentication requires encryption.`);
269 * When client has just made a new socket connection, validate the connection
270 * to ensure it meets the authenticator's policies.
273 * The host name or IP address of the devtools server.
275 * The port number of the devtools server.
276 * @param encryption boolean (optional)
277 * Whether the server requires encryption. Defaults to false.
278 * @param cert object (optional)
279 * The server's cert details.
280 * @param socket nsISocketTransport
281 * Underlying socket transport, in case more details are needed.
283 * Whether the connection is valid.
285 // eslint-disable-next-line no-shadow
286 validateConnection({ cert, socket }) {
288 // Client verifies that Server's cert matches hash(ServerCert) from the
290 dumpv("Validate server cert hash");
291 const serverCert = socket.securityInfo.QueryInterface(
292 Ci.nsITransportSecurityInfo
294 const advertisedCert = cert;
295 if (serverCert.sha256Fingerprint != advertisedCert.sha256) {
296 dumpn("Server cert hash doesn't match advertisement");
303 * Work with the server to complete any additional steps required by this
304 * authenticator's policies.
306 * Debugging commences after this hook completes successfully.
309 * The host name or IP address of the devtools server.
311 * The port number of the devtools server.
312 * @param encryption boolean (optional)
313 * Whether the server requires encryption. Defaults to false.
314 * @param cert object (optional)
315 * The server's cert details.
316 * @param transport DebuggerTransport
317 * A transport that can be used to communicate with the server.
318 * @return A promise can be used if there is async behavior.
320 // eslint-disable-next-line no-shadow
321 authenticate({ host, port, cert, transport }) {
322 const deferred = defer();
325 let activeSendDialog;
326 const closeDialog = () => {
327 // Close any prompts the client may have been showing from previous
328 // authentication steps
329 if (activeSendDialog && activeSendDialog.close) {
330 activeSendDialog.close();
331 activeSendDialog = null;
336 onPacket: async packet => {
338 const { authResult } = packet;
339 switch (authResult) {
340 case AuthenticationResult.PENDING:
342 // Client creates hash(ClientCert) + K(random 128-bit number)
343 oobData = await this._createOOB();
344 activeSendDialog = this.sendOOB({
352 case AuthenticationResult.ALLOW:
354 // Client verifies received value matches K
355 if (packet.k != oobData.k) {
356 transport.close(new Error("Auth secret mismatch"));
361 transport.hooks = null;
362 deferred.resolve(transport);
364 case AuthenticationResult.ALLOW_PERSIST:
365 // Server previously persisted Client as allowed
368 transport.hooks = null;
369 deferred.resolve(transport);
372 transport.close(new Error("Invalid auth result: " + authResult));
378 // Transport died before auth completed
379 transport.hooks = null;
380 deferred.reject(reason);
384 return deferred.promise;
388 * Create the package of data that needs to be transferred across the OOB
392 const clientCert = await cert.local.getOrCreate();
394 sha256: clientCert.sha256Fingerprint,
395 k: this._createRandom(),
400 // 16 bytes / 128 bits
402 const rng = Cc["@mozilla.org/security/random-generator;1"].createInstance(
403 Ci.nsIRandomGenerator
405 const bytes = rng.generateRandomBytes(length);
406 return bytes.map(byte => byte.toString(16)).join("");
410 * Send data across the OOB channel to the server to authenticate the devices.
413 * The host name or IP address of the devtools server.
415 * The port number of the devtools server.
416 * @param cert object (optional)
417 * The server's cert details.
418 * @param authResult AuthenticationResult
419 * Authentication result sent from the server.
420 * @param oob object (optional)
421 * The token data to be transferred during OOB_CERT step 8:
422 * * sha256: hash(ClientCert)
423 * * k : K(random 128-bit number)
424 * @return object containing:
425 * * close: Function to hide the notification
427 sendOOB: prompt.Client.defaultSendOOB,
430 OOBCert.Server = function() {};
431 OOBCert.Server.prototype = {
435 * Verify that listener settings are appropriate for this authentication mode.
437 * @param listener SocketListener
438 * The socket listener about to be opened.
439 * @throws if validation requirements are not met
441 validateOptions(listener) {
442 if (!listener.encryption) {
443 throw new Error(OOBCert.mode + " authentication requires encryption.");
448 * Augment options on the listening socket about to be opened.
450 * @param listener SocketListener
451 * The socket listener about to be opened.
452 * @param socket nsIServerSocket
453 * The socket that is about to start listening.
455 augmentSocketOptions(listener, socket) {
456 const requestCert = Ci.nsITLSServerSocket.REQUIRE_ALWAYS;
457 socket.setRequestClientCertificate(requestCert);
461 * Augment the service discovery advertisement with any additional data needed
462 * to support this authentication mode.
464 * @param listener SocketListener
465 * The socket listener that was just opened.
466 * @param advertisement object
467 * The advertisement being built.
469 augmentAdvertisement(listener, advertisement) {
470 advertisement.authentication = OOBCert.mode;
472 // Server announces itself via service discovery
473 // Announcement contains hash(ServerCert) as additional data
474 advertisement.cert = listener.cert;
478 * Determine whether a connection the server should be allowed or not based on
479 * this authenticator's policies.
481 * @param session object
482 * In OOB_CERT mode, the |session| includes:
500 * @return An AuthenticationResult value.
501 * A promise that will be resolved to the above is also allowed.
503 async authenticate({ client, server, transport }) {
505 // TLS connection established, authentication begins
506 const storageKey = `devtools.auth.${this.mode}.approved-clients`;
507 const approvedClients = (await asyncStorage.getItem(storageKey)) || {};
509 // Server sees that ClientCert is from a known client via hash(ClientCert)
510 if (approvedClients[client.cert.sha256]) {
511 const authResult = AuthenticationResult.ALLOW_PERSIST;
512 transport.send({ authResult });
519 // Server sees that ClientCert is from a unknown client
520 // Tell client they are unknown and should display OOB client UX
522 authResult: AuthenticationResult.PENDING,
526 // User is shown a Allow / Deny / Always Allow prompt on the Server
527 // with Client name and hash(ClientCert)
528 const authResult = await this.allowConnection({
529 authentication: this.mode,
534 switch (authResult) {
535 case AuthenticationResult.ALLOW_PERSIST:
536 case AuthenticationResult.ALLOW:
537 // Further processing
540 // Abort for any negative results
544 // Examine additional data for authentication
545 const oob = await this.receiveOOB();
547 dumpn("Invalid OOB data received");
548 return AuthenticationResult.DENY;
551 const { sha256, k } = oob;
552 // The OOB auth prompt should have transferred:
553 // hash(ClientCert) + K(random 128-bit number)
556 dumpn("Invalid OOB data received");
557 return AuthenticationResult.DENY;
561 // Server verifies that Client's cert matches hash(ClientCert) from
562 // out-of-band channel
563 if (client.cert.sha256 != sha256) {
564 dumpn("Client cert hash doesn't match OOB data");
565 return AuthenticationResult.DENY;
569 // Server sends K to Client over TLS connection
570 transport.send({ authResult, k });
572 // Persist Client if we want to always allow in the future
573 if (authResult === AuthenticationResult.ALLOW_PERSIST) {
574 approvedClients[client.cert.sha256] = true;
575 await asyncStorage.setItem(storageKey, approvedClients);
578 // Client may decide to abort if K does not match.
579 // Server's portion of authentication is now complete.
587 * Prompt the user to accept or decline the incoming connection. The default
588 * implementation is used unless this is overridden on a particular
589 * authenticator instance.
591 * It is expected that the implementation of |allowConnection| will show a
592 * prompt to the user so that they can allow or deny the connection.
594 * @param session object
595 * In OOB_CERT mode, the |session| includes:
597 * authentication: "OOB_CERT",
613 * @return An AuthenticationResult value.
614 * A promise that will be resolved to the above is also allowed.
616 allowConnection: prompt.Server.defaultAllowConnection,
619 * The user must transfer some data through some out of band mechanism from
620 * the client to the server to authenticate the devices.
622 * @return An object containing:
623 * * sha256: hash(ClientCert)
624 * * k : K(random 128-bit number)
625 * A promise that will be resolved to the above is also allowed.
627 receiveOOB: prompt.Server.defaultReceiveOOB,
630 exports.Authenticators = {
635 for (const key in Authenticators) {
636 const auth = Authenticators[key];
637 if (auth.mode === mode) {
641 throw new Error("Unknown authenticator mode: " + mode);