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/. */
4
5"use strict";
6
7const EXPORTED_SYMBOLS = ["SecurityInfo"];
8
9const { XPCOMUtils } = ChromeUtils.import(
10  "resource://gre/modules/XPCOMUtils.jsm"
11);
12
13const wpl = Ci.nsIWebProgressListener;
14XPCOMUtils.defineLazyServiceGetter(
15  this,
16  "NSSErrorsService",
17  "@mozilla.org/nss_errors_service;1",
18  "nsINSSErrorsService"
19);
20XPCOMUtils.defineLazyServiceGetter(
21  this,
22  "sss",
23  "@mozilla.org/ssservice;1",
24  "nsISiteSecurityService"
25);
26XPCOMUtils.defineLazyServiceGetter(
27  this,
28  "pkps",
29  "@mozilla.org/security/publickeypinningservice;1",
30  "nsIPublicKeyPinningService"
31);
32
33// NOTE: SecurityInfo is largely reworked from the devtools NetworkHelper with changes
34// to better support the WebRequest api.  The objects returned are formatted specifically
35// to pass through as part of a response to webRequest listeners.
36
37const SecurityInfo = {
38  /**
39   * Extracts security information from nsIChannel.securityInfo.
40   *
41   * @param {nsIChannel} channel
42   *        If null channel is assumed to be insecure.
43   * @param {Object} options
44   *
45   * @returns {Object}
46   *         Returns an object containing following members:
47   *          - state: The security of the connection used to fetch this
48   *                   request. Has one of following string values:
49   *                    * "insecure": the connection was not secure (only http)
50   *                    * "weak": the connection has minor security issues
51   *                    * "broken": secure connection failed (e.g. expired cert)
52   *                    * "secure": the connection was properly secured.
53   *          If state == broken:
54   *            - errorMessage: full error message from
55   *                            nsITransportSecurityInfo.
56   *          If state == secure:
57   *            - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3.
58   *            - cipherSuite: the cipher suite used in this connection.
59   *            - cert: information about certificate used in this connection.
60   *                    See parseCertificateInfo for the contents.
61   *            - hsts: true if host uses Strict Transport Security,
62   *                    false otherwise
63   *            - hpkp: true if host uses Public Key Pinning, false otherwise
64   *          If state == weak: Same as state == secure and
65   *            - weaknessReasons: list of reasons that cause the request to be
66   *                               considered weak. See getReasonsForWeakness.
67   */
68  getSecurityInfo(channel, options = {}) {
69    const info = {
70      state: "insecure",
71    };
72
73    /**
74     * Different scenarios to consider here and how they are handled:
75     * - request is HTTP, the connection is not secure
76     *   => securityInfo is null
77     *      => state === "insecure"
78     *
79     * - request is HTTPS, the connection is secure
80     *   => .securityState has STATE_IS_SECURE flag
81     *      => state === "secure"
82     *
83     * - request is HTTPS, the connection has security issues
84     *   => .securityState has STATE_IS_INSECURE flag
85     *   => .errorCode is an NSS error code.
86     *      => state === "broken"
87     *
88     * - request is HTTPS, the connection was terminated before the security
89     *   could be validated
90     *   => .securityState has STATE_IS_INSECURE flag
91     *   => .errorCode is NOT an NSS error code.
92     *   => .errorMessage is not available.
93     *      => state === "insecure"
94     *
95     * - request is HTTPS but it uses a weak cipher or old protocol, see
96     *   https://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
97     *   security/manager/ssl/nsNSSCallbacks.cpp#l1233
98     * - request is mixed content (which makes no sense whatsoever)
99     *   => .securityState has STATE_IS_BROKEN flag
100     *   => .errorCode is NOT an NSS error code
101     *   => .errorMessage is not available
102     *      => state === "weak"
103     */
104
105    let securityInfo = channel.securityInfo;
106    if (!securityInfo) {
107      return info;
108    }
109
110    securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
111
112    if (NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
113      // The connection failed.
114      info.state = "broken";
115      info.errorMessage = securityInfo.errorMessage;
116      if (options.certificateChain && securityInfo.failedCertChain) {
117        info.certificates = this.getCertificateChain(
118          securityInfo.failedCertChain,
119          options
120        );
121      }
122      return info;
123    }
124
125    const state = securityInfo.securityState;
126
127    let uri = channel.URI;
128    if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
129      // it is not enough to look at the transport security info -
130      // schemes other than https and wss are subject to
131      // downgrade/etc at the scheme level and should always be
132      // considered insecure.
133      // Leave info.state = "insecure";
134    } else if (state & wpl.STATE_IS_SECURE) {
135      // The connection is secure if the scheme is sufficient
136      info.state = "secure";
137    } else if (state & wpl.STATE_IS_BROKEN) {
138      // The connection is not secure, there was no error but there's some
139      // minor security issues.
140      info.state = "weak";
141      info.weaknessReasons = this.getReasonsForWeakness(state);
142    } else if (state & wpl.STATE_IS_INSECURE) {
143      // This was most likely an https request that was aborted before
144      // validation. Return info as info.state = insecure.
145      return info;
146    } else {
147      // No known STATE_IS_* flags.
148      return info;
149    }
150
151    // Cipher suite.
152    info.cipherSuite = securityInfo.cipherName;
153
154    // Key exchange group name.
155    if (securityInfo.keaGroupName !== "none") {
156      info.keaGroupName = securityInfo.keaGroupName;
157    }
158
159    // Certificate signature scheme.
160    if (securityInfo.signatureSchemeName !== "none") {
161      info.signatureSchemeName = securityInfo.signatureSchemeName;
162    }
163
164    info.isDomainMismatch = securityInfo.isDomainMismatch;
165    info.isExtendedValidation = securityInfo.isExtendedValidation;
166    info.isNotValidAtThisTime = securityInfo.isNotValidAtThisTime;
167    info.isUntrusted = securityInfo.isUntrusted;
168
169    info.certificateTransparencyStatus = this.getTransparencyStatus(
170      securityInfo.certificateTransparencyStatus
171    );
172
173    // Protocol version.
174    info.protocolVersion = this.formatSecurityProtocol(
175      securityInfo.protocolVersion
176    );
177
178    if (options.certificateChain && securityInfo.succeededCertChain) {
179      info.certificates = this.getCertificateChain(
180        securityInfo.succeededCertChain,
181        options
182      );
183    } else {
184      info.certificates = [
185        this.parseCertificateInfo(securityInfo.serverCert, options),
186      ];
187    }
188
189    // HSTS and static pinning if available.
190    if (uri && uri.host) {
191      // SiteSecurityService uses different storage if the channel is
192      // private. Thus we must give isSecureURI correct flags or we
193      // might get incorrect results.
194      let flags = 0;
195      if (
196        channel instanceof Ci.nsIPrivateBrowsingChannel &&
197        channel.isChannelPrivate
198      ) {
199        flags = Ci.nsISocketProvider.NO_PERMANENT_STORAGE;
200      }
201
202      info.hsts = sss.isSecureURI(uri, flags);
203      info.hpkp = pkps.hostHasPins(uri);
204    } else {
205      info.hsts = false;
206      info.hpkp = false;
207    }
208
209    return info;
210  },
211
212  getCertificateChain(certChain, options = {}) {
213    let certificates = [];
214    for (let cert of certChain) {
215      certificates.push(this.parseCertificateInfo(cert, options));
216    }
217    return certificates;
218  },
219
220  /**
221   * Takes an nsIX509Cert and returns an object with certificate information.
222   *
223   * @param {nsIX509Cert} cert
224   *        The certificate to extract the information from.
225   * @param {Object} options
226   * @returns {Object}
227   *         An object with following format:
228   *           {
229   *             subject: subjectName,
230   *             issuer: issuerName,
231   *             validity: { start, end },
232   *             fingerprint: { sha1, sha256 }
233   *           }
234   */
235  parseCertificateInfo(cert, options = {}) {
236    if (!cert) {
237      return {};
238    }
239
240    let certData = {
241      subject: cert.subjectName,
242      issuer: cert.issuerName,
243      validity: {
244        start: cert.validity.notBefore
245          ? Math.trunc(cert.validity.notBefore / 1000)
246          : 0,
247        end: cert.validity.notAfter
248          ? Math.trunc(cert.validity.notAfter / 1000)
249          : 0,
250      },
251      fingerprint: {
252        sha1: cert.sha1Fingerprint,
253        sha256: cert.sha256Fingerprint,
254      },
255      serialNumber: cert.serialNumber,
256      isBuiltInRoot: cert.isBuiltInRoot,
257      subjectPublicKeyInfoDigest: {
258        sha256: cert.sha256SubjectPublicKeyInfoDigest,
259      },
260    };
261    if (options.rawDER) {
262      certData.rawDER = cert.getRawDER();
263    }
264    return certData;
265  },
266
267  // Bug 1355903 Transparency is currently disabled using security.pki.certificate_transparency.mode
268  getTransparencyStatus(status) {
269    switch (status) {
270      case Ci.nsITransportSecurityInfo.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE:
271        return "not_applicable";
272      case Ci.nsITransportSecurityInfo
273        .CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT:
274        return "policy_compliant";
275      case Ci.nsITransportSecurityInfo
276        .CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS:
277        return "policy_not_enough_scts";
278      case Ci.nsITransportSecurityInfo
279        .CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS:
280        return "policy_not_diverse_scts";
281    }
282    return "unknown";
283  },
284
285  /**
286   * Takes protocolVersion of TransportSecurityInfo object and returns human readable
287   * description.
288   *
289   * @param {number} version
290   *        One of nsITransportSecurityInfo version constants.
291   * @returns {string}
292   *         One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if version
293   *         is valid, Unknown otherwise.
294   */
295  formatSecurityProtocol(version) {
296    switch (version) {
297      case Ci.nsITransportSecurityInfo.TLS_VERSION_1:
298        return "TLSv1";
299      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1:
300        return "TLSv1.1";
301      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2:
302        return "TLSv1.2";
303      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3:
304        return "TLSv1.3";
305    }
306    return "unknown";
307  },
308
309  /**
310   * Takes the securityState bitfield and returns reasons for weak connection
311   * as an array of strings.
312   *
313   * @param {number} state
314   *        nsITransportSecurityInfo.securityState.
315   *
316   * @returns {array<string>}
317   *         List of weakness reasons. A subset of { cipher } where
318   *         * cipher: The cipher suite is consireded to be weak (RC4).
319   */
320  getReasonsForWeakness(state) {
321    // If there's non-fatal security issues the request has STATE_IS_BROKEN
322    // flag set. See https://hg.mozilla.org/mozilla-central/file/44344099d119
323    // /security/manager/ssl/nsNSSCallbacks.cpp#l1233
324    let reasons = [];
325
326    if (state & wpl.STATE_IS_BROKEN) {
327      if (state & wpl.STATE_USES_WEAK_CRYPTO) {
328        reasons.push("cipher");
329      }
330    }
331
332    return reasons;
333  },
334};
335