ククログ

株式会社クリアコード > ククログ > Firefox ESR 102に更新して特定のWebサイトが証明書のエラーで接続できなくなった場合への対処方法

Firefox ESR 102に更新して特定のWebサイトが証明書のエラーで接続できなくなった場合への対処方法

はじめに

Firefox ESR 102 1 がリリースされてしばらく経過し、組織内のブラウザをESR 91からESR 102へと移行済みのところも増えてきました。 そのなかで、特定のWebサイトに関して、ESR 91まではアクセスできていたのにESR 102ではアクセスできなくなったというお問い合わせをいただくことがあります。

今回は、Firefox ESR 102にアップグレードすることで特定のWebサイトに接続できなくなるケースへの対応について解説します。

ESR 102で問題となった変更について

ESR 102では、証明書の検証時の「所有者共通名(Subject Common Name)」のフォールバック機能が廃止されました。

CA/Browser Forumの基本要件では2012年からSAN(Subject Alternative Name、subjectAltName)を使用することが求められています。 SANが未設定の場合の代替手段としては、信頼性が低い情報であるCNを参照しないことが推奨されています。 しかし、従来バージョンのFirefoxでは後方互換性のために、手動でインポートされた証明書でSANが未設定だった場合にのみ、代わりにCNを参照する動作が有効となっていました。

Google Chrome 66において、この動作がセキュリティ脆弱性の原因となり得ていたことを理由に完全に廃止されました。 これにより、Firefoxのみ古い仕様に対応し続ける必要性が低下したことから、Firefoxにおいても機能が廃止されました。 機能廃止のため、設定での再度の有効化はできません。

一般のWebサイトの閲覧時にはこの変更の影響はありません。 2 ただし、組織内のWebサイトでSANが未設定の証明書が使われていた場合、証明書のエラーでWebサイトを閲覧できなくなる恐れがあり、SANが設定された証明書への更新が必要です。

問題となるWebサイトがあるかどうかの確認方法

では、組織内のWebサイトでSANが未設定の証明書が使われているかはどのように判別するとよいのでしょうか。

Firefoxの場合、アドレスバーの鍵アイコンをクリックして、安全な接続 > 詳細を表示 > 証明書を表示とたどることで確認できます。

証明書の詳細情報が表示されるので、SANに対応している証明書を使っていれば、次のように主体者代替名が表示されます。(www.clear-code.com の例)

主体者代替名(SAN)に対応している証明書の詳細表示

これが表示されていなければ、SANに対応した証明書への更新が必要です。

エラーを回避して接続できるようにするには

本来であれば、証明書の更新で対応するのがまっとうな対処方法なのですが、すぐに対応が難しい場合もあります。 そのような場合にユーザーやシステム管理者側で取れる対応策はあるのでしょうか。

例外が発生すること自体は抑制できません。 代替手段としては、証明書の例外を自動承認させてサイトを閲覧できない問題を回避するという方法があります。

例えば、問題となるサイトが https://myserver だとして、証明書の例外の自動承認をするには、MCDと呼ばれるFirefoxの設定を管理する仕組みを利用します。3

具体的には次の手順でFirefoxをカスタマイズします。

  1. c:/Program Files/Mozilla Firefox/defaults/pref/autoconfig.js として次の内容のファイルを配置する
pref("general.config.obscure_value", 0);
pref("general.config.filename", "autoconfig.cfg");
pref("general.config.vendor", "autoconfig");
pref("general.config.sandbox_enabled", false);
  1. c:/Program Files/Mozilla Firefox/autoconfig.cfg として次の内容のファイルを配置する
const autoAcceptExceptionFor = (host, port = 443) => {
  const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
  const exceptionURL = port == 443 ? `https://${host}` : `https://${host}:${port}`;

  const { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
  const observer = {
    observe(aSubject, aTopic, aData) {
      switch (aTopic) {
        case 'domwindowopened':
          if (!aSubject.getInterface(Ci.nsIWebNavigation)
                       .QueryInterface(Ci.nsIDocShell)
                       .QueryInterface(Ci.nsIDocShellTreeNode || Ci.nsIDocShellTreeItem) // nsIDocShellTreeNode is merged to nsIDocShellTreeItem by https://bugzilla.mozilla.org/show_bug.cgi?id=331376
                       .QueryInterface(Ci.nsIDocShellTreeItem)
                       .parent) {
            const win = aSubject.getInterface(Ci.nsIDOMWindow);
            win.addEventListener('load', () => tryHandleWindow(win), { once: true });
          }
          return;
      }
    },
    QueryInterface(aIID) {
      if (!aIID.equals(Ci.nsIObserver) &&
          !aIID.equals(Ci.nsISupports)) {
        throw Components.results.NS_ERROR_NO_INTERFACE;
      }
      return this;
    }
  };

  const waitUntilEventOrTimeout = (target, type, timeout) => {
    return new Promise((resolve, reject) => {
      const win = (target.ownerDocument || target).defaultView || target;
      const listener = () => {
        target.removeEventListener(type, listener);
        win.clearTimeout(timer);
        resolve();
      };
      const timer = win.setTimeout(listener, timeout);
      target.addEventListener(type, listener);
    });
  };

  const WW = Cc['@mozilla.org/embedcomp/window-watcher;1'].getService(Ci.nsIWindowWatcher);
  const teardown = () => {
    WW.unregisterNotification(observer);
    //if (tab)
    //  tab.ownerDocument.defaultView.gBrowser.removeTab(tab);
  };

  let tab;
  const tryHandleWindow = async (win) => {
    Services.console.logStringMessage(`tryHandleWindow ${win.location.href}`);
    switch (win.location.href.replace(/(\?.*)(#.*)$/, '')) {
      case 'chrome://browser/content/browser.xhtml': {
        Services.console.logStringMessage(` => waiting MozAfterPaint`);
        await new Promise((resolve, reject) => {
          win.addEventListener('MozAfterPaint', resolve, { once: true });
        });
        Services.console.logStringMessage(` => MozAfterPaint done`);

        //win.openNewTabWith(exceptionURL);
        Services.console.logStringMessage(` => waiting TabAttrModified`);
        await new Promise((resolve, reject) => {
          const listener = (event) => {
            if (event.detail.changed != 'busy' ||
                event.target.getAttribute('busy') == 'true')
              return;
            win.setTimeout(() => {
              const uri = event.target.linkedBrowser.lastURI || event.target.linkedBrowser.currentURI;
              if (!uri || uri.spec.indexOf(exceptionURL) != 0)
                return;
              win.document.removeEventListener('TabAttrModified', listener);
              tab = event.target;
              resolve();
            }, 100);
          };
          win.document.addEventListener('TabAttrModified', listener);
        });
        Services.console.logStringMessage(` => TabAttrModified done`);

        const browser = tab.linkedBrowser;
        Services.console.logStringMessage(` => try load script`);
        browser.messageManager.loadFrameScript('data:text/javascript,(' + (async (waitUntilEventOrTimeout) => {
          const doc = docShell.QueryInterface(Components.interfaces.nsIWebNavigation).document;
          const win = doc.defaultView;
          await waitUntilEventOrTimeout(doc, 'AboutNetErrorLoad', 500);

          const advancedButton = doc.getElementById('advancedButton');
          if (advancedButton) {
            advancedButton.dispatchEvent(new win.MouseEvent('click', { button: 0 }));
            const exceptionDialogButton = doc.getElementById('exceptionDialogButton');
            if (exceptionDialogButton) {
              exceptionDialogButton.click();
              return; /* success case, go to next step: exceptionDialog */
            }
          }
          /* failure case */
        }).toString() + ')(' + waitUntilEventOrTimeout.toString() + ')', false, false);
        Services.console.logStringMessage(` => done`);
      }; break;
    }
  };

  WW.registerNotification(observer);
};
autoAcceptExceptionFor('myserver');

ポイントは、autoAcceptExceptionFor('myserver');として対象のホストを指定することです。 これにより、当該Webサイトの初回訪問時に、一瞬セキュリティの例外によるエラー表示がなされるものの、例外がスクリプトによって自動承認されるため、以降は支障なく当該Webサイトを閲覧できるようになります。

おわりに

今回は、Firefox ESR 102に更新して特定のWebサイトで証明書のエラーにより接続できなくなった場合への対処方法について解説しました。 正攻法はサーバー証明書の更新ですが、それが難しい場合には、Firefox側でセキュリティ例外を自動承認することで接続エラーを回避できます。 もしそのような事例に対応する必要があれば検討してみてください。

クリアコードでは、お客さまからの技術的なご質問・ご依頼に有償にて対応するFirefoxサポートサービスを提供しています。企業内でのFirefoxの運用でお困りの情シスご担当者さまやベンダーさまは、お問い合わせフォームよりお問い合わせください。

  1. ESRはExtended Support Releaseの略で、延長サポート版のFirefoxのこと。記事執筆時点の最新版はESR 102.5.0。

  2. 一般に使われている証明書はすでにSANに対応した証明書がほとんどであるため。

  3. MCDの詳細については設定の管理を参照のこと。