Bug 1553265 - add a document.addCertException function to about:certerror pages and use it there; also treat GeckoView error pages as CallerIsTrusted(Net|Cert)Error; r=snorp,johannh,baku

Add a document.addCertException function to about:certerror pages, and use it on the desktop certerror page.

Also, as the CallerIsTrusted* functions expect URLs like about:certerror, but GeckoView error pages are data URLs, and so need to be handled differently for these special error-page methods to be exposed on their documents.

Example usage of document.addCertException:
  document.addCertException(
    true|false /* true == temporary, false == permanent */
  ).then(
    () => {
      location.reload();
    },
    err => {
      console.error(err);
    }
  );

Differential Revision: https://phabricator.services.mozilla.com/D56974

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Thomas Wisniewski 2019-12-18 21:55:32 +00:00
parent af46e7328b
commit 1e12d53224
15 changed files with 289 additions and 48 deletions

View File

@ -98,41 +98,6 @@ class NetErrorParent extends JSWindowActorParent {
return false;
}
async addCertException(bcID, browser, location) {
let securityInfo = await BrowsingContext.get(
bcID
).currentWindowGlobal.getSecurityInfo();
securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(
Ci.nsICertOverrideService
);
let flags = 0;
if (securityInfo.isUntrusted) {
flags |= overrideService.ERROR_UNTRUSTED;
}
if (securityInfo.isDomainMismatch) {
flags |= overrideService.ERROR_MISMATCH;
}
if (securityInfo.isNotValidAtThisTime) {
flags |= overrideService.ERROR_TIME;
}
let uri = Services.uriFixup.createFixupURI(location, 0);
let permanentOverride =
!PrivateBrowsingUtils.isBrowserPrivate(browser) &&
Services.prefs.getBoolPref("security.certerrors.permanentOverride");
let cert = securityInfo.serverCert;
overrideService.rememberValidityOverride(
uri.asciiHost,
uri.port,
cert,
flags,
!permanentOverride
);
browser.reload();
}
async reportTLSError(bcID, host, port) {
let securityInfo = await BrowsingContext.get(
bcID
@ -270,13 +235,6 @@ class NetErrorParent extends JSWindowActorParent {
receiveMessage(message) {
switch (message.name) {
case "AddCertException":
this.addCertException(
this.browsingContext.id,
this.browser,
message.data.location
);
break;
case "Browser:EnableOnlineMode":
// Reset network state and refresh the page.
Services.io.offline = false;

View File

@ -511,7 +511,15 @@ function initPageCertError() {
}
function addCertException() {
RPMSendAsyncMessage("AddCertException", { location: document.location.href });
const isPermanent =
!RPMIsWindowPrivate() &&
RPMGetBoolPref("security.certerrors.permanentOverride");
document.addCertException(!isPermanent).then(
() => {
location.reload();
},
err => {}
);
}
function onReturnButtonClick(e) {

View File

@ -78,6 +78,8 @@
#include "mozilla/Services.h"
#include "nsScreen.h"
#include "ChildIterator.h"
#include "nsSerializationHelper.h"
#include "nsICertOverrideService.h"
#include "nsIX509Cert.h"
#include "nsIX509CertValidity.h"
#include "nsITransportSecurityInfo.h"
@ -1381,6 +1383,8 @@ Document::Document(const char* aContentType)
mReferrerInfo = new dom::ReferrerInfo(nullptr);
}
#ifndef ANDROID
// unused by GeckoView
static bool IsAboutErrorPage(nsGlobalWindowInner* aWin, const char* aSpec) {
if (NS_WARN_IF(!aWin)) {
return false;
@ -1402,10 +1406,148 @@ static bool IsAboutErrorPage(nsGlobalWindowInner* aWin, const char* aSpec) {
return aboutSpec.EqualsASCII(aSpec);
}
#endif
bool Document::CallerIsTrustedAboutNetError(JSContext* aCx, JSObject* aObject) {
nsGlobalWindowInner* win = xpc::WindowOrNull(aObject);
#ifdef ANDROID
// GeckoView uses data URLs for error pages, so for now just check for any
// error page
return win && win->GetDocument() && win->GetDocument()->IsErrorPage();
#else
return IsAboutErrorPage(win, "neterror");
#endif
}
already_AddRefed<mozilla::dom::Promise> Document::AddCertException(
bool aIsTemporary) {
nsIGlobalObject* global = GetScopeObject();
if (!global) {
return nullptr;
}
ErrorResult er;
RefPtr<Promise> promise =
Promise::Create(global, er, Promise::ePropagateUserInteraction);
if (er.Failed()) {
return nullptr;
}
nsCOMPtr<nsISupports> info;
nsCOMPtr<nsITransportSecurityInfo> tsi;
nsresult rv = NS_OK;
if (NS_WARN_IF(!mFailedChannel)) {
promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
return promise.forget();
}
rv = mFailedChannel->GetSecurityInfo(getter_AddRefs(info));
if (NS_WARN_IF(NS_FAILED(rv))) {
promise->MaybeReject(rv);
return promise.forget();
}
nsCOMPtr<nsIURI> failedChannelURI;
NS_GetFinalChannelURI(mFailedChannel, getter_AddRefs(failedChannelURI));
if (!failedChannelURI) {
promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
return promise.forget();
}
nsAutoCString host;
failedChannelURI->GetAsciiHost(host);
int32_t port;
failedChannelURI->GetPort(&port);
tsi = do_QueryInterface(info);
if (NS_WARN_IF(!tsi)) {
promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
return promise.forget();
}
bool isUntrusted = true;
rv = tsi->GetIsUntrusted(&isUntrusted);
if (NS_WARN_IF(NS_FAILED(rv))) {
promise->MaybeReject(rv);
return promise.forget();
}
bool isDomainMismatch = true;
rv = tsi->GetIsDomainMismatch(&isDomainMismatch);
if (NS_WARN_IF(NS_FAILED(rv))) {
promise->MaybeReject(rv);
return promise.forget();
}
bool isNotValidAtThisTime = true;
rv = tsi->GetIsNotValidAtThisTime(&isNotValidAtThisTime);
if (NS_WARN_IF(NS_FAILED(rv))) {
promise->MaybeReject(rv);
return promise.forget();
}
nsCOMPtr<nsIX509Cert> cert;
rv = tsi->GetServerCert(getter_AddRefs(cert));
if (NS_WARN_IF(NS_FAILED(rv))) {
promise->MaybeReject(rv);
return promise.forget();
}
if (NS_WARN_IF(!cert)) {
promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
return promise.forget();
}
uint32_t flags = 0;
if (isUntrusted) {
flags |= nsICertOverrideService::ERROR_UNTRUSTED;
}
if (isDomainMismatch) {
flags |= nsICertOverrideService::ERROR_MISMATCH;
}
if (isNotValidAtThisTime) {
flags |= nsICertOverrideService::ERROR_TIME;
}
if (XRE_IsContentProcess()) {
nsCOMPtr<nsISerializable> certSer = do_QueryInterface(cert);
nsCString certSerialized;
NS_SerializeToString(certSer, certSerialized);
ContentChild* cc = ContentChild::GetSingleton();
MOZ_ASSERT(cc);
cc->SendAddCertException(certSerialized, flags, host, port, aIsTemporary)
->Then(GetCurrentThreadSerialEventTarget(), __func__,
[promise](const mozilla::MozPromise<
nsresult, mozilla::ipc::ResponseRejectReason,
true>::ResolveOrRejectValue& aValue) {
if (aValue.IsResolve()) {
promise->MaybeResolve(aValue.ResolveValue());
} else {
promise->MaybeRejectWithUndefined();
}
});
return promise.forget();
}
if (XRE_IsParentProcess()) {
nsCOMPtr<nsICertOverrideService> overrideService =
do_GetService(NS_CERTOVERRIDE_CONTRACTID);
if (!overrideService) {
promise->MaybeReject(NS_ERROR_FAILURE);
return promise.forget();
}
rv = overrideService->RememberValidityOverride(host, port, cert, flags,
aIsTemporary);
if (NS_WARN_IF(NS_FAILED(rv))) {
promise->MaybeReject(rv);
return promise.forget();
}
promise->MaybeResolveWithUndefined();
return promise.forget();
}
promise->MaybeReject(NS_ERROR_FAILURE);
return promise.forget();
}
void Document::GetNetErrorInfo(NetErrorInfo& aInfo, ErrorResult& aRv) {
@ -1440,7 +1582,18 @@ void Document::GetNetErrorInfo(NetErrorInfo& aInfo, ErrorResult& aRv) {
bool Document::CallerIsTrustedAboutCertError(JSContext* aCx,
JSObject* aObject) {
nsGlobalWindowInner* win = xpc::WindowOrNull(aObject);
#ifdef ANDROID
// GeckoView uses data URLs for error pages, so for now just check for any
// error page
return win && win->GetDocument() && win->GetDocument()->IsErrorPage();
#else
return IsAboutErrorPage(win, "certerror");
#endif
}
bool Document::IsErrorPage() const {
nsCOMPtr<nsILoadInfo> loadInfo = mChannel ? mChannel->LoadInfo() : nullptr;
return loadInfo && loadInfo->GetLoadErrorPage();
}
void Document::GetFailedCertSecurityInfo(FailedCertSecurityInfo& aInfo,

View File

@ -4029,6 +4029,8 @@ class Document : public nsINode,
virtual bool UseWidthDeviceWidthFallbackViewport() const;
private:
bool IsErrorPage() const;
void InitializeLocalization(nsTArray<nsString>& aResourceIds);
// Takes the bits from mStyleUseCounters if appropriate, and sets them in
@ -4186,6 +4188,8 @@ class Document : public nsINode,
static bool AutomaticStorageAccessCanBeGranted(nsIPrincipal* aPrincipal);
already_AddRefed<Promise> AddCertException(bool aIsTemporary);
protected:
void DoUpdateSVGUseElementShadowTrees();

View File

@ -225,6 +225,9 @@
#include "nsIAsyncInputStream.h"
#include "xpcpublic.h"
#include "nsHyphenationManager.h"
#include "nsICertOverrideService.h"
#include "nsIX509Cert.h"
#include "nsSerializationHelper.h"
#include "mozilla/Sprintf.h"
@ -5749,6 +5752,31 @@ mozilla::ipc::IPCResult ContentParent::RecvBHRThreadHang(
return IPC_OK();
}
mozilla::ipc::IPCResult ContentParent::RecvAddCertException(
const nsACString& aSerializedCert, uint32_t aFlags,
const nsACString& aHostName, int32_t aPort, bool aIsTemporary,
AddCertExceptionResolver&& aResolver) {
nsCOMPtr<nsISupports> certObj;
nsresult rv = NS_DeserializeObject(aSerializedCert, getter_AddRefs(certObj));
if (NS_SUCCEEDED(rv)) {
nsCOMPtr<nsIX509Cert> cert = do_QueryInterface(certObj);
if (!cert) {
rv = NS_ERROR_INVALID_ARG;
} else {
nsCOMPtr<nsICertOverrideService> overrideService =
do_GetService(NS_CERTOVERRIDE_CONTRACTID);
if (!overrideService) {
rv = NS_ERROR_FAILURE;
} else {
rv = overrideService->RememberValidityOverride(aHostName, aPort, cert,
aFlags, aIsTemporary);
}
}
}
aResolver(rv);
return IPC_OK();
}
mozilla::ipc::IPCResult ContentParent::RecvAutomaticStorageAccessCanBeGranted(
const Principal& aPrincipal,
AutomaticStorageAccessCanBeGrantedResolver&& aResolver) {

View File

@ -1187,6 +1187,11 @@ class ContentParent final
mozilla::ipc::IPCResult RecvBHRThreadHang(const HangDetails& aHangDetails);
mozilla::ipc::IPCResult RecvAddCertException(
const nsACString& aSerializedCert, uint32_t aFlags,
const nsACString& aHostName, int32_t aPort, bool aIsTemporary,
AddCertExceptionResolver&& aResolver);
mozilla::ipc::IPCResult RecvAutomaticStorageAccessCanBeGranted(
const Principal& aPrincipal,
AutomaticStorageAccessCanBeGrantedResolver&& aResolver);

View File

@ -1467,6 +1467,14 @@ parent:
async AddPerformanceMetrics(nsID aID, PerformanceInfo[] aMetrics);
/*
* Adds a certificate exception for the given hostname and port.
*/
async AddCertException(nsCString aSerializedCert, uint32_t aFlags,
nsCString aHostName, int32_t aPort,
bool aIsTemporary)
returns (nsresult success);
/*
* Determines whether storage access can be granted automatically by the
* storage access API without showing a user prompt.

View File

@ -317,12 +317,14 @@ partial interface Document {
attribute EventHandler onpointerlockerror;
};
// Mozilla-internal document extensions specific to error pages.
partial interface Document {
[Func="Document::CallerIsTrustedAboutCertError"]
Promise<any> addCertException(boolean isTemporary);
[Func="Document::CallerIsTrustedAboutCertError", Throws]
FailedCertSecurityInfo getFailedCertSecurityInfo();
};
partial interface Document {
[Func="Document::CallerIsTrustedAboutNetError", Throws]
NetErrorInfo getNetErrorInfo();
};

View File

@ -20,6 +20,9 @@ const APIS = {
GetPrefs: function({ prefs }) {
return browser.test.getPrefs(prefs);
},
RemoveCertOverride: function({ host, port }) {
browser.test.removeCertOverride(host, port);
},
RestorePrefs: function({ oldPrefs }) {
return browser.test.restorePrefs(oldPrefs);
},

View File

@ -118,6 +118,13 @@ this.test = class extends ExtensionAPI {
return Services.telemetry.getHistogramById(id).add(value);
},
removeCertOverride(host, port) {
const overrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
overrideService.clearValidityOverride(host, port);
},
async setScalar(id, value) {
return Services.telemetry.scalarSet(id, value);
},

View File

@ -89,6 +89,22 @@
}
]
},
{
"name": "removeCertOverride",
"type": "function",
"async": true,
"description": "Revokes SSL certificate overrides for the given host+port.",
"parameters": [
{
"type": "string",
"name": "host"
},
{
"type": "number",
"name": "port"
}
]
},
{
"name": "setScalar",
"type": "function",

View File

@ -142,6 +142,16 @@ class NavigationDelegateTest : BaseSessionTest() {
testLoadExpectError("file:///test.mozilla",
WebRequestError.ERROR_CATEGORY_URI,
WebRequestError.ERROR_FILE_NOT_FOUND)
val promise = mainSession.evaluatePromiseJS("document.addCertException(false)")
var exceptionCaught = false
try {
val result = promise.value as Boolean
assertThat("Promise should not resolve", result, equalTo(false))
} catch (e: GeckoSessionTestRule.RejectedPromiseException) {
exceptionCaught = true;
}
assertThat("document.addCertException failed with exception", exceptionCaught, equalTo(true))
}
@Test fun loadUnknownHost() {
@ -163,14 +173,32 @@ class NavigationDelegateTest : BaseSessionTest() {
}
@Test fun loadUntrusted() {
val uri = if (sessionRule.env.isAutomation) {
"https://expired.example.com/"
val host = if (sessionRule.env.isAutomation) {
"expired.example.com"
} else {
"https://expired.badssl.com/"
"expired.badssl.com"
}
val uri = "https://$host/"
testLoadExpectError(uri,
WebRequestError.ERROR_CATEGORY_SECURITY,
WebRequestError.ERROR_SECURITY_BAD_CERT)
mainSession.waitForJS("document.addCertException(false)")
mainSession.delegateDuringNextWait(
object : Callbacks.ProgressDelegate, Callbacks.NavigationDelegate, Callbacks.ContentDelegate {
@AssertCalled(count = 1, order = [1])
override fun onPageStart(session: GeckoSession, url: String) {
assertThat("URI should be " + uri, url, equalTo(uri))
}
@AssertCalled(count = 1, order = [2])
override fun onPageStop(session: GeckoSession, success: Boolean) {
assertThat("Load should succeed", success, equalTo(true))
sessionRule.removeCertOverride(host, -1)
}
})
mainSession.evaluateJS("location.reload()")
mainSession.waitForPageStop()
}
@Test fun loadUnknownProtocol() {

View File

@ -2060,6 +2060,19 @@ public class GeckoSessionTestRule implements TestRule {
});
}
/**
* Revokes SSL overrides set for a given host and port
*
* @param host the host.
* @param port the port (-1 == 443).
*/
public void removeCertOverride(final String host, final long port) {
webExtensionApiCall("RemoveCertOverride", args -> {
args.put("host", host);
args.put("port", port);
});
}
private interface SetArgs {
void setArgs(JSONObject object) throws JSONException;
}

View File

@ -3636,6 +3636,12 @@ public class GeckoSession implements Parcelable {
* @param uri The URI that failed to load.
* @param error A WebRequestError containing details about the error
* @return A URI to display as an error. Returning null will halt the load entirely.
* The following special methods are made available to the URI:
* - document.addCertException(isTemporary), returns Promise
* - document.getFailedCertSecurityInfo(), returns FailedCertSecurityInfo
* - document.getNetErrorInfo(), returns NetErrorInfo
* @see <a href="https://searchfox.org/mozilla-central/source/dom/webidl/FailedCertSecurityInfo.webidl">FailedCertSecurityInfo IDL</a>
* @see <a href="https://searchfox.org/mozilla-central/source/dom/webidl/NetErrorInfo.webidl">NetErrorInfo IDL</a>
*/
@UiThread
default @Nullable GeckoResult<String> onLoadError(@NonNull GeckoSession session,

View File

@ -41,6 +41,7 @@ let RPMAccessManager = {
getFormatURLPref: ["app.support.baseURL"],
getBoolPref: [
"security.certerrors.mitm.priming.enabled",
"security.certerrors.permanentOverride",
"security.enterprise_roots.auto-enabled",
"security.certerror.hideAddException",
"security.ssl.errorReporting.automatic",
@ -52,6 +53,7 @@ let RPMAccessManager = {
"services.settings.last_update_seconds",
],
getAppBuildID: ["yes"],
isWindowPrivate: ["yes"],
recordTelemetryEvent: ["yes"],
addToHistogram: ["yes"],
},