株式会社クリアコード > ククログ

ククログ


「以前のバージョンでは使えていた機能が新バージョンでは使えなくなった」時の原因の調べ方

この記事はQiitaとのクロスポストです

恐らくあまり知られていませんが、Firefoxはインストール先の browser/chrome/icons/default/main-window.ico の位置(Firefoxのインストール先が C:\Program Files\Mozilla Firefox\ であれば、 C:\Program Files\Mozilla Firefox\browser\chrome\icons\default\main-window.ico)にICO形式のアイコン画像を置くと、それがメインウィンドウのアイコンになるという機能がありました*1

(スクリーンショット:実際にアイコンを変えた様子)

この機能について、次のESR版であるESR68でも有効なのかどうかを確かめようとNightly 68で試してみたところ、アイコンが変化しないという結果を得られました。

ということは、Firefox ESR68では機能が削除されたのだな……と決めつけるのは早計です。開発版のNightlyで駄目だったからといって、そのバージョンのFirefoxが正式リリースされた後も駄目だとは限りません*2。たまたまNightlyでは機能していないだけなのか、それともこの方法が使えなくなってしまったのか、きちんと裏付けを取らないとはっきりした事は言えません。

この記事では、フリーソフトウェア・OSSで遭遇したこのような挙動の変化の経緯を実装を辿って調べる方法について、Firefoxでの事例を示します。

調査の取っかかりを探す

この事例では「ウィンドウのアイコン」がポイントになります。Windows上で動くFirefoxはWindowsのアプリケーションなので、ウィンドウのタイトルバーのアイコンを変えるには当然Windowsのアプリケーションの流儀に則った方法を使っているはずです。

Windows window icon win32 といったキーワードで検索してみたところ、Win32アプリケーションでウィンドウのアイコンを変える伝統的なやり方に WM_SETICON という定数で定められたメッセージを使う方法 があるという事が分かりました。

機能が動いているバージョンでどう実装されているかを調べる

確認してみたところ、前述の方法でのアイコン変更はFirefox ESR60やFirefox 66では機能していました。そこで、Firefox ESR60のソースコード中で SM_SETICON という定数が使われている箇所を検索してみたところ、クロスプラットフォームな実装ではなくWindows固有の実装の部分で、この定数が使われている箇所が見つかりました

このメソッドの冒頭で呼ばれているResolveIconName()というメソッドの定義を辿り、さらにその中で呼ばれている ResolveIconNameHelper() というメソッドの定義まで辿ってみたところ、「与えられたファイルハンドラからiconsdefault、とディレクトリを辿り、与えられたファイル名のアイコンファイルに対応するアファイルハンドラを得る」という処理が行われている様子がうかがえました。これはまさしく、前述の「特定の位置に置かれたアイコンファイルを参照する」という処理そのものに見えます。

この箇所は nsWindow クラスの SetIcon() メソッドに含まれていますので、今度はこのメソッドを呼んでいる箇所を探しました。すると、XULのwindow要素のid属性の値を参照してアイコンを設定しているらしい箇所が見つかりました。

  // "id" attribute for icon
  windowElement->GetAttribute(NS_LITERAL_STRING("id"), attr);
  if (attr.IsEmpty()) {
    attr.AssignLiteral("default");
  }
  mWindow->SetIcon(attr);

ということで、先の方法でウィンドウのアイコンを変更するという機能は、この一連の箇所によって実現されている事が分かりました。

機能が動いていないバージョンでどう実装されているかを調べる

機能が動いているバージョンでの実装が分かったので、今度は対応する部分のNightly 68での実装を見てみました。

先ほど見ていった実装のそれぞれについてNightly 68での状況を見ていくと、ほとんどの箇所には変更が見られませんでしたが、nsWindow クラスの SetIcon() メソッドを呼んでいる箇所に違いがありました。こちらでは、XULのwindow要素のicon属性の値を参照して、値があった時にだけアイコンを設定するというコードになっています。

  // "icon" attribute
  windowElement->GetAttribute(NS_LITERAL_STRING("icon"), attr);
  if (!attr.IsEmpty()) {
    mWindow->SetIcon(attr);

    NS_ENSURE_TRUE_VOID(mWindow);
  }

Firefoxのソースコード検索システムでは、そのまま続けてMercurialのblameの結果を見る事ができます。そこから当該箇所の変更が行われたコミットの情報を見ると、バグトラッカー上での対応するbugの番号が記載されており、Bug 1531836 - Each new xul window does a stat call to look for non-existant window specific iconsに辿り着きました。

bugの内容を読むと、どうやら起動処理の高速化の一環として、「まず存在しないファイルを探しに行く」のではなく「明示的にアイコンが指定されている時だけファイルを探しに行く」という方針に改める変更が行われ、その一環として前述の機能が削除されたという事のようです。

まとめ

以上、フリーソフトウェア・OSSで「以前のバージョンでは使えていた機能が新バージョンでは使えなくなった」という事態に遭遇した時の原因の調べ方の、Firefoxでの事例をご紹介しました。

ユーザーの目に触れやすい機能の変更はリリースノートに記載されることが多いですし、また、開発者向けに影響の大きな変更は技術情報が別途まとめられている事もあります。しかし、この例のように影響度の小さい変更は、そのような変更があったという事自体は特に告知されないままになっている事があります。そういった場合でも、フリーソフトウェア・OSSではソースコードやバージョン管理システムの変更履歴を辿って、原因を特定したり回避方法を見つけたりする事ができます。

クリアコードではフリーソフトウェア・OSSに対する技術サポートを有償にて提供しており、お客様からの「今まで使えていた機能が使えなくなった事の理由を調べて欲しい」「回避策があれば、それを教えて欲しい」といったご依頼を受けてこのような調査も行っています。
業務上でのフリーソフトウェア・OSSの利用でお困りの方は、是非メールフォームからご相談下さい。

*1 正確には、`(id文字列).ico` という名前でファイルを置いておくと、ウィンドウの内部的なID(XULの`window`要素の`id`属性の値)が一致するファイルが自動的に使われるという事になっています。

*2 その逆に、Nightlyでは使えていた機能が同じバージョンのリリース版Firefoxで使えなくなる場合もあります。例えば `xpinstall.signatures.required` という設定はビルド時のオプションによって機能するかどうかが切り替わるようになっており、NightlyとDeveloper Editionでは使えますが、ベータ版および正式リリース版のFirefoxでは機能しないようになっています。

タグ: Mozilla
2019-05-14

2019年5月4日前後から発生しているFirefoxのアドオン無効化問題の詳細な解説

「Firefox」でインストール済みアドオンが利用不能になる問題が発生中 - 窓の杜Firefox 66.0.4 リリース、拡張機能全滅問題を修正 | スラド ITなどで既報の通り、2019年5月4日前後から一般向けのリリース版Firefoxでアドオンを一切使用できない状態が発生していました。緊急の修正版としてリリースされたFirefox 66.0.4およびFirefox ESR60.6.2で現象は既に解消されており、また、既にインストール済みのFirefoxに対してもホットフィックスが順次展開されていますが、本記事では今回の問題の技術的な詳細を解説します。

ユーザー側で取れる対応

まず最初に、ユーザー側で取れる対応をまとめておきます。

  • Firefox 66.0.4以降またはFirefox ESR60.6.2以降に更新する。
  • 「Firefox調査」を通じて hotfix-update-xpi-signing-intermediate-bug-1548973 が自動的に反映されるのを待つ。
  • 有効期限が更新された中間証明書のファイルをダウンロードし、「オプション」→「プライバシーとセキュリティ」→「証明書」→「証明書を表示」で表示される証明書マネージャにおいて、「認証局証明書」の一覧の「インポート」ボタンから認証局証明書としてインポートする。
  • about:config で隠し設定の xpinstall.signatures.requiredfalse に設定する。(Firefox ESR、Developer Edition、Nightlyのみ有効で、Firefox 48以降の通常のリリース版Firefoxではこの方法は無効です)

Mozillaによるサポートが終了したバージョンのFirefoxでは「Firefox調査」を通じての修正の反映が行われないため、Firefoxの更新、証明書の手動でのインポート操作、または about:config での隠し設定の変更が必要になります。

修正が反映される前のバージョンのFirefoxを起動してアドオンが無効化されてしまった場合、証明書のインポートや about:config での暫定的対応を行った後でも「アドオンが依然として無効で、且つ、ユーザー操作で再度有効化できないまま」となる場合があります。これは、アドオンが署名の期限切れで強制的に無効化されたという情報がユーザープロファイル内に残留しているために発生する現象です。この状況に陥った場合、Firefoxを終了した状態でユーザープロファイル内の extensions.json を削除して状態をリセットすると、アドオンが自動的に有効化されます(あるいは、手動操作で有効化できる状態でアドオンマネージャに表示されるようになります)。

アドオンの電子署名の基本のおさらい

Firefoxでは、Firefox 43以降のバージョンからアドオンのインストールパッケージの電子署名が必須となっています

Firefoxのアドオンはその特性上、認証局証明書の差し替えや通信の読み取りなど、Firefox上でありとあらゆる事ができる物でした*1。これではユーザーを困らせたりユーザーの機密情報を狙ったりする事を企む攻撃者にとって格好の標的となり得るため、その種の攻撃を防ぐために、Firefox 43で以下のような仕組みが導入されました。

  • すべてのアドオンは、Mozilla Add-onsでダウンロード可能な形で公開する物も、それ以外も、審査のため一旦Mozilla Add-onsのサービスにファイルをアップロードしなくてはならない。
  • アップロードされたファイルは、エディター権限を持つ人の目視による審査を受け、受理された場合にのみMozillaによって電子署名が施される。電子署名により、エディターが審査した時のファイルと同じ内容のファイル(=安全が確認されたファイル)である事が保証される。
  • ユーザー環境のFirefoxは、入手したアドオンのパッケージの電子署名を検証し、妥当な署名である事を確認できた(=エディターが審査した通りの内容のファイルであると確認できた=安全が確認された)段階で初めてそのアドオンを使用可能にする。

ただ、これではアドオンの開発中や、社内用として機密情報を含めたアドオンを使いたい場合など、審査用にファイルを供出できないケースで支障があります。そのため、以下のような救済措置が存在しています。

  • この電子署名の検証は xpinstall.signatures.required という隠し設定を false にして無効化できる。
    • ただし、Firefox 48以降のバージョンでは、法人向けのFirefox ESRと開発者向けのDeveloper Edition、Nightlyでのみ有効で、通常リリース版のFirefoxではこの設定は機能しません。
  • about:debugging から一時的に読み込まれたアドオンに対しては、電子署名の検証は行われない。

アドオンの電子署名の詳細

ここでさらっと「電子署名が施される」と述べましたが、具体的には何が行われているのでしょうか。IE View WEの場合を例に取って解説します。

「インストール」ボタンからダウンロードできるインストールパッケージ ie_view_we-1.3.2-fx.xpi の実体はZIP形式のアーカイブですが、これを展開して取り出された中にある META-INF というフォルダとその内容がまさに電子署名の実体です。それぞれの内容は以下の通りです。

  • META-INF/manifest.mf: パッケージ内に含まれる各ファイルのハッシュ値の一覧
  • META-INF/mozilla.mf: パッケージ全体のハッシュ値
  • META-INF/mozilla.rsa: DER形式の電子署名データ

このうちの mozilla.rsa には、以下の情報が含まれています。

  • 各ファイルやパッケージ全体の情報と、Mozillaが持つ秘密鍵を組み合わせて導出した署名データ
  • 署名データの導出に使われた秘密鍵に対応する公開鍵、の情報を含む証明書

ユーザー環境のFirefoxは、実際のアドオンのパッケージに含まれるファイルの内容と公開鍵を使って署名データを導出し、それが mozilla.rsa に含まれる物(秘密鍵を使って導出された物)と一致するかどうかを以て、パッケージの内容が改竄されていないことを確認することができます。

この mozilla.rsa に含まれる証明書は、openssl コマンドを使うと以下のようにして内容を確認できます。

$ openssl pkcs7 -inform DER -text -print_certs < ie_view_we-1.3.2-fx/META-INF/mozilla.rsa
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1551780906376321839 (0x158908833dcb5b2f)
    Signature Algorithm: sha384WithRSAEncryption
        Issuer: C=US, O=Mozilla Corporation, OU=Mozilla AMO Production Signing Service, CN=signingca1.addons.mozilla.org/emailAddress=foxsec@mozilla.com
        Validity
            Not Before: Mar  5 10:15:06 2019 GMT
            Not After : Mar  4 10:15:06 2020 GMT
        Subject: C=US, ST=CA, L=Mountain View, O=Addons, OU=Production, CN=ieview-we@clear-code.com
        Subject Public Key Info:
            (省略)
        X509v3 extensions:
            (省略)
    Signature Algorithm: sha384WithRSAEncryption
         (省略)
-----BEGIN CERTIFICATE-----
(省略)
-----END CERTIFICATE-----

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1048580 (0x100004)
    Signature Algorithm: sha384WithRSAEncryption
        Issuer: C=US, O=Mozilla Corporation, OU=Mozilla AMO Production Signing Service, CN=root-ca-production-amo
        Validity
            Not Before: May  4 00:09:46 2017 GMT
            Not After : May  4 00:09:46 2019 GMT
        Subject: C=US, O=Mozilla Corporation, OU=Mozilla AMO Production Signing Service, CN=signingca1.addons.mozilla.org/emailAddress=foxsec@mozilla.com
        Subject Public Key Info:
            (省略)
        X509v3 extensions:
            (省略)
    Signature Algorithm: sha384WithRSAEncryption
         (省略)
-----BEGIN CERTIFICATE-----
(省略)
-----END CERTIFICATE-----

ここでは2つの証明書が出力されています。

  • CN=ieview-we@clear-code.com である証明書(CN=signingca1.addons.mozilla.org によって発行された):そのアドオン固有の物として発行されたコード署名証明書で、パッケージの電子署名に使われた物
  • CN=signingca1.addons.mozilla.org である証明書(CN=root-ca-production-amo によって発行された):中間証明書

CN=root-ca-production-amo の証明書は addons-public.crt というファイル名でFirefoxのリポジトリに含まれている自己署名証明書で、ビルド時にソースコード内に組み込まれ組み込みの証明書として認識される、ビルトインのルート証明書の1つです*2

ここで注目するべきなのは、mozilla.rsa に含まれる証明書のうち2番目、中間証明書の方の有効期限です。当該箇所を以下に抜粋します。

Not Before: May  4 00:09:46 2017 GMT
Not After : May  4 00:09:46 2019 GMT

これを見ると、有効期限が2017年5月4日0時9分46秒(UTC)から2019年5月4日0時9分46秒(UTC)となっています。日本標準時はUTCより9時間進んでいるので、日本では2019年5月4日の9時過ぎに有効期限を迎えた事になります。この瞬間以降、この中間証明書を含むアドオンのパッケージはすべて「期限切れの中間証明書に基づく証明書で署名されたファイル」と見なされ、「署名を検証できない」=「エディターが審査した通りの内容のファイルであると確認できない」=「安全が確認されていない」と判断されるため、新規インストールは拒絶され、インストール済みの物は無効化されるという結果になっていたわけです。

Mozillaが取った対応

上記の事を踏まえ、Mozillaではいくつかの対応が検討されたようですFirefoxのリリースブランチESR60用ブランチに実際に投入された変更を追うと、以下の3通りの方法が試された模様です。

  1. コード署名証明書の検証時にだけ現在時刻を2019年4月27日に巻き戻す
  2. 新しい中間証明書をハードコードし、それを証明書データベースに追加する*3
  3. 新しい中間証明書をハードコードし、CN=root-ca-production-amo の証明書に基づく証明書ツリーを検証する時に新しい中間証明書を代わりに使う

緊急のリリースとなったFirefox 66.0.4とFirefox ESR60.6.2には、この2番目の対応が反映されています。また、動的な反映が可能である事から、統計情報の収集・ユーザーのFirefox使用状況の収集を行うための仕組みを使って、この2番目の方法と同じ手順でインストール済みのFirefoxに暫定的対策を行う(有効な中間証明書を自動インポートする)という対応が展開されています。Firefox 66.0.3以前やFirefox ESR60.6.1以前を使用中の環境では、この対応が反映された事によってアドオンが再び使用可能になっているはずです。「オプション」→「プライバシーとセキュリティ」→「証明書」→「証明書を表示」で認証局証明書の一覧を表示し、Mozilla Corporation 配下に signingca1.addons.mozilla.org が表示されていれば、この対応が既に反映された状態であるという事を確認できます。

(スクリーンショット:認証局証明書の一覧に中間証明書が現れている状態)

一方、Firefox 66.0.5とFirefox ESR60.6.3には3番目の対応が採用されており、2番目の対応は不要になったとして取り除かれています*4

中間証明書の更新では解消されない問題

現時点では以上の通りの暫定的な対応がなされていますが、今回発生したすべての問題が解決されたわけではなく、一部のアドオンではさらに手動での対応が必要です。その中でも、アナウンスされている未解決の問題の1つである、一部のアドオンが中間証明書の更新後も有効にならない件は、ここまでで解説した電子署名の仕組みが大きく関係しています。

前述の説明の通り、アドオンは中間証明書によって発行されたそのアドオン固有の証明書によって電子署名を施された状態になっています。この証明書はアドオンごとの固有の識別子(ID)に基づいて発行されていますが、EPUBReaderなどの一部のアドオンでは、そもそもこの固有の識別子が開発元によって指定されていません。そのため、Mozilla Add-onsのシステムが自動的に一意なIDを割り当てているのですが、アドオンのファイルの最終更新日が変わったなどの契機で自動割り当てされたIDが代わってしまうと、中間証明書の更新後であっても、そもそも電子署名が不正と見なされてしまうという状況が発生します。そのため、アドオンを手動で再インストールして、電子署名に記録されたIDとアドオンの一時的なIDが一致する状態を作り直す必要があるという訳です。

他にも、コンテナータブに固有の情報が失われる、検索エンジンや起動時のホームページなどアドオンによって変更されたはずのFirefox側の設定が初期状態に戻ったままになる、といった問題が残されているようです。これらの問題が解決された場合、それらもまたホットフィックスとして展開されたり、もしくは緊急の修正版としてリリースされたりする可能性がありますので、引き続き注視が必要です。

暫定的な対応を取った後の注意点

冒頭に記載したとおり、Firefox ESR、Developer EditionおよびNightlyにおいては、 xpinstall.signatures.required による電子署名の検証の無効化によって今回の問題を回避できます。しかしながら、それは同時に、この設定と同時に導入された安全確認の仕組みを無効化する事をも意味します。企業での運用でユーザーによるアドオンのインストールを禁止していて、導入されるアプリケーションの影響範囲が厳密に制御されている状況であればよいのですが、個人でESR版を使用している場合、悪意の攻撃者によるアドオンが知らず知らずのうちに読み込まれるというリスクがあります。

この方法を取った場合は、Firefoxが更新されたり、ホットフィックスが反映されたりした事を確認できたら、早急に証明書の検証を有効化する事を強くお勧めします。

まとめ

以上、2019年5月4日以降で発生しているFirefoxのアドオン無効化問題の技術的な詳細を解説しました。

当社ではFirefoxの法人向けテクニカルサポートを有償で提供していますが、実際にお客様の環境でも影響が出ている例があります。

今回の問題に見られるような証明書の期限切れに起因する問題は、度々世間を騒がせています。記憶に新しい所では、2018年から2019年にかけても以下のニュースがありました。

証明書のように有効期限が存在するデータに依存するシステムを運用する場合、有効期限の到来には充分な余裕を持って対応できるような仕組みを作っておく必要がある、という事を思い知らされますね。

*1 これは従来型のアドオン(いわゆるXULアドオン)での話です。Firefox 57以降のバージョンで使用できるアドオン(WebExtensionsベースのアドオン)では、できる事が技術的に制限されているため、危険性は大幅に減じています。

*2 編集履歴によると、このルート証明書は2015年に電子署名の仕組みが導入された時に追加されて以降特に変更された様子はありません。ルート証明書自体は2025年3月15日まで有効なので当面は問題無く利用できますが、このファイルがもし今後更新されないままであれば、2025年に再び同様のトラブルが起こる事になるでしょう。その場合、署名の検証を無効化できなくなっている現在のリリース版Firefoxをサポート切れのまま使い続けていると、アドオンを再度有効化する手立てが全く無くなるという事にもなりかねません。これらの観点からも、サポート切れのバージョンのFirefoxを使い続ける事にはリスクがあり、原則としてサポートが継続しているバージョンを使い続ける事が望ましいという事が言えます。

*3 各アドオンの署名データに含まれる中間証明書と同じSubjectの中間証明書がFirefoxの証明書データベースにも含まれるようになり、以後はそちらが優先的に認識されるために中間証明書の期限切れ問題が解消する、という理屈です。

*4 一度証明書データベース登録された中間証明書は、明示的に削除しない限りはFirefox 66.0.5への更新後もそのまま残ります。

タグ: Mozilla
2019-05-07

回帰テストの対象は適切に設定しよう

先日、Bug 1541748 - New tab and restored tab notified via tabs.onCreated can have invalid (too large) index という報告をFirefoxのバグトラッキングシステムに行った結果、提出したパッチがFirefox 68に反映される事になりました。

とはいえ、実はこのパッチで追加されるコードは皆さんのお手元にインストールされるFirefoxには反映されません。何故なら、これは自動テストを追加するだけのパッチだからです。

パッチが投入されるまでの経緯

この報告は、「Firefoxのアドオン開発に使用するWebExtensionsのいくつかのAPIにおいて、タブの位置を示すindexというプロパティが不正な値になる場合がある」という物でした。ただ、これは実はWebExtensions APIの実装の問題ではなく、Firefoxのタブが内部的に持っている対応する情報自体が壊れていたというのが真相でした。

(図:壊れた内部状態が露出した事が原因で、APIの情報が壊れている様子)

「根本的な原因」の方は Bug 1504775 - Index is wrong for restored tabs という別のBugで取り扱われていました。今回の報告と前後してそちらのBugの作業が進行していたため、そちらの方で修正が行われた事の影響として、報告からあまり間を置かずにWebExtensions APIの不具合も自然解消したという状況になっていました。

一般的に、このような「ある報告の原因が別の報告の修正で解消された」というようなケースでは、その旨を記して修正済み扱いとしたり、もしくは重複する報告であるとして処理する事が多いです。

(図:2つのBugに因果関係がある時、両方が閉じられる様子)

このような場合、そちら側に提出されたパッチはBugのクローズに伴って破棄されがちです。しかし今回はそうではなく、Bugはクローズされずに留め置かれ、新たに自動テストを加えるだけのパッチを追加投入する事が承認されました。

これは、根本原因の方のBugがFirefox内部の問題のみを取り扱う物であったのに対し、こちらで報告したBugはアドオンという外部アプリケーションに対する互換性の問題を取り扱っていたからです。

テストは目的ごとに必要

根本原因が修正されたパッチでは、Firefox内部のAPIに対するテストケースは追加されていますが、WebExtensionsのテストケースには特に変更は加えられていませんでした。

今回はFirefoxがたまたま「タブの内部的なindexがそのままWebExtensions APIを通じてアドオンに露出する」という実装を取っていたために、WebExtensions API側の問題も偶然解消されました。しかし、それはあくまで現時点の実装がそうであったからというだけで、将来に渡ってそうであるという事の保証はどこにもありません

(図:内部向けの保証はあってもアドオン相手の保証は無いという様子)

よって、今後Firefox内部の実装やWebExtensions APIの実装が変更された場合に、人知れず再び同じ問題が起こるようになるという可能性は依然としてあります。

そのため、今回のパッチでWebExtensions APIのレイヤにおいて後退バグ*1が発生していないかを検出するための回帰テスト*2を追加することで、WebExtensions APIに意図せず影響を与えてしまう(そのような変更が気付かれずに投入されてしまう)という事故の発生を未然に防ぎ、APIの安定性を保証しやすくしたという事になります。

(図:保証する相手ごとにテストがある様子)

気をつけて欲しいのは、これは「単体テストと結合テストではレイヤが違うので、単体テストで検証済みの事もすべて結合テストで再検証しよう」という話ではない、という事です。同じ事を保証するテストがそれぞれの実装レイヤに含まれるという事はあり得ますが、それはあくまで結果的にそうなっているだけに過ぎません。テストは誰に対して何を保証するか、という観点で整備される事が重要です。

今回は「Firefox内部向け」と「アドオンという外部アプリケーション向け」のそれぞれで修正・互換性を保証する必要があったためにこうなったわけです。

まとめ

Firefoxに投入された自動テストのみのパッチの背景説明を通じて、テストの目的とテストを追加するかどうかの判断基準の一例をご紹介しました。

自動テストの追加は、ドキュメントの修正に次いで「外部のコントリビューターとして開発に参加する最初のステップ」として取り組みやすい作業です。自動テストが全く存在しないプロダクトや、自動テストがあっても特定の機能についてテストが不足しているというケースに遭遇したら、皆さんもぜひテストの追加に取り組んでみて下さい。独りでやりきるには不安があるという方は、スケジュールが合うOSS Gateのワークショップに参加してサポーターの人に相談してみても良いかもしれません。

*1 今まで期待通りに動いていた物が、別の変更の影響で期待通りに動かなくなってしまう事。

*2 ある問題を修正したときに、問題が発生しなくなっている事を検証するテストのこと。

2019-04-23

FirefoxやChromiumのアドオン(拡張機能)で効率よくタブを並べ替えるには

この記事はQiitaとのクロスポストです。

Firefox 57以降のバージョンや、Google Chromeをはじめ各種のChromiumベースのブラウザでは、アドオン開発に共通のAPIセットを使用します。タブバー上のタブを任意の位置に移動するAPIとしてはtabs.move()という機能があり、これを使うと、タブを何らかのルールに基づいて並べ替えるという事もできます。実際に、以下のような実装例もあります。

このようなタブの並べ替え機能を実現する時に気をつけたいポイントとして、 いかにAPIの呼び出し回数を減らし、効率よく、且つ副作用が少なくなるようにするか? という点があります。

ソートアルゴリズムの効率とは別の話

「並べ替え」「効率」というキーワードでソートアルゴリズムの事を思い浮かべる人もいるのではないでしょうか。確かにそれと似ているのですが、実際にはこの話のポイントはそこにはありません。

確かに、ソート自体は前述の2つのアドオンでも行っており、

  • バラバラの順番のタブをコンテナごとに整理する
  • URLなどの条件でタブの順番を整理する

という処理はまさにソートそのものです。しかし、現代のJavaScript実行エンジンは充分に高速なため、余程まずいアルゴリズムでもない限りJavaScriptで実装されたソートの速度自体は実用上の問題となりにくいです。

それよりも、その後に行う 「実際のタブを移動して、算出された通りの順番に並べ替える」 という処理の方が影響度としては深刻です。「タブ」というUI要素を画面上で移動するのは、純粋なデータのソートに比べると遙かにコストが大きいため、数百・数千個といったオーダーのタブをすべて並べ替えるとなると相応の時間を要します。また、タブが移動されたという事はイベントとして他のアドオンにも通知され、その情報に基づいて様々な処理が行われ得ます*1ので、そのコストも馬鹿になりません*2

タブの並べ替えには手間がかかる

実際の並べ替えの様子を図で見てみましょう。[i, h, c, d, e, f, g, b, a, j] という順番のタブを、[a, b, c, d, e, f, g, h, i, j] に並べ替えるとします。

(図:並べ替え前後)

ぱっと見で気付くと思いますが、これは a, b, h, i の4つを左右で入れ換えただけの物です。しかし、人間にとっては一目瞭然の事でも、プログラムにとっては自明な事ではありません(それを検出するのがまさにこの記事の本題なのですが、詳しくは後述します)。

最もナイーブな*3実装としては、元の並び順からタブを1つづつ取り出していくやり方が考えられます。その場合、行われる操作は以下のようになります。

(図:ナイーブな並べ替えの流れ。計8回の移動が生じている様子。)

というわけで、この場合だと10個のタブの並べ替えに8回の移動が必要となりました。元のタブの並び順次第では、移動は最大で9回必要になります。

ちなみに、tabs.move()は第一引数にタブのidの配列を受け取ることができ、その場合、第一引数での並び順の通りにタブを並べ替えた上で指定の位置に移動するという挙動になります。そのため、

browser.tabs.move([a, b, c, d, e, f, g, h, i, j], { index: 0 });

とすれば、APIの呼び出し回数としては確かに1回だけで済ませる事もできます。ただ、これは内部的には

browser.tabs.move(a, { index: 0 });
browser.tabs.move(b, { index: 0 + 1 });
browser.tabs.move(c, { index: 0 + 2 });
browser.tabs.move(d, { index: 0 + 3 });
browser.tabs.move(e, { index: 0 + 4 });
browser.tabs.move(f, { index: 0 + 5 });
browser.tabs.move(g, { index: 0 + 6 });
browser.tabs.move(h, { index: 0 + 7 });
browser.tabs.move(i, { index: 0 + 8 });
browser.tabs.move(j, { index: 0 + 9 });

といった移動操作と同等に扱われるため、タブが実際に移動されるコストは変わらず、タブが移動された事を通知するイベントもその都度通知されます。

ソートと同時にタブを移動するのではどうか?

ここまではArray.prototype.sort()でソートした結果に従って、後からまとめてタブを並べ替えるという前提で説明していました。では、Array.prototype.sort()を使わずに独自にソートを実装し、その並べ替えの過程で同時にタブも移動する、という形にするのはどうでしょうか?

例えばこちらのクイックソートの実装の過程にtabs.move()を呼ぶ処理を追加すると、以下のようになります。

// 再帰によるクイックソートの実装
function quickSort(tabIds, startIndex, endIndex) {
  const pivot = tabIds[Math.floor((startIndex + endIndex) / 2)];
  let left = startIndex;
  let right = endIndex;
  while (true) {
    while (tabIds[left] < pivot) {
      left++;
    }
    while (pivot < tabIds[right]) {
      right--;
    }
    if (right <= left)
      break;
    // 要素の移動時に、同じ移動操作をタブに対しても行う
    browser.tabs.move(tabIds[left], { index: right });
    browser.tabs.move(tabIds[right], { index: left });
    const tmp = tabIds[left];
    tabIds[left] = tabIds[right];
    tabIds[right] = tmp;
    left++;
    right--;
  }
  if (startIndex < left - 1) {
    quickSort(tabIds, startIndex, left - 1);
  }
  if (right + 1 < endIndex ){
    quickSort(tabIds, right + 1, endIndex);
  }
}
const tabs = await browser.tabs.query({windowId: 1 });
quickSort(tabs.map(tab => tab.id), 0, tabIds.length - 1);

先の [i, h, c, d, e, f, g, b, a, j] を [a, b, c, d, e, f, g, h, i, j] に並べ替えるという例で見てみると、

(図:クイックソートでの並べ替えの流れ。計4回の移動が生じている様子。)

このように、今度は4回の移動だけで並べ替えが完了しました。先の場合に比べると2倍の高速化なので、これは有望そうに見えます。

……が、そう考えるのは早計です。この例で並べ替えが4回で終わったのは、部分配列の左右端から要素を見ていくというアルゴリズムが、左右の端に近い要素が入れ替わる形になっていた [i, h, c, d, e, f, g, b, a, j] という元の並び順に対して非常にうまく作用したからに過ぎません。例えば入れ替わった要素の位置が左右の端から遠い位置にある [c, d, i, h, e, b, a, f, g, j] のようなケースでは、

(図:クイックソートでの並べ替えの流れ。計12回の移動が生じている様子。)

と、今度は12回も移動が発生してしまいます。クイックソートは場合によっては同じ要素を何度も移動する事になるため、タブの移動のコストが大きい事が問題となっている今回のような場面では、デメリットの方が大きくなり得るのです。

また、クイックソートには「ソート結果が安定でない」という性質もあります。各要素のソートに使う値がすべて異なっていれば並べ替え結果は安定しますが、Firefox Multi-Account Containersでの「タブのコンテナごとに整理する」というような場面では、同じグループの要素の順番がソートの度に入れ替わってしまうという事になります。このような場合は要素間の比較の仕方を工夫するか、別のソートアルゴリズムを使う必要があります。

どうやら、このアプローチでは一般的に効率よくタブを並べ替えるという事は難しいようです。一体どうすれば「人が目視でするように、可能な限り少ない移動回数で、タブを目的の順番に並べ替える」という事ができるのでしょうか。

diffのアルゴリズムを応用する

このような問題を解決するには、diff編集距離といった考え方が鍵となります。

Unix系の環境に古くからあるdiffというコマンドを使うと、2つの入力の差を表示する事ができます。Gitなどのバージョン管理システムにおいてもgit diffのような形で間接的に機能を利用できるようになっているため、各コミットで行った変更点を見るために使った事がある人は多いでしょう。例えば、A = [a, b, c, d] と B = [a, c, d, b] という2つの配列があったとして、AとBの差分はdiffでは以下のように表されます。

  a
- b
  c
  d
+ b

行頭に-が付いている行は削除(AにはあってBには無い)、+が付いている行は追加(Aには無くてBにはある)を意味していて、これを見れば、「Aのbを2番目から4番目に移動する」というたった1回の操作だけでAをBにできるという事が見て取れます。このような「最小の変更手順」を求めるのが、いわゆる「diffのアルゴリズム」です。

つまり、これを「タブの並べ替え」に当てはめて、 「現時点でのタブの並び順」と「期待される並び順」とを比較し、算出された差分の情報に則ってタブを実際に移動するようにすれば、最小の手間でタブを並べ替えられる というわけです。

JavaScriptでのdiffの実装

diffの実装というとdiffコマンドが真っ先に思い浮かぶ所ですが、diffをライブラリとして実装した物は各言語に存在しており、もちろんJavaScriptの実装もあります。以下はその一例です。

diffのアルゴリズムをタブの並べ替えに使うには、こういったライブラリをプロダクトに組み込むのがよいでしょう。ただ、その際には以下の点に気をつける必要があります。

  • ライブラリのライセンスが他の部分のライセンスと競合しない事。
  • ライブラリの内部表現を機能として利用できる事。
  • 入力として任意の要素の配列を受け受ける事。

ライセンスの事は当然として、内部表現が利用できるとはどういう事でしょうか。

前述の例の「行頭に-+が付く」という表現は、git diffなどで目にする事の多い「Unified Diff」形式に見られる表記ですが、これはあくまで最終出力です。ライブラリ内部では、相当する情報を構造化された形式で持っている事が多いです。npmのdiffであるjsdiffの場合は以下の要領です。

[
  { count: 1, value: [a] },                // 共通の箇所
  { count: 1, value: [b], removed: true }, // 削除された箇所
  { count: 2, value: [c, d] },             // 共通の箇所
  { count: 1, value: [b], added: true }    // 追加された箇所
]

先のテキストでの表現をパースしてこのような表現を組み立てる事もできますが、ライブラリの内部表現→出力結果のテキスト表現→内部表現風の表現 という無駄な変換をするよりは、素直に内部表現をそのまま使えた方が話が早いです。

また、ライブラリの入力形式には任意の配列を受け付ける物である方が望ましいです。文字列のみを入力として受け付けるライブラリでも使えなくはないのですが、内部ではどの実装も基本的に配列を使っています。こちらも、比較したい配列→ライブラリに与えるための文字列→内部表現の配列 という無駄な変換が必要の無い物を使いたい所です。

npmのdiffパッケージであるjsdiffは、これらの点で使い勝手が良いのでおすすめの実装と言えます。

diffのアルゴリズムを使ったタブの並べ替え

diffを使ってタブを並べ替えるには、「ソート前の配列」「ソート後の配列」に「作業中の状態を保持する配列」を加えた3つの配列を使います。

const tabs = await borwser.tabs.query({ currentWindow: true });

const beforeIds = tabs.map(tab => tab.id); // ソート前のタブのidの配列
const afterIds  = tabs.sort(comparator)
                      .map(tab => tab.id); // ソート後のタブのidの配列
let   currentIds = beforeIds.slice(0);     // 現在の状態を保持する配列(ソート前の状態で初期化)

次に、ソート前後のタブのidの配列同士の差分を取り、その結果に従ってタブを移動していきます。

const differences = diff.diffArrays(beforeIds, afterIds);
for (const difference of differences) {
  if (!difference.added) // 削除または共通の部分は無視する
    continue;
  // ここにタブの移動処理を書いていく
}

タブの移動による並べ替えという文脈では、「削除」の差分で操作されるタブは対応する「追加」の操作によっても操作されます。また、「なくなるタブ(=処理中に閉じなければいけないタブ)」は原則として存在しません。よって、差分情報のうち「共通」と「削除」は無視して、「追加」にあたる物のみを使用します。

「追加」の差分情報を検出したら、まずタブの移動先位置を計算します。tabs.move()はタブの位置をindexで指定する必要がありますが、この時のindexは以下の要領で求められます。

  // 以下、forループ内の処理

  // 移動するタブ(1個以上)
  const movingIds = difference.value;
  // 移動するタブ群の中の右端のタブを得る
  const lastMovingId = movingIds[movingIds.length - 1];
  // ソート後の配列の中で、移動するタブ群の右に来るタブの位置を得る
  const nearestFollowingIndex = afterIds.indexOf(lastMovingId) + 1;
  // 移動するタブ群がソート後の配列の最後に来るのでないなら、
  // 移動するタブ群の右に来るタブのindexを、タブ群の最初のタブの移動先位置とする
  let newIndex = nearestFollowingIndex < afterIds.length ? currentIds.indexOf(afterIds[nearestFollowingIndex]) : -1;
  if (newIndex < 0)
    newIndex = beforeIds.length;

  // 移動するタブ群の左端のタブの現在のindexを調べる
  const oldIndex = currentIds.indexOf(movingIds[0]);
  // 現在の位置よりも右に移動する場合、移動先の位置を補正する
  if (oldIndex < newIndex)
    newIndex--;

タブ群の移動先の位置を求めたら、いよいよtabs.move()でタブを移動します。このAPIは第1引数にタブのidの配列を渡すと複数のタブを一度に指定位置に移動できるので、APIの呼び出し回数を減らすためにもそのようにします。

  // 実際にタブを移動する
  browser.tabs.move(movingIds, {
    index: newIndex
  });
  // 現在の状態を保持する配列を、タブの移動後として期待される状態に合わせる
  currentIds = currentIds.filter(id => !movingIds.includes(id));
  currentIds.splice(newIndex, 0, ...movingIds);

タブの移動はAPIを介して非同期に行われますが、タブの移動指示そのものはまとめて一度にやってしまって構いません。むしろ、途中でtabs.move()の完了をawaitで待ったりすると、タブの並べ替えの最中に外部要因でタブが移動されてしまうリスクが増えるので、ここはバッチ的に同期処理で一気にやってしまうのが得策です。

ただしその際は、2回目以降のtabs.move()に指定するタブの移動後の位置として、前のtabs.move()の呼び出しで並べ替えが完了した後の位置を指定しなくてはならないという点に注意が必要です。そのため、ソート前後の配列とは別に「並べ替えが実施された後の状態」を保持する配列を別途用意しておき、それだけを更新しています。前述のコード中で「タブの現在の位置」を求める際に、tabs.Tabindexを参照せずにこの配列内での要素の位置を使用していたのは、これが理由なのでした。

実際の並べ替えの様子を、最初と同じ例を使って見てみましょう。[i, h, c, d, e, f, g, b, a, j] を [a, b, c, d, e, f, g, h, i, j] に並べ替えるとします。初期状態は以下のようになります。

  • beforeIds: [i, h, c, d, e, f, g, b, a, j]
  • afterIds: [a, b, c, d, e, f, g, h, i, j]
  • currentIds: [i, h, c, d, e, f, g, b, a, j]
  • 実際のタブ: [i, h, c, d, e, f, g, b, a, j]

この時、beforeIdsafterIdsを比較して得られる差分情報は以下の通りです。

// 実際にはvalueはタブのidの配列だが、分かりやすさのためにここではアルファベットで示す
[
  { count: 2, value: [i, h], removed: true },
  { count: 2, value: [a, b], added: true },
  { count: 5, value: [c, d, e, f, g] },
  { count: 2, value: [b, a], removed: true },
  { count: 2, value: [h, i], added: true },
  { count: 1, value: [j] }
]

前述した通り、ここで実際に使われる差分情報は「追加」にあたる以下の2つだけです。

  1. { count: 2, value: [a, b], added: true }
  2. { count: 2, value: [h, i], added: true }

まず1つ目の差分情報に基づき、aとbを移動します。移動先の位置は、bの右隣に来る事になるタブ(=c)のcurrentIds内での位置ということで、2になります。

(図:diffを使った並べ替えの流れ。まず2つのタブを移動する。)

この時、b, aだった並び順も併せてa, bに並べ替えています。タブの移動完了後は、それに合わせる形でcurrentIdsも [i, h, a, b, c, d, e, f, g, j] へと更新します。

次は2つ目の差分情報に基づき、hとiを移動します。移動先の位置は、iの右隣に来る事になるタブ(=j)のcurrentIds内での位置から求めます。jの位置は9ですが、これはhの現在位置より右なので、そこから1減らした8が最終的な移動先位置になります。

(図:diffを使った並べ替えの流れ。次も2つのタブを移動する。)

この時も、i, hだった並び順をh, iに並べ替えています。タブの移動完了後は、それに合わせる形でcurrentIdsも [a, b, c, d, e, f, g, h, i, j] へと更新します。

ということで、APIの呼び出しとしては2回、タブの移動回数としては4回で並べ替えが完了しました。冒頭の例の8回に比べるとタブの移動回数は半分で済んでいます。

クイックソートでは却って移動回数が増えてしまった、[c, d, i, h, e, b, a, f, g, j] からの並べ替えではどうでしょうか。この場合、比較結果は以下のようになります。

[
  { count: 2, value: [a, b], added: true },
  { count: 2, value: [c, d] },
  { count: 2, value: [i, h], removed: true },
  { count: 2, value: [e] },
  { count: 2, value: [b, a], removed: true },
  { count: 2, value: [f, g] },
  { count: 2, value: [h, i], added: true },
  { count: 1, value: [j] }
]

前述の通り、このうち実際に使われる差分情報は以下の2つだけです。

  1. { count: 2, value: [a, b], added: true }
  2. { count: 2, value: [h, i], added: true }

これは先の例と全く同じなので、APIの呼び出し回数は2回、実際のタブの移動は4回で済みます。

(図:diffを使った並べ替えの流れ。タブを4回移動している様子。)

これらの例から、diffのアルゴリズムを用いると安定して少ない移動回数でタブを並べ替えられるという事が分かります。diffのアルゴリズムを用いた差分の計算自体はタブの移動に比べれば一瞬で済みますので、タブの移動回数を少なく抑えられれば充分に元が取れると言えるでしょう。

まとめ

以上、diffのアルゴリズムを用いてブラウザのタブを効率よく並べる方法について解説しました。

冒頭で例として挙げたタブの並べ替え機能を持つアドオンに対しては、以上の処理を実際に組み込むプルリクエストを既に作成済みです。

これらの変更(およびこの記事)は、「ツリー型タブ」での内部的なタブの並び順に合わせてFirefoxのタブを並べ替える処理が元になっています。ツリー型タブではnpmのdiffとは別のdiff実装を使用していますが、diffの結果として得られる内部表現の形式はよく似ており、タブの並べ替え処理そのものは同じロジックである事が見て取れるはずです。

このように「最小の変更内容を計算して適用する」という考え方は、Reactで広まった仮想DOMという仕組みのベースとなっています。コンピューターが充分に高性能となり、富豪的プログラミングが一般化した現代においては、多少の事は力業で解決してしまって問題無い事が多いです。しかし、ネットワーク越しの処理や、携帯端末などデスクトップPCよりも性能が制限されがちな環境などで、何千・何万回といった単位で実行されるような処理にボトルネックがあると致命的な性能劣化に繋がりかねず、そのような場合には「コストの高い処理の実行頻度を減らしてトータルの性能を向上する」という正攻法での性能改善が依然として有効です。

なお、2者間で「変更箇所を検出する」というのは「共通部分を検出する」という事と表裏一体ですが、そのような計算は最長共通部分列問題と呼ばれ、diffのアルゴリズムの基礎となっています。どのような理屈で共通箇所を検出しているのか知りたい人は解説や論文を参照してみると良いでしょう。筆者は理解を諦めましたが……

*1 例えば、タブをツリー状の外観で管理できるようにするアドオンであるツリー型タブでは、親のタブが移動された場合は子孫のタブもそれに追従して移動されますし、既存のツリーの中にタブが出現した場合や既存のツリーの一部だけが別の場所に移動された場合などにも、ツリー構造に矛盾が生じなくなるようにタブの親子関係を自動的に調整するようになっています。

*2 ブラウザ自体のタブの並べ替えにかかる時間の事よりも、むしろ、こちらの副作用の方が問題だと言っていいかもしれません。

*3 愚直な。

2019-04-19

公開中のソフトウェアがWindows Defenderでマルウェアとして判定された場合の対応

当社ではIE View WEというアドオンを開発・公開しています。これはFirefox上で閲覧中のページやマッチングパターンに当てはまるURLのページをIEもしくは任意の外部アプリケーションで開くという物で、Firefox法人サポートでの需要を想定して、既存のアドオン「IE View」の仕様を参考にFirefox 57以降で使用できる形(WebExtensionsベース)でスクラッチで開発したという物です。

WebExtensionsではアドオンが直接任意の外部アプリケーションを起動する事はできず、Native Messaging Hostと呼ばれるヘルパープログラムを介する必要があります。Native Messaging Hostは任意の言語で実装することができますが、IE View WEのNative Messaging Hostの場合はGoで開発しており、一般のユーザのためにコンパイル済みのバイナリも公開・配布しています。

このNative Messaging Hostのバイナリについて、先日、「Windows Defenderでトロイ(マルウェア)と判定されている」というご指摘を頂きました。

結果的にはただの誤判定だったのですが、配布物がそのような警告を受ける状態となった事は当社では過去に無かったため、対応のノウハウが無く手探りでの対応とならざるを得ませんでした。本記事では、本件に際して実際に行った対応の内容をご紹介します。同様の事態に遭遇された方の参考になりましたら幸いです。

第一報〜状況確認〜バイナリ公開停止

最初に受け取ったのは、以下のような内容の指摘のメール(英語)でした。

件名: Native Messaging Host……
本文: Windows 10 32bit版のWindows Defenderで、トロイと判定されます。

これを受け、まず、本当に配布ファイルがWindows Defenderでマルウェアと判定されるのかどうかを確認しました。

IE View WEのNative Messaging Hostのバイナリは、現在の所GitHubのリリースページでのみ配布しています。まず公開中のファイルをダウンロードし、実際にWindows Defenderに判定させてみました*1。その結果、確かに Trojan:Win32/Azden.A!cl として判定される結果となりました。

そこで、公開中のバイナリをリリースページから削除し、以下の文言を表示するようにしました。

Note: ieview-we-host.zip is undistributed due to security reason for now. Please wait for a while.

註:ieview-we-host.zipはセキュリティ上の理由により現在公開を停止中です。しばらくお待ち下さい。

さらなる状況確認

次に確認したのは、ダウンロードした配布ファイルはこちらで作成してアップロードした物と同一なのかどうかという点です。

ダウンロード用のURLはHTTPSとなっており、サーバーからのダウンロード過程でファイルが攻撃者によって置き換えられてしまう可能性はありません。しかし、バイナリ自体に電子署名は施していないため、サーバー上にあるファイル自体がこちらでアップロードした物から差し替えられてしまっている可能性は0とは言えません*2。そのため、次の段階としてダウンロード後のファイルとアップロード前のファイルのハッシュ値をsha256sumコマンドで算出し、両者が一致するかどうかを確認しました。その結果、ハッシュ値は一致し、アップロードの前後でファイルは変化していない事を確認できました。

(なお、仮にこの段階でファイルのハッシュ値が一致しなかった場合、アップロードされた後でファイルが改竄されたという事になります。それが可能となるのは、当社エンジニアが使用しているSSH秘密鍵が流出したか、GitHubのWebサービスが攻撃を受けてファイルが改竄されたというケースにあたり、また別の対応が必要になってきます。)

アップロード前のファイルがマルウェア判定された場合の対応

アップロード前の状態と同一のファイルがマルウェアと判定されるということは、ファイルが作成された時点からそのような内容が含まれていたという事になります。そうなると、今度は以下の可能性が疑われます。

  • バイナリ作成者のPCがすでに攻撃者によって乗っ取られていた。
  • バイナリに含まれる外部ライブラリに攻撃コードが混入していた。

作業者のPCが攻撃者によって乗っ取られるというのは、現代でもあり得る脅威です。例えば、いわゆるフリーWi-Fiの使用時は、同一のLAN内に全く素性の知れない他人が接続してきている状況で、攻撃の踏み台として標的にされるという事は十分に起こり得ます。

ただ、今回の事例ではこの可能性はそう高くないと考えられました。作業者のPCはUbuntuのデスクトップ環境が動作しており、外部からの接続も公開鍵認証のSSH以外は受け付けないよう設定されていたからです。絶対数が少ない環境の上、フリーWi-Fiに接続する機会もほぼ無いため、あらかじめ攻撃を受けていて踏み台にされていたという事は考えにくいです。

また、現代のソフトウェアは多数の依存ライブラリを引用する形で成り立っている事が多く、その中にこっそり悪意あるコードが混入していたとしても容易に気付けないという状況にあります。実際、そのような攻撃が行われた事例や、攻撃が行われうる脆弱性が指摘された事例は複数あります。

こちらの可能性を疑い始めると、調査は困難になってきます。依存ライブラリもまた別のライブラリに依存しているという事が多く、場合によっては調査範囲が際限なく広がってしまうからです。

Microsoftへの連絡

ここで初めて、それまで意図的に敢えて除外していた、擬陽性であった可能性を考え始める事にしました。

現代のマルウェアは日々多種多様な亜種が発生しており、単純なパターンマッチングでは「本当はマルウェアなのにマルウェアとして判定されない」というケース(擬陰性)が発生しやすいです。そのため各セキュリティ対策ソフトのベンダーは、機械学習を用いた分析や実際のソフトウェアの動作の様子を監視するなどして、疑わしい振る舞いをするソフトウェアは積極的に脅威判定するようになっています。その結果、本来は無害なソフトウェアであってもマルウェアの疑い有りとして検出されるというケース(擬陽性)が発生してしまう事があります。

Microsoftではこのようなケースを想定して、提出されたファイルを詳細に分析する受付窓口を提供しています。Windows Defenderでマルウェアとして判定されたソフトウェアをここで提出すると、専門家が分析した上で「本当にマルウェアである」あるいは「マルウェアではない」といった判定を行った結果を連絡してくれます。

今回のように自作ソフトウェアがマルウェア判定されたケースでは、「Software Developer(ソフトウェア開発者)」を選択して報告します。実際の報告内容は以下のようにしました。

  • 会社名:ClearCode Inc.

  • 判定名:Trojan:Win32/Azden.A!cl

  • 定義ファイルバージョン:1.289.911.0

  • 追加の情報:

    This program is a native messaging host for a Firefox addon "IE View WE", and it provides following features:
    1) Read configurations from local config files placed under specific locations, distributed by the system administrator.
    2) Read registry to know where the executable file of the Internet Explorer (or Edge) is.
    3) Launch the Internet Explorer (or Edge) with some options.
    The source codes of the file are published under https://github.com/clear-code/ieview-we/tree/master/host and https://github.com/clear-code/mcd-go
    

    参考訳(※送信した報告は英語の方のみです)

    このプログラムはFirefox用アドオン「IE View WE」用のNative Messaging Hostで、以下の機能を提供します:
    1) システム管理者によって特定の位置に置かれたローカル設定ファイルから設定情報を読み取る。
    2) レジストリからIE(またはEdge)の実行ファイルの位置を調べる。
    3) いくつかのオプションを指定してIE(またはEdge)を起動する。
    ソースコードは以下のサイトで公開しています: https://github.com/clear-code/ieview-we/tree/master/host および https://github.com/clear-code/mcd-go
    

送信した報告内容はキューに溜められ、順次処理されていきます。有償のサポート契約や開発者ライセンスを持っている場合、高い優先度でキューに割り込む事もできるようですが、当社はそのようなライセンスを特に保持していないので、一般的なソフトウェア開発者として報告するに留まりました。

報告者への連絡と、追加の確認

急いで取れる対応はここまでということで、この時点で一旦報告者の方に状況を伝える事にしました。

Hello,

Thank you for the information. We've removed the downloadable file for now, until those files are confirmed as completely safe. We've report those files to Microsoft security researchers, and waiting for responses.

regards,

こんにちは

情報をありがとうございます。安全が確認できるまで、ダウンロード可能なファイルは削除する事にしました。これらのファイルはすでにMicrosoftのセキュリティリサーチャーに報告済みで、返事を待っている状態です。

それでは

また、複数のセキュリティ対策ソフトでの検知結果を横断して確認できるVirusTotalというサイトでも確認を行った所、Windows Defender以外にもいくつかのソフトで脅威判定されている様子が窺えました。具体的な結果は以下の通りです。

  • 32bit版バイナリ: 検出率: 2/70(現時点では0/70
    • Cylance: Unsafe (20190312)
    • Microsoft: Trojan:Win32/Azden.A!cl (20190307)
  • 64bit版バイナリ: 検出率: 1/65(現時点でも変わらず
    • Jiangmin: Trojan.Ebowla.k (20190312)

安全確認〜再公開

そうこうするうちに、Microsoftから分析完了の通知メールが届きました。メールに記載されていたリンクを開くと、以下のように記載されていました。

Submission details
host.exe

Submission ID: xxxx-xxxx-xxxx-xxxx-xxxx

Status: Completed

Submitted by: (送信者のMicrosoftアカウントのメールアドレス)

Submitted: Mar 12, 2019 11:19:43

User Opinion: Incorrect detection

Analyst comments:
We have removed the detection. Please follow the steps below to clear cached detection and obtain the latest malware definitions.

  1. Open command prompt as administrator and change directory to c:\Program Files\Windows Defender
  2. Run “MpCmdRun.exe -removedefinitions -dynamicsignatures”
  3. Run “MpCmdRun.exe -SignatureUpdate”

Alternatively, the latest definition is available for download here: https://www.microsoft.com/en-us/wdsi/definitions

Thank you for contacting Microsoft.

報告の詳細
host.exe

報告ID: xxxx-xxxx-xxxx-xxxx-xxxx

状態: 完了

送信者: (送信者のMicrosoftアカウントのメールアドレス)

送信日時: 2019年3月12日 11:19:43

ユーザーの意見: 誤判定

分析者のコメント:
私達は判定を削除しました。以下の手順で判定結果のキャッシュを消去し、最新のマルウェア定義を入手して下さい。

  1. コマンドプロンプトを管理者として開き、ディレクトリを「c:\Program Files\Windows Defender」に切り替える。
  2. 「MpCmdRun.exe -removedefinitions -dynamicsignatures」を実行する。
  3. 「MpCmdRun.exe -SignatureUpdate」を実行する。

もしくは、最新の定義ファイルは以下からもダウンロード可能です:https://www.microsoft.com/en-us/wdsi/definitions

表示されている時刻は日本時間です。11時台に報告して、結果が返ってきたのは14時〜16時にかけてでした*3。数日待たされる事も覚悟していたのですが、予想よりずっと早い回答でした。

案内のあった手順の通りに操作して再確認した所、バイナリはマルウェアとは検出されないようになりました。よって、これを以て安全の確認が取れたと見なし、以下の文言を添えて、公開を取り下げていたバイナリを再公開しました

Files named host.exe in ieview-we-host.zip may be detected as Trojan:Win32/Azden.A!cl by Microsoft Windows Defender, but it is false positive and actually safe. Those misdetectons have been reported to Microsoft, and go away after you update the malware definitions.

ieview-we-host.zipに含まれるhost.exeという名前のファイルがMicrosoft Windows DefenderによってTrojan:Win32/Azden.A!clと判定される事がありますが、これは擬陽性で、実際には安全です。この誤判定はMicrosoftに報告済みで、マルウェア定義の更新後は警告されなくなります。

また、報告者の方にも以下の通り連絡しました。

Hello,

The trojan alert was a false positive, and Microsoft researchers decided to remove them from detection. > After you update malware definitions, the alert will go away. Detailed steps described by Microsft:

(中略)

So we've re-published native messaging host binaries. Thanks again!

regards,

こんにちは

トロイ警告は擬陽性で、Microsoftのリサーチャーはこの判定結果を取り除く決定をしました。
マルウェア定義の更新後は、この警告は表示されなくなります。Microsoftによって案内された手順は以下の通りです:

(中略)

よって、Native Messaging Hostのバイナリを再公開しました。重ねてありがとうございます!

それでは

以上を以て、本件へのWindows Defenderに関する対応は完了としました。

まとめ

以上、Windows Defenderでマルウェアとして誤判定された場合の対応フローの例をご紹介しました。

被害の拡大を防ぐ事を第一に置くと、最初に取るべき対応は「実際にマルウェア判定されるか確認する」ではなく「サンプルとしてファイルをダウンロードした上で、バイナリの公開を停止する」だったと言えます。実際にマルウェア判定されるかどうかは公開を取り下げた後で行えば良かったにも関わらず、それを後回しにして「マルウェア判定されるのは事実か?」を先に確認してしまったのは不徳の致すところです。

今回は擬陽性での誤判定という事で決着しましたが、実際にマルウェアだったという場合にはまた別の対応が必要になってきます。そのような事は起こらないに越した事はありませんが、もし万が一そのような事態が発生した際には、被害の拡大を防ぐ事を最優先として対応したいです。

なお、2019年3月18日現在、VirusTotalで確認した限りでは、他の製品では脅威判定はなされない状態になっているようなのですが、Jiangminという製品で依然として脅威として判定されている模様です。Jiangminは中国系企業の製品で、JUSTセキュリティのバックエンドにも採用されているそうなのですが、公式サイトが中国語のみで提供されているため、報告の窓口が分からずお手上げとなっています。もし報告窓口をご存じの方がいらっしゃいましたら、情報をお寄せいただければ幸いです。

*1 この段階でもしマルウェアとして判定されなければ、報告者がファイルをダウンロードしたWebサイトで配布されているファイルが改竄済みの物である(改竄されたファイルが第三者によって配布されている)という事になります。

*2 電子署名を施していた場合、「署名した時点のファイルから改竄されていない事」が保証されるため、この確認は不要になります。

*3 32bit版と64bit版の各バイナリについて、結果が出るまでに若干タイムラグがありました。

タグ: Mozilla
2019-03-18

Firefox 67 以降でPolicy Engineによるポリシー設定でPOSTメソッドの検索エンジンを追加できるようになります

Firefoxのポリシー設定では、以下の要領でロケーションバーやWeb検索バー用の検索エンジンを登録することができます。

{
  "policies": {
    "SearchEngines": {
      "Add": [
        {
          "Name":               "検索エンジンの表示名",
          "IconURL":            "https://example.com/favicon.ico",
          "Alias":              "ex",
          "Description":        "検索エンジンの説明文",
          "Method":             "GET",
          "URLTemplate":        "https://example.com/search?q={searchTerms}",
          "SuggestURLTemplate": "https://example.com/suggest?q={searchTerms}"
        }
      ]
    }
  }
}

Firefox 66以前のバージョンではこの時、HTTPのメソッドとしてGETのみ使用可能で、POSTメソッドを要求する検索エンジンは登録できないという制限事項がありました。OpenSearchの検索プラグインではPOSTの検索エンジンも登録できたので、これはPolicy Engineだけの制限ということになります。

RESTの原則からいうと「何回検索しても同じ結果が返る事が期待される検索結果一覧ページを表示するためのリクエストはGETが当然」という事になるのですが、実際にはFirefoxのロケーションバーやWeb検索バーは「検索」に限らず、入力された語句を含む任意のHTTPリクエストを送信する汎用のフォームとして使えます。よって、適切な内容の「検索エンジン」を登録しておきさえすれば、FirefoxのUI上から直接「Issueの登録」や「チャットへの発言」を行うといった応用すらも可能となります。しかし、そういった「新規投稿」にあたるリクエストはPOSTメソッドを要求する場合が多いため、それらは残念ながらPolicy Engine経由では登録できませんでした。

この件について 1463680 - Policy: SearchEngines.Add cannot add effective search engine with POST method として報告していたのですが、最近になって「バックエンドとなる検索エンジン管理の仕組みの改良によって、仕組み的にはPOSTメソッドも受け取るようになったので、パッチを書いてみては?」と促されました。そこでさっそく実装してみた所、変更は無事マージされ、Firefox 67以降では以下の書式でPOSTメソッドとそのパラメータを指定できるようになりました。

{
  "policies": {
    "SearchEngines": {
      "Add": [
        {
          "Name":        "検索エンジンの表示名",
          "IconURL":     "https://example.com/favicon.ico",
          "Alias":       "ex",
          "Description": "検索エンジンの説明文",
          "Method":      "POST",
          "URLTemplate": "https://example.com/search",
          "PostData":    "q={searchTerms}"
        }
      ]
    }
  }
}

"Method"の指定を"POST"にする事と、"URLTemplate"ではなく"PostData"の方に{searchTerms}というパラメータを指定する事がポイントです。

今回の変更自体は別のBugでの改良に依存しているため、ESR60に今回の変更がupliftされるためには、依存する変更も併せてupliftされる必要があります。ESR版自体のメジャーアップデートも視野に入りつつあるこの時期ですので、そこまでの手間をかけてこの変更がESR60に反映されるかどうかについては、現状では何とも言えません。とはいえ、次のESRのメジャーアップデート以降で使用可能になる事は確実です。もしこの機能をお使いになりたいというESR版ユーザーの方がいらっしゃいましたら、期待してお待ちいただければと思います。

2019-03-01

Firefox ESR60でタイトルバーのウィンドウコントロールボタンが機能しなくなる事がある問題の回避方法

Firefox ESR60をWindows 7で使用していると、ウィンドウコントロール(タイトルバーの「最小化」「最大化」「閉じる」のボタン)が動作しなくなる、という現象に見舞われる場合があります。Firefoxの法人サポート業務の中でこの障害についてお問い合わせを頂き、調査した結果、Firefox自体の不具合である事が判明しました。

この問題は既にMozillaに報告済みで、Firefox 67以降のバージョンで修正済みですが、Firefox ESR60では修正されない予定となっています。この記事では、Firefox ESR60をお使いの方向けに暫定的な回避方法をご案内します。

問題の状況とその原因

この現象は、FirefoxのUIの設計に由来する物です。

一般的なWindowsアプリケーションは、タイトルバーなどを含めたウィンドウの枠そのものはWindowsに描画や制御を任せて、枠の内側だけでUIを提供します。それに対し、Firefoxのブラウザウィンドウでは、タイトルバー領域に食い込む形でタブを表示させるため、タイトルバーを含むウィンドウの枠まで含めた全体を自前で制御しています。そのため、タイトルバー領域に食い込む形でタブが表示されている場面では、Windowsが標準で提供しているウィンドウコントロールのボタンは実は使われておらず、それを真似る形で置かれた独自のUI要素で代用しています。

通常、これらの代用ボタンは他のUI要素よりも全面に表示されるため、タブなどの下に隠れる事はなく、ユーザーは見たままの位置にあるボタンをクリックできます。しかし、特定の条件下ではこれらの代用ボタンが他のUI要素の下に隠れてしまう形となり、ボタンをクリックできなくなってしまいます。

この問題が起こる条件は、以下の通りです。

  • Windows 7で、Windows自体のテーマとして「Classic」テーマを使用している。
  • ツールバー上の右クリックメニューから「メニューバー」にチェックを入れており、メニューバーが常時表示される状態になっている。
  • ウィンドウがwindow.open()で開かれ、その際、メニューバーを非表示にするように指定された。

Windowsのテーマ設定とFireofxのツールバーの設定を整えた状態で、w3schools.comのwindow.oepn()の各種指定のサンプルを開き「Try it」ボタンをクリックしてみて下さい。実際に、ボタンが機能しないためウィンドウを閉じられなくなっている*1事を確認できるはずです。

問題が再現した時のFirefoxの内部状態を詳しく調査すると、Classicテーマにおいては、この状況下ではタブバーのz-index(重ね合わせの優先順位)が2になるのに対し、ウィンドウコントロールのz-indexは常に1になるために、タブバーがウィンドウコントロールの上に表示されてしまっている状態である*2という事が分かりました。

なお、ClassicテーマはWindows 7以前のバージョンでのみ使用できる機能で、Windows 10以降では使えません*3。そのため、この問題はWindows 10以降では再現しません。

回避方法

原因が分かれば対策は容易です。

最も単純な対策は、ユーザープロファイル内にchromeという名前でフォルダを作成し、以下の内容のファイルをuserChrome.cssの名前で設置するというものです。

@namescape url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");

/* WindowsでClassicテーマが反映されている場合、 */
@media (-moz-windows-classic) {
  /* タイトルバーにタブを表示する設定で、フルスクリーン表示でない時は、 */
  #main-window[tabsintitlebar]:not([sizemode=fullscreen]) #titlebar-buttonbox {
    /* ウィンドウコントロールの重ね合わせ順位をタブバーよりも上にする */
    z-index: 3 !important;
  }
}

ただ、ユーザープロファイルの位置はFirefoxの実行環境ごとに異なる*4ため、管理者側でこの対応を全クライアントに反映するのは難しいです。クライアント数が多い場合は、Firefoxのインストール先のchromebrowser\chrome配下に置かれたCSSファイルを自動的に読み込むためのスクリプトMCD用設定ファイルに組み込むなどの方法をとるのがおすすめです。

修正の状況

原因が判明した時点で、本件はFirefox本体の問題としてbugzilla.mozilla.orgに以下の通り報告しました。

また、調査を行った時期にちょうどFirefoxのタブバー・タイトルバー周りの実装の仕方が変化していたため、最新の開発版でも状況を確認した所、Firefox 65以降ではWindows 7でなくても同様の問題が起こり、しかも今度はそもそもウィンドウコントロールが表示されなくなってしまっているという、より酷い状況でした。そのため、そちらは別の問題として以下の通り報告しました。

Firefox ESR60およびFirefox 64以前での問題(本記事で解説している問題)については、Firefox 65でタブバーの設計が変わったために現象としては再現しなくなった事と、セキュリティに関わる問題ではない事から、修正はされないという決定がなされています。

Firefox 65以降での問題については、既に修正のためのパッチを提供し、Firefox 67以降のバージョンに取り込まれる事が確定しています。Firefox 65に対しては修正はバックポートされず、Firefox 66については特に誰も働きかけなければ修正は反映されないままとなる見込みです。

まとめ

以上、Firefoxの法人向け有償サポートの中で発覚したFirefoxの不具合について暫定的な回避方法をご案内しました。

当社のフリーソフトウェアサポート事業では、当社が開発した物ではないソフトウェア製品についても、不具合の原因究明、暫定的回避策のご提案、および(将来のバージョンでの修正のための)開発元へのフィードバックなどのサポートを有償にて行っております。Firefoxのようなデスクトップアプリケーションだけでなく、サーバー上で動作する各種ソフトウェアについても、フリーソフトウェアの運用でトラブルが発生していてお困りの企業のシステム管理担当者さまがいらっしゃいましたら、メールフォームよりご相談下さい。

*1 そのため、ウィンドウを閉じるにはCtrl-F4などのキーボードショートカットを使うなどの、ボタンを使わない方法を使う必要があります。

*2 タブバーの右端はあらかじめウィンドウコントロールを重ねて表示するために余白領域が設けられていますが、現象発生時には、この余白領域の上ではなく下にウィンドウコントロールが表示されており、「ボタンは見えているのにその手前に透明な壁があってクリックできない」というような状況が発生しているという状況です。

*3 Classicテーマ風のテーマは存在していますが、Windows 7以前のそれとは異なり、単に配色等をClassicテーマ風にするだけの物です。

*4 安全のため、パスにランダムな文字列が含まれる形になっています。

2019-02-15

Gecko Embedded: 60ESR対応のフィードバック

60ESR対応フィードバックの顛末

クリアコードでは Gecko(Firefox)を組み込み機器向けに移植する取り組みを行っています。

この対応にあたっては OSSystems/meta-browser というYoctoレイヤをフォークして作業を行っていますが、対応が落ちついたバージョンについてはアップストリーム(OSSystems/meta-browser)に成果をフィードバックしています。例えば52ESRへのメジャーバージョンアップの対応はクリアコードの成果が元となっています。

さて、60ESRへのメジャーバージョンアップ対応についてもフィードバックをしていたのですが、最後まで自分たちでは解決できなかった問題が一つありました。今回は、OSSにはこんなフィードバックの仕方もあるのだという事例として紹介したいと思います。

meta-rustへの依存

52ESRから60ESRへの更新にあたって一番大きな問題となったのが、Firefox本体にRustで書かれたコードが導入された点です。通常、Firefoxをビルドする際にはrustupというコマンドでツールチェーンを導入します。クリアコードで60ESRのYoctoへのポーティングを開始した当初も、Rustのツールチェーンについてはrustupで導入したものを使用していました。

しかし、Yoctoでは一貫したクロスビルド環境を提供するために、ツールチェーン類も全てYoctoでビルドするのが基本です。このため、RustのツールチェーンについてもYoctoでビルドした物を使う方がよりYoctoらしいと言えます。調べてみたところ、既にmeta-rustというYoctoレイヤが存在していることが分かりましたので、これを利用することとしました。

ターゲット環境用のlibstd-rsを参照できない問題

meta-rustへの対応作業を進めていく中で、Firefoxのビルドシステムがターゲット環境用のlibstd-rsを発見してくれないという問題に遭遇しました。しかし、我々はRustのビルドの仕組みには不慣れであったため、それがどこの問題であるかをはっきりと切り分けることができていませんでした。一方で、meta-rustでのビルドの際に、libstd-rsのみビルド済みのものを導入することで、ひとまずこの問題を突破できることが分かりました。

その他発生していた問題については解決することができ、Firefoxのビルドが全て通るところまでは到達しました。

上記の回避策が正しい修正方法ではないということは分かっていたのですが、ではどこでどう修正をするのが正しいのかという点については、少し調査をしてみた限りでは判断がつかない状態でした。時間をかければいずれは解明できるのは間違いないのですが、我々には他にも取り組まなければいけない問題が山積していたため、その時点では、これ以上時間をかけることはできませんでした。

ひとまずOSSystems/meta-browserに対しては「こういう問題があってまだ調査中だから、マージするのはもうちょっと待ってね」という形でpull requestを投げておきました。

未解決のままマージされる

ところがしばらくすると、 Yocto/Open EmbeddedプロジェクトのKhem Raj氏が、OSSystems/meta-browserのメンテナであるOtavio Salvador氏に働きかけ、上記のlibstd-rsの問題が未解決であるにも関わらず、このpull requestをmasterにマージしてしまいました。上記の回避策はmeta-rustにはマージされていませんので、そのままではビルドを通すことすらできません。私の目から見るとちょっとした暴挙にも見えたのですが、状況についてはKhem Raj氏にも間違いなく伝わっているので、何か算段があるのだろうということでしばらく経過を見守ることにしました。

あとから分かったのですが、ちょうどYoctoの次のリリース向けの開発が始まるタイミングであったため、とりあえずmasterにマージしてCIに組み込んでしまってからの方が問題に取り組みやすいという事情があったようです。しばらく時間はかかりましたが、無事Khem Raj氏から問題を解決するpull requestが投げられました。

我々の方ではmeta-rustとmeta-browserのどちらで対処すべき問題なのかすら切り分けることができていませんでしたが、meta-browserで対処すべき問題だったようです。

本事例の感想

今回の件については、以下のような懸念から、ともすればフィードバックを躊躇してしまいがちな事例ではないかと思います。

  • このような中途半端なpull requestを送ると、アップストリームの開発者から罵倒されたりはしないか?
  • 「この程度の問題すら自分たちで解決できていない」ということを晒すことになるので、恥ずかしいなぁ。

前者については、確かにプロジェクトによってはそのようなpull requestが歓迎されないこともあるでしょう。とはいえ、今回の事例についてはそこそこ工数がかかる60ESRポーティング作業を粗方済ませており、その成果が有益と感じる開発者も多いはずです。また、後者については「聞くのは一時の恥、聞かぬは一生の恥」の良い事例ではないかと思います。結果的にはアップストリームの開発者の協力を得て問題を解決することもできて、成果をより多くの人に使ってもらえる状態にできたので、中途半端でもフィードバックをしておいて良かったなと感じています。

皆さんも「この程度のものは誰の役にも立たないだろう」「この程度のものを世に出すのは恥ずかしい」などと一人で勝手に思い込まないで、手元にある成果を積極的に公開してみてはいかがでしょうか?ひょっとすると、それが世界のどこかで悩んでいる開発者の問題を解決して、感謝してもらえるかもしれませんよ。

2019-01-24

Firefox 64およびFirefox ESR60.4以降で可能になる、ルート証明書の自動インポートの方法

Firefox 52以降で可能になったエンタープライズの証明書の自動インポート機能は、WindowsでActive Directoryのグループポリシー機能を使って配布された証明書をFirefoxから認識できるようになるという物でした。そのため、Windows以外の環境では使用できず、また、WindowsでもActive Directoryを運用していない場合はレジストリを直接編集して証明書を配布する必要がありました。

Firefox 64およびFirefox ESR60.4では新たに、PEM形式の証明書ファイルとして配布されたルート証明書の自動インポートが可能になりました。この機能はプラットフォームを問わず動作するため、LinuxやmacOSを企業で運用する場合にも使用できます。

以下、Ubuntu 16.04LTSの環境でCert Importerアドオンのテスト用のダミーのルート証明書(cacert.pemをインポートさせる場合を例として、具体的な手順を解説します。

ステップ1:証明書ファイルの配布

PEM形式の証明書ファイルは、以下の位置に設置します。

  • Windows:
    • %AppData%\MozillaC:\Users\(ユーザー名)\AppData\Roaming\Mozilla\Certificates
    • %LocalAppData%\MozillaC:\Users\(ユーザー名)\AppData\Local\Mozilla\Certificates
  • macOS
    • /Users/(username)/Library/Application Support/Mozilla/Certificates
    • /Library/Application Support/Mozilla/Certificates
  • Linux
    • ~/.mozilla/certificates
    • /usr/lib/mozilla/certificates

これら以外の位置に置かれた証明書ファイルはインポートできませんので、注意して下さい。

それでは実際に、cacert.pemをダウンロードして、上記のいずれかの位置に設置します。
ここでは ~/.mozilla/certificates の位置に置く事にしました。

$ mkdir -p ~/.mozilla/certificates
$ curl https://raw.githubusercontent.com/clear-code/certimporter/master/doc/cacert.pem > ~/.mozilla/certificates/cacert.pem

ステップ2:証明書ファイルをインポートするためのポリシー設定の作成と配布

以下の形式でポリシー設定ファイルを作成し、policies.json というファイル名で保存します。

{
  "policies": {
    "Certificates": {
      "Install": [
        "ファイル名1",
        "ファイル名2",
        ...
      ]
    }
  }
}

今回は cacert.pem 一つだけをインポートするため、以下のようになります。

{
  "policies": {
    "Certificates": {
      "Install": [
        "cacert.pem"
      ]
    }
  }
}

このファイルを、Firefoxの実行ファイルと同じ位置の distribution ディレクトリに設置します。
具体的には以下の要領となります。

  • Windows:
    • C:\Program Files\Mozilla Firefox\distribution\policies.json など
  • macOS
    • /Applications/Firefox.app/Contents/MacOS/distribution/policies.json など
  • Linux
    • /usr/lib/firefox/distribution/policies.json など

今回の実験環境では ~/opt/firefox-nightly/ 配下に置いたFirefox(Nightly)を使用するため、設置先は ~/opt/firefox-nightly/distribution/policies.json とします。

$ mkdir -p ~/opt/firefox-nightly/distribution
$ cat << END > ~/opt/firefox-nightly/distribution/policies.json
> {
>   "policies": {
>     "Certificates": {
>       "Install": [
>         "cacert.pem"
>       ]
>     }
>   }
> }
> END

ステップ3:Firefoxの起動と結果の確認

以上で証明書の自動インポート機能の準備は完了です。Firefoxを起動すると policies.json に列挙された証明書ファイルが自動的に検出され、ルート証明書としてFirefoxの証明書データベースにインポートされます。

以上の手順でインポートされた証明書は、設定画面の「プライバシーとセキュリティ」配下の「証明書を表示...」ボタンをクリックして開かれる証明書マネージャーの一覧上に現れます(グループポリシーで配布されたエンタープライズの証明書はこの一覧上には現れません)。
先の cacert.pem は「!example」という組織の「example.com」という名前の証明書になっていますので、実際に証明書の一覧に現れている事を確認してみて下さい。

(画像:証明書一覧を表示した所)

まとめ

以上、Firefox 64およびFirefox ESR60.4で可能となった、PEM形式の証明書ファイルによるルート証明書の自動インポートの手順をご案内しました。

Firefox 64では他にもポリシー設定にいくつかの新機能が加わっています。また、ポリシー設定に関する変更はFirefox ESR60へも随時反映されています。5月12日付の記事のポリシー設定の解説に現時点で判明している変更点を反映済みですので、併せてご覧下さい。

また、当社ではFirefoxの法人での利用中に発生した様々なトラブルに対するテクニカルサポートを有償でご提供しています。Firefoxの運用にお悩みのシステム管理者さまは、お問い合わせフォームよりお気軽にお問い合わせ下さい。

タグ: Mozilla
2018-11-30

PhabricatorでのFirefoxへのパッチ投稿方法

以前の記事で、Firefoxへのパッチ投稿の一手段としてMozReviewという仕組みがあることと、その使用方法を紹介しました。しかし、その後すぐにMozillaのコードレビューシステムがPhabricator完全移行してしまい、MozReviewの運用は止まってしまったようです。記事公開時点ではまだMozReviewを使う開発者が大半のように見えていたため、MozReviewの運用停止が近づいているということを把握できていませんでした。

今回は改めてPhabricatorでのパッチ投稿を実践してみたため、その方法を紹介します。

セットアップ

本記事で紹介するセットアップ方法はMozilla Phabricator User Guideを元にしています。詳細はそちらを参照して下さい。

事前に用意するもの
  • BMO(bugzilla.mozilla.org)のアカウント
    • まだBMOアカウントを取得していない場合は https://bugzilla.mozilla.org/createaccount.cgi で作成します。
    • Real nameにはYour Name [:ircnick]のような形で末尾にニックネームを付けておきましょう。
      • Phabricator上ではこのニックネームがユーザー名として表示されます。
      • ただし、既に存在するニックネームは使用できません。
    • Phabricatorにアクセスするためには、Bugzilla側で2段階認証を有効化しておく必要があります。
  • mozilla-centralのワーキングコピー
    • Firefoxへのフィードバックの仕方:Windows編等を参考に、hg clone https://hg.mozilla.org/mozilla-centralで取得して下さい。
    • 本記事ではバージョン管理システムとしてMercurialを使用する場合のみを対象とします
      • PhabricatorはGitでも使用できるようですが、本記事では対象としていません。
Phabricatorへのログイン確認

MozillaのPhabricatorは https://phabricator.services.mozilla.com/ でアクセスできます。
Loginボタンを押して、ログインを試みます。

Phablicatorログイン

認証はBugzilla側で行われるため、以下のようなボタンが表示されます。

Bugzilla認証

最初のアクセス時には、Bugzilla側での認証成功後、Phabricator上でのアカウント登録を促されます。

Phabricatorユーザー登録

アカウントの登録を完了させて、Phabricatorを使用できる状態にしておきましょう。

Arcanistおよびmoz-phabのセットアップ

パッチをコマンドラインから投稿するためには、ArchanistというPhablicatorのコマンドラインツールと、そのラッパーコマンドであるmoz-phabをインストール必要があります。

Archanist

Archanistのインストール方法は以下に記載されています。

Ubuntuの場合は比較的簡単で、依存パッケージをインストール後、ArchanistのGitリポジトリをcloneして、パスを通します。

$ sudo apt install php php-curl
$ mkdir somewhere/
$ cd somewhere/
$ git clone https://github.com/phacility/libphutil.git
$ git clone https://github.com/phacility/arcanist.git
$ export PATH="$PATH:/somewhere/arcanist/bin/"

Windowsの場合のインストール方法は以下に記載されています。

こちらについては筆者の手元で検証できていないため、本記事では省略します。

MozillaBuild を使用している場合の具体的な手順は以下の通りです。

  1. Visual Studio 2017 の Microsoft Visual C++ 再頒布可能パッケージをダウンロードし、インストールする。

  2. Windows版PHPのzipファイルをダウンロードし、展開した物をC:\PHPに置く。(検証はPHP 7.2の「VC15 x64 Non Thread Safe」と書かれている物で行いました)

  3. C:\PHP\php.ini-development の位置にあるファイルを C:\PHP\php.ini にコピーし、以下の通り編集する。

    • ;extension=php_curl.dll または ;extension=curl と書かれた行の行頭の ; を削除する。
    • ;extension_dir = "ext" と書かれた行を extension_dir = "C:\PHP\ext" に書き換える(行頭の ; を削除し、実際のパスを記入する)。
  4. MozillaBuild のシェル上で /c/PHP/php.exe -i | grep curl と実行し、結果に curl という行が含まれている事を確認する。

  5. Windows版Gitをダウンロードし、インストールする。

  6. GitBash を起動し、以下の操作で必要なツールをダウンロードする。

    $ mkdir ~/phabricator
    $ cd ~/phabricator
    $ git clone https://github.com/phacility/libphutil.git
    $ git clone https://github.com/phacility/arcanist.git
    
  7. MozillaBuild で以下のコマンド列を実行し、各ツールにパスを通す。

    $ echo 'export PATH=${PATH}:/c/PHP:${HOME}/phabricator/arcanist/bin:/c/Program\ Files/Git/bin' >> ~/.bash_profile
    $ echo 'export EDITOR=/usr/bin/vim' >> ~/.bash_profile
    $ source ~/.bash_profile
    

    MozillaのドキュメントにはテキストエディタとしてVim以外を使用する場合の手順も書かれていますので、他のテキストエディタを使いたい場合はそちらも併せて参照して下さい。

Ubuntu、Windowsのそれぞれの方法でArchanistのインストールが完了したら、APIキーを設定します。mozilla-centralのソースディレクトリ下で以下のコマンドを実行します。

$ arc install-certificate

表示されたURLをブラウザで開いてログインするとAPIキーが表示されますので、そのAPIキーをコマンドラインにコピー&ペーストすると、APIキーが取り込まれます。
APIキーは以下のようなJSON形式で~/.arcrcに書き込まれます。

{
  "hosts": {
    "https://phabricator.services.mozilla.com/api/": {
      "token": "xxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  }
}
moz-phab

moz-phabのインストール方法は、moz-phabREADME.mdに記載されていますので、そちらを参照して下さい。

単にmoz-phabコマンドをパスの通ったディレクトリにコピーし、実行権限を付けるだけで良いようです。以下はその操作例です。

$ cd ~/phabricator
$ git clone https://github.com/mozilla-conduit/review.git
$ echo 'export PATH=${PATH}:${HOME}/phabricator/review' >> ~/.bash_profile
$ source ~/.bash_profile

レビューリクエストの作成

以前の記事で紹介したように、Firefoxで何かパッチを投稿したい場合は、全てBugzilla上の該当Bugを起点に作業を行います。何か新機能を追加したり、不具合修正を行いたい場合は、まず該当するBugが既に存在するかどうかを確認し、無い場合は新たに自分で新しいBugをfileします。

該当Bugでソースコードに対して何か変更を行って、いざPhabricatorにパッチを投稿したいという状況になった場合、まずはMercurialで手元のリポジトリに変更をコミットします。

$ hg branch fix-bug-xxxxx
$ hg commit

このとき、コミットメッセージの形式に注意しましょう。具体的には以下のような形式にする必要があります。

Bug [Bug番号] - [Bugの概要(一行)]

以下、Bugの詳細についての記述...

Mercurialでリモートリポジトリにpushする際、上記のコミットメッセージのBug番号から自動的にBugzillaの該当Bugにパッチが投稿されます。

また、末尾にr?ircnickという形式でレビュアーを指定すると、push後に自動的に該当レビュアーにレビューリクエストを投げることもできます。このレビュアーの指定は、パッチを送信した後にPhabricatorのWeb UIから行うこともできますので、必ずしもコミットメッセージに含める必要はありません。

以下に、筆者が実際にパッチを投稿した際のコミットメッセージを示します。

Bug 1502786 - Break cycle between PureOmxPlatformLayer and OmxDataDecoder r?jya

OmxDataDecoder, OmxPromiseLayer and PureOmxPlatformLayer consist
circular reference by RefPtr, and no one sever the reference. As a
result their refcount never decrease to 0.
This commit sever it at PureOmxPlatformLayer::Shutdown() which is
called by OmxDataDecoder.

詳細な議論はBug番号から辿ることができるため、コミットメッセージには必ずしも詳細な記述は必要ないようです。有った方が好ましいとは思いますが、慣れていない場合には、まずはBug番号と一行サマリを適切に記載することに注力すると良いでしょう。

ローカルリポジトリへのコミットが完了したら、リモートリポジトリにsubmitします。

$ moz-phab submit

submitが完了した後、先ほどコミットした内容をhg exportで確認してみると、以下のようにDifferential Revision:という行が追加されていることに気が付きます。

# HG changeset patch
# User Takuro Ashie <ashie@clear-code.com>
# Date 1541472583 -32400
#      Tue Nov 06 11:49:43 2018 +0900
# Node ID 25c8e78baa9aa8189ca7026d7ac7868c69d483f3
# Parent  9f9a9234959f114825f58beee0cffbab82d0bb29
Bug 1502786 - Break cycle between PureOmxPlatformLayer and OmxDataDecoder r?jya

OmxDataDecoder, OmxPromiseLayer and PureOmxPlatformLayer consist
circular reference by RefPtr, and no one sever the reference. As a
result their refcount never decrease to 0.
This commit sever it at PureOmxPlatformLayer::Shutdown() which is
called by OmxDataDecoder.

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

...

この行はレビュー結果を受けてパッチを修正する際に必要になります。また、この行に記載されているURLをブラウザで開くと、Phabricator上でレビューリクエストを参照することができます。以後、レビュアーとのやりとりはこのページで行うことになります。

パッチの修正

レビュアーによってパッチがレビューされ、Phabricator上で修正箇所を指摘されたら、パッチを修正して再度Phabricatorにsubmitすることになります。この際、同一のレビューリクエストに対する修正であることを指定するために、先ほどと同じDifferential Revisionをコミットメッセージに含めてhg commitし、moz-phab submitします。

Mercurialでのパッチ管理方法は本記事のスコープ外のため割愛しますが、パッチ(コミット)が1つのみで、ローカルリポジトリに過去のバージョンが不要である場合、もっとも簡単な修正方法はhg commit --amendで前回のコミットをやり直す方法でしょう。この方法の場合、コミットメッセージは特に修正しなければ前回のままとなりますので、Differential Revisionも前回と同じものが使用されます。ローカルリポジトリの修正は上書きされてしまいますが、リモートリポジトリ上では過去のバージョンも管理され、その差分を確認することもできます。

Phabricator diffリビジョン

修正をsubmitしたら、Phabricator上でその修正に対応するコメントの「Done」にチェックを入れ、レビュアーのコメントに返信をします。この際も、最後にSubmitボタンを押すことを忘れないで下さい。なお、MozReviewの時とは違い、Phabricator上での会話が自動的にBugzillaにも投稿されるという機能は無いようです。

Bugzilla上でパッチを添付する場合とは事なり、Phabricator上でレビュアーの情報が紐付けられているため、変更の度に改めてレビュー依頼をし直す必要はありません。再度レビューしてもらえるのをおとなしく待ちましょう。

レビューが通ったら

レビュアーによってパッチに問題ないと判断された場合、以下のようにAcceptedのマークが付きます。

Phabricator Accepted

この状態になったら、パッチのランドが可能になります。Mozilla Phabricator User GuideのLanding Patchesの項によると、パッチのランドにはLandoというシステムを使うことを強く推奨するとなっていますが、mozilla-centralへのコミット権限が無い場合、このシステムを使用することはできません。実際に試してみたところ、以下のように弾かれてしまいました(筆者の権限はLevel 1)。

Landoエラー

コミット権限が無い場合は、これまでと同様に、Bugzilla側で「Keywords」欄にcheckin-neededというキーワードを付加しておいて、権限のある開発者にコミットしてもらえば良いようです。この際、Bugzilla側ではレビュー承認済みであるr=ではなくレビューリクエスト中であるr?のマークのままになっていることがあるようですが、Phabricator側でAcceptedになっていれば、構わずcheckin-neededにしてしまって問題無いようです。

Accept後のパッチ修正

単にAcceptされただけであればそのままランドしてしまえば良いだけですが、場合によっては「Acceptするけど、こことここだけは修正しておいてね」と言われる場合があります。この場合はAcceptedのマークは付きますが、パッチは修正して再度送信する必要があります。すると、マークが以下のように変わります。

Phabricator Other Diff Accepted

この場合、修正版のパッチを再度レビューしてもらう必要があるのか疑問に思うところでしょう。結論から言えば、特にレビューしてもらう必要は無いようです。自分で修正できたと判断すれば、そのままランドしてしまうことができます。ただし、指摘された箇所は全て「Done」にチェックを入れておきましょう。

Phabricator Doneフラグ

この時も、「Done」のチェック後にSubmitボタンを押す必要があります(チェックを付けただけでは送信されません)。

Backoutされたパッチの修正

一旦ランドされたパッチが自動テストの失敗によってBackoutされる事もあります。ここのような場合、Phabricator上のパッチは既に一旦Closedになってしまっているため、そのままでは修正を継続できません。

FAQによると、このようなケースでは以下の手順でパッチをReopenする必要があります。

  1. ページ最下部のコメント入力欄までスクロールする。
  2. 「Add Action...」のドロップダウンリストを開き、「Revision Actions」配下の「Reopen Revision」を選択する。
  3. 「Submit」をクリックしてアクションを確定する。

この操作を行うとパッチが「Accepted(レビュー完了済み)」の状態に戻りますので、hg commit --amendでパッチを修正してmoz-phab submitし、再度レビュアーの反応を待つ事になります。

まとめ

PhabricatorでのFirefoxへのパッチ投稿方法について紹介しました。

なお、本記事内で紹介した実例はBug 1502786: Memory leaks in OpenMAX PDMになります。以前Firefox本体にフィードバックしたOpenMAX IL対応パッチにバグがあることを発見したので、その修正を再度フィードバックしています。

元となるOpenMAX対応パッチについては、特にレビュアーを指定せずにとりあえずMozReviewで上げてみただけだったのですが、Mozillaの開発者の目に止まって勝手にレビューされ、本体にマージされるところまで進みました。やはりコードレビューシステムで登録しておいた方が開発者としてもレビューが捗るのかもしれませんね。

2018-11-15

タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|