359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
/* 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/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const wpl = Ci.nsIWebProgressListener;
|
|
const lazy = {};
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"NSSErrorsService",
|
|
"@mozilla.org/nss_errors_service;1",
|
|
"nsINSSErrorsService"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"pkps",
|
|
"@mozilla.org/security/publickeypinningservice;1",
|
|
"nsIPublicKeyPinningService"
|
|
);
|
|
|
|
// NOTE: SecurityInfo is largely reworked from the devtools NetworkHelper with changes
|
|
// to better support the WebRequest api. The objects returned are formatted specifically
|
|
// to pass through as part of a response to webRequest listeners.
|
|
|
|
export const SecurityInfo = {
|
|
/**
|
|
* Extracts security information from nsIChannel.securityInfo.
|
|
*
|
|
* @param {nsIChannel} channel
|
|
* If null channel is assumed to be insecure.
|
|
* @param {object} options
|
|
*
|
|
* @returns {object}
|
|
* Returns an object containing following members:
|
|
* - state: The security of the connection used to fetch this
|
|
* request. Has one of following string values:
|
|
* - "insecure": the connection was not secure (only http)
|
|
* - "weak": the connection has minor security issues
|
|
* - "broken": secure connection failed (e.g. expired cert)
|
|
* - "secure": the connection was properly secured.
|
|
* If state == broken:
|
|
* - errorMessage: full error message from
|
|
* nsITransportSecurityInfo.
|
|
* If state == secure:
|
|
* - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3.
|
|
* - cipherSuite: the cipher suite used in this connection.
|
|
* - cert: information about certificate used in this connection.
|
|
* See parseCertificateInfo for the contents.
|
|
* - hsts: true if host uses Strict Transport Security,
|
|
* false otherwise
|
|
* - hpkp: true if host uses Public Key Pinning, false otherwise
|
|
* If state == weak: Same as state == secure and
|
|
* - weaknessReasons: list of reasons that cause the request to be
|
|
* considered weak. See getReasonsForWeakness.
|
|
*/
|
|
getSecurityInfo(channel, options = {}) {
|
|
const info = {
|
|
state: "insecure",
|
|
};
|
|
|
|
/**
|
|
* Different scenarios to consider here and how they are handled:
|
|
* - request is HTTP, the connection is not secure
|
|
* => securityInfo is null
|
|
* => state === "insecure"
|
|
*
|
|
* - request is HTTPS, the connection is secure
|
|
* => .securityState has STATE_IS_SECURE flag
|
|
* => state === "secure"
|
|
*
|
|
* - request is HTTPS, the connection has security issues
|
|
* => .securityState has STATE_IS_INSECURE flag
|
|
* => .errorCode is an NSS error code.
|
|
* => state === "broken"
|
|
*
|
|
* - request is HTTPS, the connection was terminated before the security
|
|
* could be validated
|
|
* => .securityState has STATE_IS_INSECURE flag
|
|
* => .errorCode is NOT an NSS error code.
|
|
* => .errorMessage is not available.
|
|
* => state === "insecure"
|
|
*
|
|
* - request is HTTPS but it uses a weak cipher or old protocol, see
|
|
* https://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
|
|
* security/manager/ssl/nsNSSCallbacks.cpp#l1233
|
|
* - request is mixed content (which makes no sense whatsoever)
|
|
* => .securityState has STATE_IS_BROKEN flag
|
|
* => .errorCode is NOT an NSS error code
|
|
* => .errorMessage is not available
|
|
* => state === "weak"
|
|
*/
|
|
|
|
let securityInfo = channel.securityInfo;
|
|
|
|
if (!securityInfo) {
|
|
return info;
|
|
}
|
|
|
|
if (lazy.NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
|
|
// The connection failed.
|
|
info.state = "broken";
|
|
info.errorMessage = securityInfo.errorMessage;
|
|
if (options.certificateChain && securityInfo.failedCertChain) {
|
|
info.certificates = this.getCertificateChain(
|
|
securityInfo.failedCertChain,
|
|
false,
|
|
options
|
|
);
|
|
}
|
|
return info;
|
|
}
|
|
|
|
const state = securityInfo.securityState;
|
|
|
|
let uri = channel.URI;
|
|
if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
|
|
// it is not enough to look at the transport security info -
|
|
// schemes other than https and wss are subject to
|
|
// downgrade/etc at the scheme level and should always be
|
|
// considered insecure.
|
|
// Leave info.state = "insecure";
|
|
} else if (state & wpl.STATE_IS_SECURE) {
|
|
// The connection is secure if the scheme is sufficient
|
|
info.state = "secure";
|
|
} else if (state & wpl.STATE_IS_BROKEN) {
|
|
// The connection is not secure, there was no error but there's some
|
|
// minor security issues.
|
|
info.state = "weak";
|
|
info.weaknessReasons = this.getReasonsForWeakness(state);
|
|
} else if (state & wpl.STATE_IS_INSECURE) {
|
|
// This was most likely an https request that was aborted before
|
|
// validation. Return info as info.state = insecure.
|
|
return info;
|
|
} else {
|
|
// No known STATE_IS_* flags.
|
|
return info;
|
|
}
|
|
|
|
// Cipher suite.
|
|
info.cipherSuite = securityInfo.cipherName;
|
|
|
|
// Length (in bits) of the secret key
|
|
info.secretKeyLength = securityInfo.secretKeyLength;
|
|
|
|
// Key exchange group name.
|
|
if (securityInfo.keaGroupName !== "none") {
|
|
info.keaGroupName = securityInfo.keaGroupName;
|
|
}
|
|
|
|
// Certificate signature scheme.
|
|
if (securityInfo.signatureSchemeName !== "none") {
|
|
info.signatureSchemeName = securityInfo.signatureSchemeName;
|
|
}
|
|
|
|
if (
|
|
securityInfo.overridableErrorCategory ==
|
|
Ci.nsITransportSecurityInfo.ERROR_TRUST
|
|
) {
|
|
info.overridableErrorCategory = "trust_error";
|
|
info.isUntrusted = true;
|
|
} else if (
|
|
securityInfo.overridableErrorCategory ==
|
|
Ci.nsITransportSecurityInfo.ERROR_DOMAIN
|
|
) {
|
|
info.overridableErrorCategory = "domain_mismatch";
|
|
info.isDomainMismatch = true;
|
|
} else if (
|
|
securityInfo.overridableErrorCategory ==
|
|
Ci.nsITransportSecurityInfo.ERROR_TIME
|
|
) {
|
|
info.overridableErrorCategory = "expired_or_not_yet_valid";
|
|
info.isNotValidAtThisTime = true;
|
|
}
|
|
info.isExtendedValidation = securityInfo.isExtendedValidation;
|
|
|
|
info.certificateTransparencyStatus = this.getTransparencyStatus(
|
|
securityInfo.certificateTransparencyStatus
|
|
);
|
|
|
|
// Protocol version.
|
|
info.protocolVersion = this.formatSecurityProtocol(
|
|
securityInfo.protocolVersion
|
|
);
|
|
|
|
if (options.certificateChain && securityInfo.succeededCertChain) {
|
|
info.certificates = this.getCertificateChain(
|
|
securityInfo.succeededCertChain,
|
|
securityInfo.isBuiltCertChainRootBuiltInRoot,
|
|
options
|
|
);
|
|
} else {
|
|
info.certificates = [
|
|
this.parseCertificateInfo(securityInfo.serverCert, false, options),
|
|
];
|
|
}
|
|
|
|
// HSTS and static pinning if available.
|
|
if (uri && uri.host) {
|
|
info.hsts = channel.loadInfo.hstsStatus;
|
|
info.hpkp = lazy.pkps.hostHasPins(uri);
|
|
} else {
|
|
info.hsts = false;
|
|
info.hpkp = false;
|
|
}
|
|
|
|
// These values can be unset in rare cases, e.g. when stashed connection
|
|
// data is deseralized from an older version of Firefox.
|
|
try {
|
|
info.usedEch = securityInfo.isAcceptedEch;
|
|
} catch {
|
|
info.usedEch = false;
|
|
}
|
|
try {
|
|
info.usedDelegatedCredentials = securityInfo.isDelegatedCredential;
|
|
} catch {
|
|
info.usedDelegatedCredentials = false;
|
|
}
|
|
info.usedOcsp = securityInfo.madeOCSPRequests;
|
|
info.usedPrivateDns = securityInfo.usedPrivateDNS;
|
|
|
|
return info;
|
|
},
|
|
|
|
getCertificateChain(certChain, isRootBuiltInRoot, options = {}) {
|
|
let certificates = [];
|
|
// The end-entity and intermediates aren't built-in roots.
|
|
for (let cert of certChain.slice(0, -1)) {
|
|
certificates.push(this.parseCertificateInfo(cert, false, options));
|
|
}
|
|
// The last in the chain may be the root (for successful chains), which may
|
|
// be a built-in root.
|
|
let rootCert = certChain.at(-1);
|
|
if (rootCert) {
|
|
certificates.push(
|
|
this.parseCertificateInfo(rootCert, isRootBuiltInRoot, options)
|
|
);
|
|
}
|
|
return certificates;
|
|
},
|
|
|
|
/**
|
|
* Takes an nsIX509Cert and returns an object with certificate information.
|
|
*
|
|
* @param {nsIX509Cert} cert
|
|
* The certificate to extract the information from.
|
|
* @param {boolean} isBuiltInRoot
|
|
* Whether or not this certificate is a built-in root.
|
|
* @param {object} options
|
|
* @returns {object}
|
|
* An object with following format:
|
|
* {
|
|
* subject: subjectName,
|
|
* issuer: issuerName,
|
|
* validity: { start, end },
|
|
* fingerprint: { sha1, sha256 }
|
|
* }
|
|
*/
|
|
parseCertificateInfo(cert, isBuiltInRoot, options = {}) {
|
|
if (!cert) {
|
|
return {};
|
|
}
|
|
|
|
let certData = {
|
|
subject: cert.subjectName,
|
|
issuer: cert.issuerName,
|
|
validity: {
|
|
start: cert.validity.notBefore
|
|
? Math.trunc(cert.validity.notBefore / 1000)
|
|
: 0,
|
|
end: cert.validity.notAfter
|
|
? Math.trunc(cert.validity.notAfter / 1000)
|
|
: 0,
|
|
},
|
|
fingerprint: {
|
|
sha1: cert.sha1Fingerprint,
|
|
sha256: cert.sha256Fingerprint,
|
|
},
|
|
serialNumber: cert.serialNumber,
|
|
isBuiltInRoot,
|
|
subjectPublicKeyInfoDigest: {
|
|
sha256: cert.sha256SubjectPublicKeyInfoDigest,
|
|
},
|
|
};
|
|
if (options.rawDER) {
|
|
certData.rawDER = cert.getRawDER();
|
|
}
|
|
return certData;
|
|
},
|
|
|
|
// Bug 1355903 Transparency is currently disabled using security.pki.certificate_transparency.mode
|
|
getTransparencyStatus(status) {
|
|
switch (status) {
|
|
case Ci.nsITransportSecurityInfo.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE:
|
|
return "not_applicable";
|
|
case Ci.nsITransportSecurityInfo
|
|
.CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT:
|
|
return "policy_compliant";
|
|
case Ci.nsITransportSecurityInfo
|
|
.CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS:
|
|
return "policy_not_enough_scts";
|
|
case Ci.nsITransportSecurityInfo
|
|
.CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS:
|
|
return "policy_not_diverse_scts";
|
|
}
|
|
return "unknown";
|
|
},
|
|
|
|
/**
|
|
* Takes protocolVersion of TransportSecurityInfo object and returns human readable
|
|
* description.
|
|
*
|
|
* @param {number} version
|
|
* One of nsITransportSecurityInfo version constants.
|
|
* @returns {string}
|
|
* One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if version
|
|
* is valid, Unknown otherwise.
|
|
*/
|
|
formatSecurityProtocol(version) {
|
|
switch (version) {
|
|
case Ci.nsITransportSecurityInfo.TLS_VERSION_1:
|
|
return "TLSv1";
|
|
case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1:
|
|
return "TLSv1.1";
|
|
case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2:
|
|
return "TLSv1.2";
|
|
case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3:
|
|
return "TLSv1.3";
|
|
}
|
|
return "unknown";
|
|
},
|
|
|
|
/**
|
|
* Takes the securityState bitfield and returns reasons for weak connection
|
|
* as an array of strings.
|
|
*
|
|
* @param {number} state
|
|
* nsITransportSecurityInfo.securityState.
|
|
*
|
|
* @returns {Array<string>}
|
|
* List of weakness reasons. A subset of { cipher } where
|
|
* cipher: The cipher suite is consireded to be weak (RC4).
|
|
*/
|
|
getReasonsForWeakness(state) {
|
|
// If there's non-fatal security issues the request has STATE_IS_BROKEN
|
|
// flag set. See https://hg.mozilla.org/mozilla-central/file/44344099d119
|
|
// /security/manager/ssl/nsNSSCallbacks.cpp#l1233
|
|
let reasons = [];
|
|
|
|
if (state & wpl.STATE_IS_BROKEN) {
|
|
if (state & wpl.STATE_USES_WEAK_CRYPTO) {
|
|
reasons.push("cipher");
|
|
}
|
|
}
|
|
|
|
return reasons;
|
|
},
|
|
};
|