javax.swing.JEditorPaneを使って簡易ブラウザを作った。戻るボタンとリロードボタンは付けたが、まだフォームの処理や text/html 以外のContent-Typeの処理がない。
これに、先週の日記「Java用「winnytp://」プロトコルハンドラを作ってみたら簡単にできた」で作ったハンドラファクトリをセットしてみたところ、そのまま動いた。
ファイルのリンクをクリックするとそのサイトからそれをダウンロードする仕掛けになっている。しかし、プロトコルハンドラがダウンロード機能に対応していない。
import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import java.io.*; import java.net.*; public class Nyzilla extends TinyWebBrowser { public static void main(String[] args) { URL.setURLStreamHandlerFactory(new WinnytpURLStreamHandlerFactory()); new Nyzilla(); } protected String getDefaultPage() { return "winnytp://"; } protected String getWindowTitle() { return "Nyzilla 0.2"; } } class TinyWebBrowser { public static void main(String[] args) { new TinyWebBrowser(); } TinyWebBrowser() { SwingUtilities.invokeLater(new Runnable() { public void run() { createAndShowGUI(); } }); } protected String getDefaultPage() { return "http://www.yahoo.co.jp/"; } protected String getWindowTitle() { return "TinyWebBrowser 0.2"; } protected Dimension getDefaultWindowSize() { return new Dimension(750, 800); } JPanel browserPanel; JTextField addressField = new JTextField(); JButton backButton = new JButton("Back"); JButton reloadButton = new JButton("Reload"); JLabel statusArea = new JLabel(" "); Cursor waitCursor = new Cursor(Cursor.WAIT_CURSOR); Page currentPage = null; java.util.Stack pageStack = new java.util.Stack(); static final Font font = new Font("SansSerif", Font.PLAIN, 14); private void createAndShowGUI() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { } JFrame f = new JFrame(getWindowTitle()); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); browserPanel = new JPanel(); browserPanel.setOpaque(true); browserPanel.setLayout(new BorderLayout()); JPanel addressBar = new JPanel(new BorderLayout()); JLabel l = new JLabel("Address (URL): "); l.setFont(font); addressBar.add(l, BorderLayout.LINE_START); addressField.setFont(font); setAddress(getDefaultPage()); addressField.addActionListener(new AddressEnterAction()); addressBar.add(addressField, BorderLayout.CENTER); JPanel buttonPanel = new JPanel(new FlowLayout()); backButton.addActionListener(new BackButtonAction()); backButton.setEnabled(false); buttonPanel.add(backButton); reloadButton.addActionListener(new ReloadButtonAction()); buttonPanel.add(reloadButton); addressBar.add(buttonPanel, BorderLayout.LINE_END); browserPanel.add(addressBar, BorderLayout.PAGE_START); statusArea.setFont(font); browserPanel.add(statusArea, BorderLayout.PAGE_END); f.setContentPane(browserPanel); f.setSize(getDefaultWindowSize()); f.setVisible(true); } class Page { JScrollPane pane; JEditorPane editor; Page() { editor = new JEditorPane(); editor.setEditable(false); editor.setContentType("text/html"); pane = new JScrollPane(editor); pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); editor.addHyperlinkListener(new HyperlinkAction()); } void setPage(String url) throws IOException { if (url == null || url.length() == 0) return; try { browserPanel.setCursor(waitCursor); editor.setPage(url); setAddress(editor.getPage().toString()); } catch (IOException e) { showAccessError(e); throw e; } finally { browserPanel.setCursor(Cursor.getDefaultCursor()); } } } class HyperlinkAction implements HyperlinkListener { public void hyperlinkUpdate(HyperlinkEvent v) { HyperlinkEvent.EventType t = v.getEventType(); if (t == HyperlinkEvent.EventType.ACTIVATED) { String url = v.getURL().toString(); statusArea.setText(" "); replaceLocation(url); } else if (t == HyperlinkEvent.EventType.ENTERED) { String url = v.getURL().toString(); statusArea.setText(url); } else if (t == HyperlinkEvent.EventType.EXITED) { statusArea.setText(" "); } } } class AddressEnterAction implements ActionListener { public void actionPerformed(ActionEvent v) { String url = addressField.getText(); replaceLocation(url); } } private void replaceLocation(String url) { try { Page newPage = new Page(); newPage.setPage(url); if (currentPage != null) { pageStack.push(currentPage); backButton.setEnabled(true); browserPanel.remove(currentPage.pane); } currentPage = newPage; browserPanel.add(newPage.pane, BorderLayout.CENTER); refresh(); setAddress(newPage.editor.getPage().toString()); } catch (IOException e) { } } class BackButtonAction implements ActionListener { public void actionPerformed(ActionEvent v) { if (pageStack.isEmpty()) return; backButton.setEnabled(false); Page prevPage = (Page)pageStack.pop(); browserPanel.remove(currentPage.pane); currentPage = prevPage; browserPanel.add(currentPage.pane, BorderLayout.CENTER); refresh(); if (!pageStack.isEmpty()) { backButton.setEnabled(true); } } } class ReloadButtonAction implements ActionListener { public void actionPerformed(ActionEvent v) { String url = currentPage.editor.getPage().toString(); try { reloadButton.setEnabled(false); Page newPage = new Page(); newPage.setPage(url); browserPanel.remove(currentPage.pane); currentPage = newPage; browserPanel.add(newPage.pane, BorderLayout.CENTER); refresh(); setAddress(newPage.editor.getPage().toString()); } catch (IOException e) { } finally { reloadButton.setEnabled(true); } } } private void refresh() { browserPanel.validate(); browserPanel.repaint(); } private void setAddress(String url) { addressField.setText(url); addressField.setCaretPosition(0); } private void showAccessError(Exception e) { JOptionPane.showMessageDialog( browserPanel, e.toString(), "Access Error", JOptionPane.ERROR_MESSAGE ); } }
javax.swing.JEditorPaneを用いた簡易Webブラウザの例
「アンチウイルスベンダーがWinnyのCacheフォルダ内のウイルスを駆除しない理由」に書いたように、Winnyを媒介するウイルスの勢いが通常に比べて衰えにくい原因として、アンチウイルスソフトが「出て行く」ファイルに対する検疫を行っていないことがあるが、ならば、WinnyのCacheフォルダの中身をWindowsの「仮想ドライブ」としてマウントするソフトウェアを作って配布してはどうだろうか。
そうすれば、一般的なアンチウイルスソフトを用いてそのドライブにウイルススキャンをかけることで、Cacheフォルダ内のウイルス駆除ができるはずだ。その仮想ドライブでは、ファイル一覧の操作が復号されたファイル名でリストされ、ファイルの読み出し操作が復号しながらの読み出しとなり、削除操作が対応するファイルの削除となるようサポートされていればよい*1。(書き込み機能はいらない。)
これは結果として、「自分が何をやっているか(やらされているか)を知りなさい」ということも同時に達成できるわけで、一石二鳥だ。「キャッシュと嘘とファイル放流」に書いていたように、「Cacheフォルダ」の中身がそのままでは見えないようにあえて作りこまれていることがWinnyの不健全性の根源なのだから、それを取り除くのにちょうどよい。
*1 ついでに、「WinnyのDownフォルダをインターネットゾーンにする」のように、仮想ドライブ内のそれぞれのファイルにZoneIdが付くようにするとモアベターだ。
報道各社は、我孫子市の防災・防犯情報メール配信サービスでウイルス入りメールが配信された事故について、「何者かが外部からサーバーに不正侵入した」などと報道した。
千葉県我孫子市は、防災・防犯情報をメール配信しているサーバーシステムが外部から不正に侵入され、市民2583人にウイルスメールが送信されたと2日発表した。(略)我孫子署に報告し、被害届の提出を検討している。
(略)
市は直ちに配信サービスを停止して調査。その結果、何者かが外部からサーバーに不正侵入し、市のメールアドレスを使ってウイルスメールを送信したと判断した。(略)
市によると、何者かが市役所のシステムに侵入。システムが操作され1日午前9時20分ごろ、ウイルス感染メールが一斉に配信されたという。
市によると、一日午前九時二十分ごろ、何者かが外部から市のメール配信システムに侵入し、市のサーバーからウイルスを添付した偽メールを配信したという。
市によると、何者かが市役所のメールアドレスを使ってシステムに侵入。システムが操作され1日午前9時20分ごろ、ウイルス感染メールが一斉に配信されたという。
我孫子市では1日、メール配信サービスを停止して調査を実施。その結果、メール配信サーバーが外部から不正侵入を受けたことが判明した。
本当に侵入があったのか疑わしいので、我孫子市役所に電話して聞いてみた。広報室に電話すると、詳しいことはわからないとのことで、情報システム課につないで頂いた。情報システム課の担当の方にはたいへん丁寧に対応していただいた。そのやりとりはおおむね次のような感じの内容になった。
私: システムへの侵入はなかったのではないかと思うのですが、いかがでしょうか。
情報システム課: はい、システムへの侵入はありませんでした。
私: 以前からよくある事故と同じですよね? つまり、メーリングリストを用いた配信システムになっていて、そこにメールが流れただけと。つまり、あるメールアドレスに送信すれば登録会員全員にメールが送信される仕組みになっていて、誰でもそのメールアドレスにメールを送信できる状態になっていたという。
情: そのようです。
私: しかし、報道ではシステムへの侵入があったと伝えられています。これはよくないことと思うのですが、いかがでしょうか。
情: そうですね。私どももそのように発表したつもりはないのです。侵入されて情報を盗まれたということはないと伝えています。どうも新聞記者の方は、侵入があったということにしたいようで、面白いように書きたいということがあるんじゃないでしょうかね。
私: しかし、さきほどこの電話で最初にお話しした広報室の方は、「システムへの侵入があったのですか?」と聞くと、あったとお答えになりましたよ?
情: うーんやはり、広報の者はITに詳しくないものですから、言葉の使い方とか至らないところがあるのかなとおもいます。
私: 「千葉県警に被害を届ける方針」という報道もありますが、どのような被害を主張なさるのでしょうか。意図的に投げ込まれたのではなく、ウイルスによってランダムに送信されているメールがたまたま到着しただけですよね?
情: たまたまかどうかはまだはっきりしていませんが、被害を届けるかどうかも含めてまだ調べているところです。
(以下略)
市の広報がそのように発表したから、それをそのまま伝えざるを得ないという、報道機関の都合も理解できるけれど、このパターンの事故はしょっちゅう起きていて、侵入があったなんてためしはないのだから、報道機関は、ちゃんと事実かどうか疑ってかかって取材をしたうえで、報道してほしい。せめて、INTERNET Watchくらいは。
INTERNET Watchの当該記事が訂正された。
記事初出時、今回のウイルスメールが配信された原因について、「メール配信サーバーが外部から不正侵入を受けた」ためとしていました。しかし、 4日に我孫子市に再取材したところ、メール配信サービスで利用していたMLにウイルスメールが配信されたことが原因であることがわかりましたので、記事を修正しました。
しかし、訂正版の記事でも、「管理者のアドレスを詐称した何者かによって、ウイルスメールが配信されてしまったという」となっていて、意図的な攻撃だということになっている。
Fromアドレス詐称型のワームメールは普通、ランダムにメールアドレスを(Webブラウザのcacheや過去の送受信メールなどから)選んで無差別に送信しているだけなのだが、それによって起きた事故*1を指して、「管理者のアドレスを詐称した何者かによって」などと言ってよいのだろうか? 我孫子市がまだそう主張しているということなのだろうが、もっと懐疑的に書けないものだろうか。せめて、INTERNET Watchくらいは。
一方、こんな反応もあった。
こういう誤解をする人がけっこういるようだ。他にも、トラックバックを頂いたところのひとつも別の事故について、本件、システム侵入を受けたという報道があります*1が、そのような事実は無いとのことです。すなわち、「内部に存在するウイルス感染端末から、誤ってウイルス付きメールが配信されてしまった」という可能性があるようですね。
福井県観光振興課の課員は、仕事中にエロサイト巡回したり 出会い系に投稿したりして、ヘンなウィルス拾ったんじゃないの?
と同様の誤解をしている。
おそらく我孫子市はこの誤解をされることを最も嫌ったために、「外部から」ということを強調し、「外部から」という事態を素人記者に理解させるために、「不正に侵入」という表現を使ってしまったのではないか。
勝手に憶測するとこんな記者会見の風景が見えてくる。
記者: 市のメールマガジンの複数の購読者がウイルスを受信したと言っています。全員に送られているのでは?
市: はい、○千○百人の購読者の全員に送られたようです。
記者: 登録者の個人情報が漏れているということではないですか?
市: それはありません。市のシステムから送信されたものです。
記者: 市役所の端末がウイルスに感染して、ウイルスをばら撒いたということですか?
市: いえ違います。外部から不正に送られたものです。
記者: 不正侵入があったということですか?
市: ……。まあそんなところです。
記者: 不正侵入で個人情報を盗まれていませんか?
市: それはありません。
事実が何かということよりも、「ということにしたい」願望が当事者とマスコミにあって、その表れではないか。
事実候補 | 愚かな当事者 | マスゴミ | まともな当事者 | まともな報道機関 |
---|---|---|---|---|
個人情報が漏れてそれが使われた | それは避けたい、他の原因を探す | 大スクープ | その可能性がないか念のため調査 | この事象は普通それが原因じゃないよな |
不正侵入がありシステムを操作された | 違うけどそれでもいいや(↑よりはまし)(↓はどうせマスコミは理解しない) | 中スクープ | その可能性がないか念のため調査 | この事象は普通それが原因じゃないよな |
MLに意図的にウイルスが投げ込まれた | それだ、俺たち被害者 | 小スクープ(または理解不能) | その可能性がないか念のため調査 | その可能性もあるがそれは重要ではなく、配信システムの設定の不備がまだあるようだとして注意喚起 |
購読者が感染したためウイルスがMLに到着して流れた | その可能性に想像が及ばない | ネタにならない(または理解不能) | 普通はこれが原因、だけど「購読者が感染したため」は重要ではないし、記者には言わないほうがいい | 配信システムの設定の不備がまだあるようだとして注意喚起、「購読者が感染したため」は重要でないので略 |
第三者からたまたまウイルスがMLに到着して流れた | その可能性に想像が及ばない | ネタにならない(または理解不能) | ↑の可能性もあるがこれが原因ということでよい | 配信システムの設定の不備がまだあるようだとして注意喚起 |
*1 もちろん、意図的に送信される可能性がないわけではないので、その可能性を疑うことも重要ではあるが。
1日のプロトコルハンドラを改良して、ファイルごとに「キー消滅判定タイマー」の値を表示するようにしてみた。図1のように、ファイル名の左側にその値を表示し、値が1500以上のものについてリンク部分を強調表示するようにした。
図1の画面にあるリンク先はすべて、アドレスバーのサイト上のURLになっている*1。この例では、画面上2つのファイルで「キー消滅判定タイマー」の値が1500未満となっている。全体の479項目のうち 86パーセントが1500以上だったことを示している。
「キー消滅判定タイマー」とは、金子勇氏の著書によれば次のように説明されている。
タイマーを使ったキーの削除
では、ダウンロード不能となったキーはどのように削除するのでしょうか。Winnyはこのために「キーの寿命」を設けています。すなわち、キーのそれぞれに一定値のタイマーを設け、定期的に減じて“0”になったらダウンロード不可能とみなすのです。
拡散や検索の際に、クエリが完全なキャッシュファイル(オリジナルファイルかもしれない)を持っているノードを経由すると、クエリにはタイマーが一定値の新鮮なキーが詰められます。このクエリを受け取ったノードはキーのタイマーがリフレッシュされ、キーは延命します。(略)キーの寿命の初期値は、現在約1500秒程度に設定していますが、これは(略)
金子勇, Winnyの技術, アスキー, p.119
つまり、図1の強調表示されていない2つのファイルが1500よりかなり小さい値(450前後)となっているのは、他のホストからこのホストへ情報が流れてきた際に、タイマーが1500前後にリセットされないまま、ファイルの提供元IPアドレス(とポート)情報がこのホストのものとして書き換えられた*2ものと考えられる。元のホストから情報が出てから、それがこのブラウザに送られてくるまでに1000秒ほど経過しているということだろうか。
次に、URLにオプションを指定して、このホストが送ってくるファイルリストのすべてを表示(つまり、他のホストが送信可能と主張するファイルをも含めて表示)させたのが図2である。(図2と図1の違いは、6月25日の日記の図1と図2の違い。今日の図2が6月25日の図1のHTMLで、今日の図1が6月25日の図2のHTML。)
図2では、ほとんどのファイルでタイマー値が1500未満となっている。何箇所かにある1500以上のファイル(強調表示)のリンクにマウスポインタを載せて、リンク先のURLをステータスバーで確かめてみると、そのほとんど*3がアドレスバーのホストと一致していた。つまり、他のホストにあるファイルなのに1500を超えるということはほとんどないと言えるのかもしれない。
次に、図1と同様の表示を別のホストのURLについて表示させたところ、ファイルリストの全部についてタイマ値が1500未満となるところが何箇所かあった(図3)。そうしたサイトでは表示されるファイル数が極端に少ないという傾向があるように思えた。これは、cacheファイルが削除されているノードだということを意味しているのだろうか。
Winnyネットワークの規模(同時稼動ノード数)の推計については、古くは3年前のネットアーク社の松本氏によるもの、そしてネットエージェント社の杉浦氏によるもの、さらに最近ではeEye Digital Security社の鵜飼氏らによるものがある。
ネットアークは14日、P2Pノードの自動探索システム「P2P FINDER」を稼働開始したと発表した。WinMXとWinnyの国内ノードが対象となっており、6月3日から7月14日までに20万5,597ノードが発見されたとしている。
株式会社ネットアークは、同社のP2Pノードの自動探索システム「P2P FINDER」の稼動状況を発表し、8月1日時点で「Winny」と「WinMX」の利用者数が561,459ノードに達したと報告した。
(略)比率としてはWinMXとWinnyが3対1程度だとしている。
ネットアークは6月16日に「P2Pネットワーク実態調査2003」を発表しており、その時点ではWinMXが3万2,882ノード、Winnyが3万 2,496ノードで、合計6万5,378ノードだった。わずか1カ月で3倍以上に増加したように見えるが、そうではないようだ。今回稼働を開始したP2P FINDERは、前回の調査に利用したシステムをチューニングしたものにあたり、性能向上によって発見ノードが大幅に増加したという。つまり、「潜在する P2Pノード数は膨大にあり、それをすべて調べきれていない」(松本直人代表取締役)のが実状らしい。
同社がWinny検知システムを開発した当初、2004年5月時点の接続数は27万5,133ノード(ネットエージェント調べ)だった。2005年12月になってもWinnyネットワークには約30万ノードが接続しており、Winny経由の情報流出が相次いでいるにも関わらず、利用者は減少していない状況だ。
ネットエージェントは25日、P2P型ファイル共有ソフト「Winny」のノード数を発表した。4月10日から23日にかけて独自のWinny検知システムを使って調査したもの。平日で44万〜49万、土日だと50万〜53万以上のノード数を観測したという。
(略)ネットエージェントでは同社独自の検知システムを11台稼働することで、平日で延べ約350万台のノード情報を取得。IPアドレスなどで重複分を削除してユニークノード数を確定しているという。
鵜飼氏らは、Winnyのプロトコルや挙動を解析し、Winnyネットワークに参加しているユーザーの一覧を数時間で取得できるシステムも開発した。現在、常時数十万のユーザーがWinnyネットワークに参加していることが観測されており、潜在的には100万人程度のユーザーが存在するのではないかと推測している。
今や、金子勇氏の著書「Winnyの技術」(アスキー)や、日経NETWORK誌の記事「Winnyの通信解読に挑戦!」などによって、Winnyプロトコルの仕様は公知となっており、こうした調査は平均的なプログラマならば誰でも可能な状況にあるといえる。私も、これらの既存の調査の確かさを確認するため、追試を行ってみた。
使用した機材及び環境は以下である。
調査に使用したプログラムのアルゴリズムを図2に示す。指定されたノード(ホスト名とポート番号の組)に対してTCP/IPで接続し、Winnyプロトコルにしたがってコマンド10(「拡散クエリ送信要求」)を一個送信しながら、接続先からのコマンドを受信する。受信したコマンド4および13から他のノードについての情報を抽出し、新たに見つかったノードを優先度付きタスク待ち行列に投入して、スレッド数800のスレッドプールを用いて同時並行的にそれらについて同じ処理を繰り返す。優先度は、そのノード情報の作成時刻の新しいものを優先するものとした。タスク待ち行列が空になり、処理中のタスクが終了すると停止する*1。接続については、接続不能エラー発生時は1回で中止するものとし、正常接続時には30秒以上経過すると切断するものとした。
実行中にWindowsのタスクマネージャで観測したところによると、CPUの使用率は 60% 〜 80% 程度、インターネット側ネットワークの通信使用速度は最大 2 Mbps 程度、データ記録側ネットワークは平均 0.8 Mbps程度と、いずれもボトルネックとはなっていないようだった。
このときの実験結果を図1に示す。モニター出力をグラフ化したもので、横軸は経過時間(秒)、縦軸はノード数、赤色(上)のプロットはその時刻における見つかったノード総数(重複除く)であり、緑色(真ん中)は接続を試み終えた数、青色(下)は接続が正常に完了(コマンド97、0、1、2、3のハンドシェイクシーケンスを正常に完了)し終えた数である。
このように、3時間20分ほどで39万ノードを発見しそのすべてに接続を試行した。そのうち、57 % のノードが接続に成功した。1秒当たり平均 32.5ノードに対して接続試行できたことになる。
以下はモニター出力の冒頭の部分と最後の部分である。
0; 0, 0, 0, 0, NaN%, NaN%; 0; 2; NaN, NaN 10; 2492, 33, 0, 0, 0.0%, 0.0%; 1659; 802; 3.500, 0.000 20; 16565, 90, 34, 33, 37.8%, 36.7%; 15676; 802; 4.500, 1.700 30; 19826, 203, 58, 57, 28.6%, 28.1%; 18823; 802; 6.767, 1.933 40; 24152, 736, 556, 525, 75.5%, 71.3%; 22797; 802; 18.425, 13.925 50; 30748, 910, 686, 649, 75.4%, 71.3%; 29038; 802; 18.200, 13.720 60; 34058, 1086, 794, 757, 73.1%, 69.7%; 32177; 802; 18.100, 13.233 70; 38849, 1424, 1051, 1009, 73.8%, 70.9%; 36632; 802; 20.343, 15.014 80; 42730, 1757, 1322, 1273, 75.2%, 72.5%; 40173; 802; 21.962, 16.525 90; 44970, 1932, 1451, 1399, 75.1%, 72.4%; 42238; 802; 21.467, 16.122 100; 48513, 2332, 1721, 1654, 73.8%, 70.9%; 45384; 802; 23.320, 17.210 110; 51283, 2636, 1971, 1896, 74.8%, 71.9%; 47850; 802; 23.964, 17.918 120; 53646, 2865, 2097, 2019, 73.2%, 70.5%; 49984; 802; 23.875, 17.475 130; 56392, 3217, 2354, 2268, 73.2%, 70.5%; 52379; 802; 24.746, 18.108 140; 58784, 3487, 2566, 2477, 73.6%, 71.0%; 54498; 802; 24.907, 18.329 150; 61268, 3767, 2747, 2655, 72.9%, 70.5%; 56706; 802; 25.113, 18.313 160; 63774, 4094, 2996, 2899, 73.2%, 70.8%; 58894; 802; 25.587, 18.725 170; 66206, 4382, 3194, 3093, 72.9%, 70.6%; 61024; 802; 25.776, 18.788 180; 68231, 4652, 3385, 3277, 72.8%, 70.4%; 62780; 802; 25.844, 18.806 190; 70664, 4967, 3630, 3518, 73.1%, 70.8%; 64905; 802; 26.142, 19.105 200; 72863, 5300, 3859, 3743, 72.8%, 70.6%; 66765; 802; 26.500, 19.295 210; 74593, 5572, 4051, 3932, 72.7%, 70.6%; 68221; 802; 26.533, 19.290 220; 76438, 5894, 4250, 4128, 72.1%, 70.0%; 69744; 802; 26.791, 19.318 (略) 11890; 388941, 386167, 219832, 215572, 56.9%, 55.8%; 1977; 802; 32.478, 18.489 11900; 388978, 386476, 220028, 215767, 56.9%, 55.8%; 1703; 802; 32.477, 18.490 11910; 389023, 386790, 220225, 215961, 56.9%, 55.8%; 1439; 802; 32.476, 18.491 11920; 389071, 387119, 220429, 216157, 56.9%, 55.8%; 1156; 802; 32.476, 18.492 11930; 389120, 387448, 220622, 216346, 56.9%, 55.8%; 876; 802; 32.477, 18.493 11940; 389162, 387741, 220787, 216510, 56.9%, 55.8%; 630; 802; 32.474, 18.491 11950; 389201, 388087, 220994, 216714, 56.9%, 55.8%; 332; 802; 32.476, 18.493 11960; 389235, 388393, 221188, 216907, 56.9%, 55.8%; 47; 802; 32.474, 18.494 11970; 389254, 388660, 221363, 217080, 57.0%, 55.9%; 0; 802; 32.470, 18.493 11980; 389261, 388954, 221565, 217282, 57.0%, 55.9%; 0; 802; 32.467, 18.495 11990; 389265, 389161, 221748, 217459, 57.0%, 55.9%; 0; 802; 32.457, 18.494 12000; 389273, 389246, 221828, 217539, 57.0%, 55.9%; 0; 802; 32.437, 18.486 12010; 389282, 389264, 221844, 217555, 57.0%, 55.9%; 0; 802; 32.412, 18.472 12020; 389286, 389270, 221847, 217558, 57.0%, 55.9%; 0; 802; 32.385, 18.456 12030; 389287, 389275, 221851, 217562, 57.0%, 55.9%; 0; 802; 32.359, 18.441 12040; 389288, 389282, 221857, 217568, 57.0%, 55.9%; 0; 802; 32.332, 18.427 12050; 389288, 389284, 221859, 217570, 57.0%, 55.9%; 0; 802; 32.306, 18.412 12060; 389288, 389287, 221862, 217573, 57.0%, 55.9%; 0; 802; 32.279, 18.397 12070; 389288, 389288, 221863, 217574, 57.0%, 55.9%; 0; 802; 32.253, 18.381 12080; 389288, 389288, 221863, 217574, 57.0%, 55.9%; 0; 802; 32.226, 18.366 12090; 389288, 389288, 221863, 217574, 57.0%, 55.9%; 0; 802; 32.199, 18.351
このように、接続に成功するノードの割合は最初のうちは 70 % 程度あるものが、後に減少していた。この原因についてはまだ検討していない。接続に成功しないノードは、「Port0」モードで稼動しているもの(もしくは「Port0」に設定していないがNATルータのポートフォワード設定ができていないもの)と推定できる。
5番目の列は、接続に完了してそこから他のノード情報を取得できたノード数を示しており、接続したノードの 98 % から取得ができたことがわかる。今回は、全体を巡回することを優先して接続を早期に切断しているが、接続を継続することによってこの割合は100 % に近づくと思われる。
今回の巡回(日曜日の昼に実施)で見つかった 38万9288ノードという数は、ネットエージェント社が発表した「平日で44万〜49万、土日だと50万〜53万以上」という値よりいささか少ない。同社の7月3日の発表でも、
2006年7月1日(土)のWinnyノード数は471449、2006年7月2日(日)のWinnyノード数が481359であることが当社Winny調査システムにより観測されました。
となっている。今回のプログラムが全部のノードに到達することができなかった可能性と、7月中旬で利用者が減少している可能性が考えられる。
全部のノードに到達しない可能性の原因として、Winnyネットワークの「クラスタ化」動作により、いくつかのWinnyノードクラスタに到達できなかったということが考えられる。しかし、Winnyの「クラスタ化」は、ユーザにより指定された3個以下のキーワードの類似性により構成されるものであるため、「A, B」を指定しているユーザと「A, C」を指定しているユーザがいれば、Aのクラスタを経由してBのそれとCのそれはつながっていることになること、また、クラスタ分けは緩やかに構成されているものであることから、完全に分離独立したクラスタというものは存在しないと考えられるし、非常に疎に結合したクラスタの存在も考えにくい。
図1のグラフで、発見ノード数が急激に増加する現象を示すところが2箇所の時刻でみられるが、これは、新たなクラスタが見つかったというタイミングを示しているように見える。今回のアルゴリズムは、ノード情報の新しいものを優先しており、ほぼLIFO(スタック)の順序で実行している*2ため、先に同じクラスタのノードを探し切った後に別のクラスタのノードを探すという傾向の動作をしていることになる。ノードが飽和して新しいノードがほとんど見つからなくなると、古いノード情報を使い始め、古いものをいくらか使っているうちにこのような急激な新規ノード発見増加の現象が生じたようである。
今回の実験では 57 % のノードにしか接続していないのだから、接続できなかった残りのノードに接続していれば到達していたクラスタの存在というものも考えられるが、その可能性は低いように思われる。たしかに、急激にノード数が増加する現象がただ1つのノード(もしくはごくわずか)によってのみ引き起こし得るものだと仮定すれば、そのような可能性も信憑性が出てくるが、これは、最初にそのようなノードが処理された時点で、先にそこから辿られる新しいクラスタのノードを優先して処理することになるため、急激な形で見えているだけで、スタックのすぐ奥には他にも同様のノード(新しいクラスタへリンクしているノード)が多数存在していると考えるのが自然と思われる。そうすると、半分程度のノードを調べないことによって到達できないクラスタが生ずるという可能性は、きわめて小さいのではないか。
今後、優先度の評価方法を変更して実験をすることにより、違うパターンの順序でのノード巡回を試してみたい。
package jp.takagi_hiromitsu.winny.crawler;
import java.io.*;
import java.net.*;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
import jp.takagi_hiromitsu.winny.net.WinnyProtocolInterpreter;
import jp.takagi_hiromitsu.winny.net.WinnyProtocolException;
import jp.takagi_hiromitsu.winny.net.WinnyCommandVisitor;
import jp.takagi_hiromitsu.winny.net.WinnyProtocolCommander;
import jp.takagi_hiromitsu.winny.net.Command3;
import jp.takagi_hiromitsu.winny.net.Command4;
import jp.takagi_hiromitsu.winny.net.Command13;
import jp.takagi_hiromitsu.winny.net.Command10;
import jp.takagi_hiromitsu.winny.Node;
import jp.takagi_hiromitsu.winny.WinnyKey;
import jp.takagi_hiromitsu.winny.WinnyFileID;
import jp.takagi_hiromitsu.winny.WinnyConfiguration;
public class WinnyWalker {
Set<Node> foundNodes = new HashSet<Node>();
final int startTime = currentTime();
int connectionTriedCount = 0;
int handshakeSucceededCount = 0;
int extractionSucceededCount = 0;
static final int MAX_THREADS = 800;
ThreadPoolExecutor threadPool;
final Timer timer = new Timer(true);
WinnyConfiguration conf = new WinnyConfiguration();
WinnyWalker(Node initialNode) {
threadPool = new ThreadPoolExecutor(
MAX_THREADS, MAX_THREADS, 0L, TimeUnit.SECONDS,
new PriorityBlockingQueue<Runnable>()
);
timer.scheduleAtFixedRate(new MonitorTask(), 0L, 10 * 1000L);
addNode(initialNode, 0);
}
boolean addNode(Node node, int priority) {
if (!node.isValid()) return false;
boolean added = foundNodes.add(node);
if (!added) return false;
int elapsed = currentTime() - startTime;
threadPool.execute(new Task(node, priority - elapsed));
return true;
}
class Task implements Runnable, Comparable<Task> {
Node target;
int priority;
int taskSubmittedTime;
int taskStartTime;
Task(Node target, int priority) {
this.target = target;
this.priority = priority;
taskSubmittedTime = currentTime();
}
public int compareTo(Task another) {
if (this.priority < another.priority) return -1;
if (this.priority == another.priority) return 0;
return 1;
}
int handshakeCount = 0;
int extractedNodeCount = 0;
int newNodeCount = 0;
Exception lastException;
String[] clusterWords;
public void run() {
taskStartTime = currentTime();
int errorCount = 0;
for (int i = 0; i < 4; i++) {
if (errorCount >= 1) break;
if (currentTime() - taskStartTime >= 20) break;
try {
connect(30);
} catch (IOException e) {
errorCount++;
}
}
connectionTriedCount++;
if (handshakeCount > 0) {
handshakeSucceededCount++;
}
if (extractedNodeCount > 0) {
extractionSucceededCount++;
}
printTasklog();
}
private void connect(int timeLimit) throws IOException {
InputStream is = null;
OutputStream os = null;
Socket s = null;
TimerTask closer = null;
try {
s = new Socket(target.addr, target.port);
closer = new SocketCloser(s);
timer.schedule(closer, timeLimit * 1000L);
is = new BufferedInputStream(s.getInputStream());
os = new BufferedOutputStream(s.getOutputStream());
new WinnyProtocolCommander(os, conf).sendCommand(new Command10());
WinnyCommandVisitor visitor = new NodeAddingVisitor();
new WinnyProtocolInterpreter(is, visitor).interpret();
} catch (WinnyProtocolException e) {
lastException = e;
} catch (IOException e) {
lastException = e;
throw e;
} finally {
if (closer != null) closer.cancel();
try {
if (os != null) os.close();
if (is != null) is.close();
if (s != null) s.close();
} catch (IOException e2) {
}
}
}
class NodeAddingVisitor extends WinnyCommandVisitor {
public void visit(Command3 c) {
handshakeCount++;
clusterWords = c.clusterWords;
}
public void visit(Command4 c) {
extractedNodeCount++;
boolean added = addNode(new Node(c.addr, c.port), 0);
if (added) newNodeCount++;
}
public void visit(Command13 c) {
for (int i = 1; i < c.nodePath.length; i++) {
Node n = c.nodePath[i];
extractedNodeCount++;
boolean added = addNode(n, 0);
if (added) newNodeCount++;
}
Map<Node,Integer> minimum = new HashMap<Node,Integer>();
for (WinnyKey k: c.keys) {
dumpKey(target, k);
Node node;
if (k.addr.isSiteLocalAddress() || k.addr.isLinkLocalAddress()) {
node = target;
} else {
node = new Node(k.addr, k.port);
}
int pri = 1500 - k.keyExpirationTimer;
if (pri < 0) pri = 0;
Integer old = minimum.get(node);
if (old == null || old > pri) {
minimum.put(node, pri);
}
}
for (Node n: minimum.keySet()) {
extractedNodeCount++;
boolean added = addNode(n, minimum.get(n));
if (added) newNodeCount++;
}
}
}
class SocketCloser extends TimerTask {
Socket socket;
int lastExtractedNodeCount = 0;
SocketCloser(Socket socket) {
this.socket = socket;
}
public void run() {
try {
socket.getInputStream().close();
} catch (IOException e) {
}
try {
socket.close();
} catch (IOException e) {
}
}
}
private void printTasklog() {
taskOut.printf(
"%21s pri: %4d life: %3d con: %d new: %5.1f%% (%4d/%4d) %d %d %s %s\n",
target.toString(),
priority + (taskStartTime - startTime),
currentTime() - taskStartTime,
handshakeCount,
(float)newNodeCount / extractedNodeCount * 100,
newNodeCount,
extractedNodeCount,
currentTime() - startTime,
currentTime(),
clusterWords == null ?
"" :
clusterWords[0] + "|" + clusterWords[1] + "|" + clusterWords[2],
lastException == null ? "" : lastException
);
taskOut.flush();
}
}
class MonitorTask extends TimerTask {
public void run() {
int elapsed = currentTime() - startTime;
monitorOut.printf(
// "Elapsed time: %7d; Nodes found: %6d, tried: %6d, " +
// "connected: %6d, succeeded: %6d, live: %5.1f%%, " +
// "extracted: %5.1f; %Queue lingth: %6d; Threads: %4d;" +
// " /sec tried: %5.3f, connected: %5.3f\n",
"%7d; %6d, %6d, %6d, %6d, %5.1f%%, %5.1f%%; %6d; %4d; %5.3f, %5.3f\n",
elapsed,
foundNodes.size(),
connectionTriedCount,
handshakeSucceededCount,
extractionSucceededCount,
(float)handshakeSucceededCount / connectionTriedCount * 100,
(float)extractionSucceededCount / connectionTriedCount * 100,
threadPool.getQueue().size(),
Thread.activeCount(),
(float)connectionTriedCount / elapsed,
(float)handshakeSucceededCount / elapsed
);
monitorOut.flush();
}
}
private void dumpKey(Node node, WinnyKey k) {
keydumpOut.printf(
"%s %s:%d %s %d %d %d %d \"%s\" %d %s\n",
node.toString(),
k.addr.getHostAddress().toString(),
k.port,
k.fileid.toString(),
k.keyExpirationTimer,
currentTime(),
k.keyUpdateTime,
k.referenceCount,
k.trip,
k.filesize,
k.filename
);
}
private static int currentTime() {
return (int)(System.currentTimeMillis() / 1000);
}
static final String usage = "Usage: java WinnyWalker host port [outputdir]";
static PrintStream keydumpOut, taskOut, monitorOut;
public static void main(String[] args) throws Exception {
if (args.length < 2 || args.length > 3) {
System.err.println(usage);
System.exit(1);
}
String host = args[0];
int port = Integer.parseInt(args[1]);
Node initialNode = new Node(InetAddress.getByName(host), port);
File dir = new File(".");
if (args.length > 2) {
dir = new File(args[2]);
}
keydumpOut = newPrintStream(dir, "keydump.log");
taskOut = newPrintStream(dir, "task.log");
monitorOut = newPrintStream(dir, "monitor.log");
new WinnyWalker(initialNode);
}
static PrintStream newPrintStream(File dir, String file) throws Exception {
OutputStream os = new FileOutputStream(new File(dir, file));
return new PrintStream(new BufferedOutputStream(os));
}
}
上記には誤りがあり、18日の日記で訂正した。
昨日の続き。昨日の実験で見つかった「38万9288ノード」というのはそれで全部ではなかったことがわかった。 その後、同じ条件のままもう一回、そしてスタートノードだけを変えて2回実験したところ、発見できたノード数は以下の表のとおりになった。
開始時刻 | 経過時間 | ノード数 |
---|---|---|
17日(祝日)午前4時40分 | 3時間22分 | 388,994 |
17日(祝日)午前8時45分 | 2時間56分 | 343,001 |
17日(祝日)午前11時51分 | 5時間02分 | 581,856 |
同じスタートノードから実行した場合、ほとんど同じ結果になった。グラフにすると、発見ノードが急増する特徴も同じように現れ、そのタイミングがわずかに前後しているものの、同じように10万弱と、20万弱のノードが見つかったタイミングで起きていた。この現象が起きる原因やタイミングは、予想と異なり、偶然に左右されにくいのかもしれない。
次に、無作為に選んだノードにスタートノードを変更して実験したところ、34万ノードしか巡回できなかった。発見ノードが急増するタイミングはほぼ同じだった。急に4万ノードがシャットダウンしたとは考えにくく、なぜこうなったのかわからない。
さらにもう一度、無作為に選んだノードにスタートノードを変更して実験したところ、今度は図1の結果となり、58万ノードに達した。同様に 10万弱と20万弱ノードあたりで急増現象が見られ(ただしタイミングが早かった)、3時間が経過した時点では、前回同様に 3時間20分あたりで 39万ノードで終わるように見えた。しかし、予想された終了時刻の直前になって発見ノードが急増する現象が起き、58万ノードが見つかった。
このような急増現象がいつ起きるかわからないので、「これでほぼ全部」と言えそうにない。このケースでも、5時間経過時点でノード数は十分にサチっておらず、単にノードの消費(緑のプロット)が速過ぎたから終わっただけと言うべきのようだ。
現在のアルゴリズムでは接続を30秒で打ち切るようにしているが、これをもっと長くすると、得られるノード数が増加し、ノードの消費速度は遅くなるので、緑の曲線が赤の曲線に追いつきにくくなると考えられ、より確かに全体を巡回しやすくなるように思われる。しかし、全体の実行時間が長くなるため、稼動ノード数のスナップショットとしては不正確さが増してしまう(もし24時間かかるようであれば、午前と午後の比較ができない)。
今後の方向性として次が考えられる。
上記には誤りがあり、18日の日記で訂正した。
ギャー! やってしまった orz。16日の日記掲載のプログラムはバグっていた。java.util の新しい方のコレクションクラスは synchronized じゃないのを思い出し、どのくらい影響があったか task.log からノードの重複を除いて計数したところ、相当な数のノードがダブっていたもよう。
以下の通り訂正。実行時間やグラフ形状がどう変化するかはまだ不明。
16日の日記の実験結果ノード数
誤: 389,288 → 正: 200,036
17日の日記の実験結果1つ目のノード数
誤: 388,994 → 正: 201,619
17日の日記の実験結果2つ目のノード数
誤: 343,001 → 正: 199,316
17日の日記の実験結果3つ目のノード数
誤: 581,856 → 正: 224,453
この結果はネットエージェント7月3日発表のノード数よりかなり少ない。うむむ。
16日の日記のプログラムの訂正:
誤:
Set<Node> foundNodes = new HashSet<Node>();正:
Set<Node> foundNodes = java.util.Collections.synchronizedSet(new HashSet<Node>());
ギャー、ここも杜撰だった。(モニタリング用のコードとはいえ。)
int connectionTriedCount = 0;
int handshakeSucceededCount = 0;
int extractionSucceededCount = 0;
connectionTriedCount++;
handshakeSucceededCount++;
extractionSucceededCount++;
java.util.concurrent.atomic.AtomicInteger を使ってみよう。
import java.util.concurrent.atomic.AtomicInteger;
...
AtomicInteger connectionTriedCount = new AtomicInteger(0);
AtomicInteger handshakeSucceededCount = new AtomicInteger(0);
AtomicInteger extractionSucceededCount = new AtomicInteger(0);
...
connectionTriedCount.getAndIncrement();
handshakeSucceededCount.getAndIncrement();
extractionSucceededCount.getAndIncrement();
こんどの金曜日は、Webアプリケーションセキュリティフォーラムの第4回コンファレンス。
日時: 2006年7月28日(金) 10:30〜17:30 受付開始: 10:00
会場: 丸の内コンファレンススクエアM+ 10階 グランド
今回は、三井住友銀行のネットバンキンググループで「簡単!やさしいセキュリティ教室」の作成にも携われた山口賢二様をお招きし、同行における公式メールへのS/MIMEの導入と運用についてご講演いただけることになった。
三井住友銀行は、2004年から口座の入出金をメールで知らせるサービスを提供しており、PayPalなどと同様に Phishingメールと公式メールが明確に区別できることが重要となっていたところ、今年5月から、公式メールにS/MIME署名を付ける対策が実施されていた。
一般に、こうした比較的新しい試みは導入時後の顧客サポートなどでコストがかかるのではないかという声をしばしば耳にするが、今回のご講演では、実際に運用を始めてどんなトラブルが発生したか、顧客からどんな反応があったかなどについてお話しいただけることになった。他では得難い興味深いお話となると思う。
またその他、マイクロソフト様より、Internet Explorer 7における新たなセキュリティ機能についてのご講演もいただけることになっている。
13:20〜14:20 『Internet Explorerでのセキュリティ 』
井戸 文彦 マイクロソフト株式会社 エバンジェリスト14:20〜15:10 『公式メールへのS/MIMEの導入と運用』
山口 賢二 株式会社三井住友銀行マスリテール事業部ネットバンキンググループ
S/MIME署名されたメールをS/MIMEに対応していないMUA(電子メール利用ソフト)で表示すると、「smime.p7s」という添付ファイル付きの形で表示される(図1)。
これは、RFC 2633にあるように、S/MIMEが、署名データをMIMEの添付ファイルの形式で付加するように設計されているからだ。
3.4.3.3 Sample multipart/signed Message
Content-Type: multipart/signed;
protocol="application/pkcs7-signature";
micalg=sha1; boundary=boundary42
--boundary42
Content-Type: text/plain
This is a clear-signed message.
--boundary42
Content-Type: application/pkcs7-signature; name=smime.p7s
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=smime.p7s
ghyHhHUujhJhjH77n8HHGTrfvbnj756tbB9HG4VQpfyF467GhIGfHfYT6
4VQpfyF467GhIGfHfYT6jH77n8HHGghyHhHUujhJh756tbB9HGTrfvbnj
n8HHGTrfvhJhjH776tbB9HG4VQbnj7567GhIGfHfYT6ghyHhHUujpfyF4
7GhIGfHfYT64VQbnj756
--boundary42--
この添付ファイルをダブルクリックで開くとどうなるか。Windowsでは証明書ビューアが起動する(図2)。
ここで中身を見ていくと署名者の証明書が見つかる。ダブルクリックすると証明書が表示される(図3)。
ここで、証明書が正当なものと確認できたからといって、S/MIME署名メールが正当なものと判定できたと誤解してはいけない。
これは単に署名に用いられた証明書を表示しただけであり、元々証明書自体は誰にでもコピーできるものだ。たとえば私が、三井住友銀行から送られてきたメールに添付されている「smine.p7s」ファイルを保存して、自分のメールに添付して誰かに送信すると、受け取った人は上の手順でダブルクリックしていくと、正当な三井住友銀行の証明書を目にすることになるだろう*1。
三井住友銀行のサイトにある「メール受信用ソフト毎の確認手順」の説明でも、添付ファイルを開く方法で確認してはいけない旨の注意書きがなされている(図4)。
このような誤解されかねない挙動をするのは、Windowsのデフォルトの拡張子設定によるものだ*2。図5のように、「.p7s」に「Crypto Shell Extensions」が関連付けされている。
署名データは署名対象の本文とセットになったときのみ意味を持つのだから、署名データ(.p7s)単体で何らかの処理ができるようになっている Windowsの設計が不適切だ*3。何にも関連付けないのが正しいのではないか。
内閣官房情報セキュリティセンター発行 のメールマガジンは、創刊当初からS/MIME署名されている。
○ 当センターからのメールは電子証明書(署名者:NISC Information Systems Officer、メールアドレス:nisc-news@bits.go.jp)によって署名されています。電子証明書のないメールや、異なる署名情報が付いているメールは当センターからのものではありませんので、くれぐれもご注意下さい。
○ 当センターからのメールにはファイルは一切添付されておりませんが、お使いのメールソフトによっては電子証明書が添付ファイル(smime.p7s)として表示される場合があります。*4
内閣官房情報セキュリティセンター NISC NEWS 創刊号
しかし、使用されている証明書の有効期限が2か月と短い。5月18日に送られてきた第2号の署名の有効期限は6月20日(図6)で、1か月間しか読むことができなかった*5。
「まあ、最初のうちだけ取り急ぎかな?」と思っていたところ、第3号の冒頭で、
○ 当センターのドメインは6月1日を以て bits.go.jp から nisc.go.jp に変更になりました。
内閣官房情報セキュリティセンター NISC NEWS 第3号
と書かれていた*6。「なるほど、ドメイン変更が予定されていたから臨時だったのか」と一瞬思ったが、第3号のS/MIME署名メールも、ふたたび2か月期限の証明書になっていた(図7)。
もしかして、VeriSign の free 60-day trial edition を常用するつもりなのだろうか?
むろん、オレオレ認証局を安直に入れさせて憚らないどこぞの省より十万倍ましだというのは、もはや説明するまでもなく理解されるところだ。
*1 Outlook ExpressなどのS/MIME対応MUAで受信した場合でも、S/MIME形式でない方法で単にファイル「smime.p7s」が添付されたメールを受信した場合、改竄されているとの警告は出ない。(署名されているとのマークも出ないが。)したがって、正しいS/MIME署名メールで「smime.p7s」の添付ファイルが見えないようにうなっているMUA(Outlook Expressなど)では、「smime.p7s」の添付ファイルが見えるときは異常だと理解しなくてはならない。(混乱を避けるため、Outlook Expressしか使わないユーザは、秀丸やBecky!でS/MIME署名メールがどうなっているか、知らないでいるほうがよい。)
*2 秀丸メールについても、せっかくS/MIME対応しているのだから、smime.p7sが添付ファイルとして見えないように工夫した方がよいとは言える。Becky!のS/MIMEプラグインもそのようにした方がよい。
*3 仮にこれを「脆弱性」としてMicrosoft社に通告しても同社は脆弱性とは認めないだろうから、ここに書いて利用者に対する注意喚起としつつ、同社の知り合いには意見しておくことにする。
*4 添付されているのは証明書ではなく署名なのだが。たしかに証明書も含まれてはいるが、Windowsでダブルクリックしたときの挙動(証明書だけが表示される)に惑わされていないか?
*5 警告を無視して読むことはできる。
*6 普通ならここで、「ドメイン変更を自称するこのメール自体が偽だったら? 前号で予告もされてなかったしな。」と疑うべきところだが、go.jp ドメインなので、まあ疑う必要はないといったところか。(でも、go.jpドメインの管理ってどうなってるの?)
JPNIC認証局なるものが実験運用されているようだ。利用対象者は「IPアドレス管理指定事業者」に限定されているため、物理的に配布されている「JPNIC認証局証明書の入手と確認の手順」に従うことになっているらしい。「誤った認証局証明書の組み込みは、Webサーバのなりすましの原因になります」との警告もされている。
それでもなお、fingerprintがWebにも掲示されている*1。ただし、https://serv.nic.ad.jp/capub/fingerprint.html という https のページに。そして、そのサーバのサーバ証明書は VeriSign発行のものになっている。そこまではよい。
だが、その証明書が7月20日で期限切れだ。
こうした事故はしばしば見かけるが、どうやって防止するのがよいのだろう?
*1 MD5はもうやめたほうがいいと思うが。(LGPKIは昨年12月にMD5を廃止している。)