最新 追記

高木浩光@自宅の日記

目次 はじめに 連絡先:blog@takagi-hiromitsu.jp
訪問者数 本日: 241   昨日: 1144

2006年07月01日

簡易Webブラウザに winnytp:// プロトコルハンドラを組み込んでみた

javax.swing.JEditorPaneを使って簡易ブラウザを作った。戻るボタンとリロードボタンは付けたが、まだフォームの処理や text/html 以外のContent-Typeの処理がない。

これに、先週の日記「Java用「winnytp://」プロトコルハンドラを作ってみたら簡単にできた」で作ったハンドラファクトリをセットしてみたところ、そのまま動いた。

図1: コマンド4とコマンド13の受信から他のサイトへのリンク集を表示した様子

図2: コマンド13の受信からこのサイトが自ら「このファイルを送信可能です」と主張しているものを表示した様子

ファイルのリンクをクリックするとそのサイトからそれをダウンロードする仕掛けになっている。しかし、プロトコルハンドラがダウンロード機能に対応していない。

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ブラウザの例


2006年07月02日

ウイルス駆除のためWinnyのCacheフォルダを仮想ドライブ化してはどうか

アンチウイルスベンダーがWinnyのCacheフォルダ内のウイルスを駆除しない理由」に書いたように、Winnyを媒介するウイルスの勢いが通常に比べて衰えにくい原因として、アンチウイルスソフトが「出て行く」ファイルに対する検疫を行っていないことがあるが、ならば、WinnyのCacheフォルダの中身をWindowsの「仮想ドライブ」としてマウントするソフトウェアを作って配布してはどうだろうか。

そうすれば、一般的なアンチウイルスソフトを用いてそのドライブにウイルススキャンをかけることで、Cacheフォルダ内のウイルス駆除ができるはずだ。その仮想ドライブでは、ファイル一覧の操作が復号されたファイル名でリストされ、ファイルの読み出し操作が復号しながらの読み出しとなり、削除操作が対応するファイルの削除となるようサポートされていればよい*1。(書き込み機能はいらない。)

これは結果として、「自分が何をやっているか(やらされているか)を知りなさい」ということも同時に達成できるわけで、一石二鳥だ。「キャッシュと嘘とファイル放流」に書いていたように、「Cacheフォルダ」の中身がそのままでは見えないようにあえて作りこまれていることがWinnyの不健全性の根源なのだから、それを取り除くのにちょうどよい。

*1 ついでに、「WinnyのDownフォルダをインターネットゾーンにする」のように、仮想ドライブ内のそれぞれのファイルにZoneIdが付くようにするとモアベターだ。


2006年07月03日

我孫子市のウイルスメール配信事故で報道各社がそろって誤報

報道各社は、我孫子市の防災・防犯情報メール配信サービスでウイルス入りメールが配信された事故について、「何者かが外部からサーバーに不正侵入した」などと報道した。

  • 我孫子市から2583人にウイルスメール 外部から侵入, 朝日新聞, 2006年7月2日

    千葉県我孫子市は、防災・防犯情報をメール配信しているサーバーシステムが外部から不正に侵入され、市民2583人にウイルスメールが送信されたと2日発表した。(略)我孫子署に報告し、被害届の提出を検討している。

    (略)

    市は直ちに配信サービスを停止して調査。その結果、何者かが外部からサーバーに不正侵入し、市のメールアドレスを使ってウイルスメールを送信したと判断した。(略)

  • 我孫子市役所からウイルスメール, 日刊スポーツ, 2006年7月2日

    市によると、何者かが市役所のシステムに侵入。システムが操作され1日午前9時20分ごろ、ウイルス感染メールが一斉に配信されたという。

  • 我孫子市の防犯メール登録者に ウイルス添付メール, 中日新聞, 2006年7月3日

    市によると、一日午前九時二十分ごろ、何者かが外部から市のメール配信システムに侵入し、市のサーバーからウイルスを添付した偽メールを配信したという。

  • 市役所から防災・防犯情報を伝えるメールにウイルス, ZAKZAK, 2006年7月3日

    市によると、何者かが市役所のメールアドレスを使ってシステムに侵入。システムが操作され1日午前9時20分ごろ、ウイルス感染メールが一斉に配信されたという。

  • 我孫子市のアドレスから2,583人にウイルスメールが送信, INTERNET Watch, 2006年7月3日

    我孫子市では1日、メール配信サービスを停止して調査を実施。その結果、メール配信サーバーが外部から不正侵入を受けたことが判明した。

本当に侵入があったのか疑わしいので、我孫子市役所に電話して聞いてみた。広報室に電話すると、詳しいことはわからないとのことで、情報システム課につないで頂いた。情報システム課の担当の方にはたいへん丁寧に対応していただいた。そのやりとりはおおむね次のような感じの内容になった。


私: システムへの侵入はなかったのではないかと思うのですが、いかがでしょうか。

情報システム課: はい、システムへの侵入はありませんでした。

私: 以前からよくある事故と同じですよね? つまり、メーリングリストを用いた配信システムになっていて、そこにメールが流れただけと。つまり、あるメールアドレスに送信すれば登録会員全員にメールが送信される仕組みになっていて、誰でもそのメールアドレスにメールを送信できる状態になっていたという。

情: そのようです。

私: しかし、報道ではシステムへの侵入があったと伝えられています。これはよくないことと思うのですが、いかがでしょうか。

情: そうですね。私どももそのように発表したつもりはないのです。侵入されて情報を盗まれたということはないと伝えています。どうも新聞記者の方は、侵入があったということにしたいようで、面白いように書きたいということがあるんじゃないでしょうかね。

私: しかし、さきほどこの電話で最初にお話しした広報室の方は、「システムへの侵入があったのですか?」と聞くと、あったとお答えになりましたよ?

情: うーんやはり、広報の者はITに詳しくないものですから、言葉の使い方とか至らないところがあるのかなとおもいます。

私: 「千葉県警に被害を届ける方針」という報道もありますが、どのような被害を主張なさるのでしょうか。意図的に投げ込まれたのではなく、ウイルスによってランダムに送信されているメールがたまたま到着しただけですよね?

情: たまたまかどうかはまだはっきりしていませんが、被害を届けるかどうかも含めてまだ調べているところです。

(以下略)


市の広報がそのように発表したから、それをそのまま伝えざるを得ないという、報道機関の都合も理解できるけれど、このパターンの事故はしょっちゅう起きていて、侵入があったなんてためしはないのだから、報道機関は、ちゃんと事実かどうか疑ってかかって取材をしたうえで、報道してほしい。せめて、INTERNET Watchくらいは。

後日追記

INTERNET Watchの当該記事が訂正された。

記事初出時、今回のウイルスメールが配信された原因について、「メール配信サーバーが外部から不正侵入を受けた」ためとしていました。しかし、 4日に我孫子市に再取材したところ、メール配信サービスで利用していたMLにウイルスメールが配信されたことが原因であることがわかりましたので、記事を修正しました。

しかし、訂正版の記事でも、「管理者のアドレスを詐称した何者かによって、ウイルスメールが配信されてしまったという」となっていて、意図的な攻撃だということになっている。

Fromアドレス詐称型のワームメールは普通、ランダムにメールアドレスを(Webブラウザのcacheや過去の送受信メールなどから)選んで無差別に送信しているだけなのだが、それによって起きた事故*1を指して、「管理者のアドレスを詐称した何者かによって」などと言ってよいのだろうか? 我孫子市がまだそう主張しているということなのだろうが、もっと懐疑的に書けないものだろうか。せめて、INTERNET Watchくらいは。

一方、こんな反応もあった。

本件、システム侵入を受けたという報道があります*1が、そのような事実は無いとのことです。すなわち、「内部に存在するウイルス感染端末から、誤ってウイルス付きメールが配信されてしまった」という可能性があるようですね。

やまにょん@サーバー管理者日誌, 2006.7.4

こういう誤解をする人がけっこういるようだ。他にも、トラックバックを頂いたところのひとつも別の事故について、

福井県観光振興課の課員は、仕事中にエロサイト巡回したり 出会い系に投稿したりして、ヘンなウィルス拾ったんじゃないの?

Birth of Blues, 2005年02月26日

と同様の誤解をしている。

おそらく我孫子市はこの誤解をされることを最も嫌ったために、「外部から」ということを強調し、「外部から」という事態を素人記者に理解させるために、「不正に侵入」という表現を使ってしまったのではないか。

勝手に憶測するとこんな記者会見の風景が見えてくる。


記者: 市のメールマガジンの複数の購読者がウイルスを受信したと言っています。全員に送られているのでは?

市: はい、○千○百人の購読者の全員に送られたようです。

記者: 登録者の個人情報が漏れているということではないですか?

市: それはありません。市のシステムから送信されたものです。

記者: 市役所の端末がウイルスに感染して、ウイルスをばら撒いたということですか?

市: いえ違います。外部から不正に送られたものです。

記者: 不正侵入があったということですか?

市: ……。まあそんなところです。

記者: 不正侵入で個人情報を盗まれていませんか?

市: それはありません。


事実が何かということよりも、「ということにしたい」願望が当事者とマスコミにあって、その表れではないか。

事実候補愚かな当事者マスゴミまともな当事者まともな報道機関
個人情報が漏れてそれが使われたそれは避けたい、他の原因を探す大スクープその可能性がないか念のため調査この事象は普通それが原因じゃないよな
不正侵入がありシステムを操作された違うけどそれでもいいや(↑よりはまし)(↓はどうせマスコミは理解しない)中スクープその可能性がないか念のため調査この事象は普通それが原因じゃないよな
MLに意図的にウイルスが投げ込まれたそれだ、俺たち被害者小スクープ(または理解不能)その可能性がないか念のため調査その可能性もあるがそれは重要ではなく、配信システムの設定の不備がまだあるようだとして注意喚起
購読者が感染したためウイルスがMLに到着して流れたその可能性に想像が及ばないネタにならない(または理解不能)普通はこれが原因、だけど「購読者が感染したため」は重要ではないし、記者には言わないほうがいい配信システムの設定の不備がまだあるようだとして注意喚起、「購読者が感染したため」は重要でないので略
第三者からたまたまウイルスがMLに到着して流れたその可能性に想像が及ばないネタにならない(または理解不能)↑の可能性もあるがこれが原因ということでよい配信システムの設定の不備がまだあるようだとして注意喚起

*1 もちろん、意図的に送信される可能性がないわけではないので、その可能性を疑うことも重要ではあるが。


2006年07月08日

winnytp:// ハンドラに「キー消滅判定タイマー」値の表示を付けてみた

1日のプロトコルハンドラを改良して、ファイルごとに「キー消滅判定タイマー」の値を表示するようにしてみた。図1のように、ファイル名の左側にその値を表示し、値が1500以上のものについてリンク部分を強調表示するようにした。

図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: 他のホストが送信可能と主張するファイルをも含めて全部を表示した場合

図2では、ほとんどのファイルでタイマー値が1500未満となっている。何箇所かにある1500以上のファイル(強調表示)のリンクにマウスポインタを載せて、リンク先のURLをステータスバーで確かめてみると、そのほとんど*3がアドレスバーのホストと一致していた。つまり、他のホストにあるファイルなのに1500を超えるということはほとんどないと言えるのかもしれない。

次に、図1と同様の表示を別のホストのURLについて表示させたところ、ファイルリストの全部についてタイマ値が1500未満となるところが何箇所かあった(図3)。そうしたサイトでは表示されるファイル数が極端に少ないという傾向があるように思えた。これは、cacheファイルが削除されているノードだということを意味しているのだろうか。

図3: 当該ホストが送信可能と主張するファイルの全部でタイマー値が1500未満となった例

*1 当該ホストに接続したときに当該ホストが送ってくるコマンド13の内容から、他のホストについてのエントリを除外して表示したもの。

*2 いわゆる「Winnyの中継」を実現する仕組み。

*3 何度かリロードしているうちに、稀に例外があり、他のホストのファイルなのに1500を超えているものもあった。詳しくはまだ調べていない。


2006年07月16日

Winny稼動コンピュータ数調査の追試をしてみた

Winnyネットワークの規模(同時稼動ノード数)の推計については、古くは3年前のネットアーク社の松本氏によるもの、そしてネットエージェント社の杉浦氏によるもの、さらに最近ではeEye Digital Security社の鵜飼氏らによるものがある。

  • ネットアーク、P2Pノードの自動探索システムを稼働, INTERNET Watch, 2003年7月14日

    ネットアークは14日、P2Pノードの自動探索システム「P2P FINDER」を稼働開始したと発表した。WinMXとWinnyの国内ノードが対象となっており、6月3日から7月14日までに20万5,597ノードが発見されたとしている。

  • ネットアーク、8月1日時点でのWinnyとWinMXの利用者数は56万ノード, INTERNET Watch, 2003年8月4日

    株式会社ネットアークは、同社の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接続数増加、ネットエージェントが情報流出調査サービス, INTERNET Watch, 2005年12月26日

    同社がWinny検知システムを開発した当初、2004年5月時点の接続数は27万5,133ノード(ネットエージェント調べ)だった。2005年12月になってもWinnyネットワークには約30万ノードが接続しており、Winny経由の情報流出が相次いでいるにも関わらず、利用者は減少していない状況だ。

  • Winnyのノード数に減少傾向なし、ネットエージェントが調査, INTERNET Watch, 2006年4月25日

    ネットエージェントは25日、P2P型ファイル共有ソフト「Winny」のノード数を発表した。4月10日から23日にかけて独自のWinny検知システムを使って調査したもの。平日で44万〜49万、土日だと50万〜53万以上のノード数を観測したという。

    (略)ネットエージェントでは同社独自の検知システムを11台稼働することで、平日で延べ約350万台のノード情報を取得。IPアドレスなどで重複分を削除してユニークノード数を確定しているという。

  • 「ウイルスの登場は時間の問題、Winnyの使用は今すぐ中止してほしい」〜Winnyの脆弱性を発見した米eEyeの鵜飼裕司氏, INTERNET Watch, 2006年5月25日

    鵜飼氏らは、Winnyのプロトコルや挙動を解析し、Winnyネットワークに参加しているユーザーの一覧を数時間で取得できるシステムも開発した。現在、常時数十万のユーザーがWinnyネットワークに参加していることが観測されており、潜在的には100万人程度のユーザーが存在するのではないかと推測している。

今や、金子勇氏の著書「Winnyの技術」(アスキー)や、日経NETWORK誌の記事「Winnyの通信解読に挑戦!」などによって、Winnyプロトコルの仕様は公知となっており、こうした調査は平均的なプログラマならば誰でも可能な状況にあるといえる。私も、これらの既存の調査の確かさを確認するため、追試を行ってみた。

使用した機材及び環境は以下である。

  • 古いノートPC 1台
    • CPU: Pentium III 900MHz-M
    • メインメモリ: 512MB
  • Windows XP Professional SP1
  • Java Runtime Environment Version 5.0 Update 7
  • 実効最大スループット 7 Mbps 程度の ADSL回線(100BASE-TXで接続) 1本
  • データ記録用LAN接続ハードディスク装置(無線LAN 802.11bで接続) 1台

調査に使用したプログラムのアルゴリズムを図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のハンドシェイクシーケンスを正常に完了)し終えた数である。

図1: 実行結果(開始時刻 2006年7月16日日曜日午前10時)

このように、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));
    }
}
図2: 追試に使用したノード巡回アルゴリズムの説明用コード「WinnyWalker」

訂正

上記には誤りがあり、18日の日記で訂正した。

*1 停止させる機能はまだ作っていない。

*2 FIFOの順序としないのは、それぞれのノードの接続試行開始時刻が、ノード情報発見時刻から数時間後となってしまい、接続成功率が低下するのを避けるため。


2006年07月17日

続・Winny稼動コンピュータ数調査の追試をしてみた

昨日の続き。昨日の実験で見つかった「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万ノードが見つかった。

図1: 実行結果(開始時刻 2006年7月17日月曜日(祝日)午前11時51分)

このような急増現象がいつ起きるかわからないので、「これでほぼ全部」と言えそうにない。このケースでも、5時間経過時点でノード数は十分にサチっておらず、単にノードの消費(緑のプロット)が速過ぎたから終わっただけと言うべきのようだ。

現在のアルゴリズムでは接続を30秒で打ち切るようにしているが、これをもっと長くすると、得られるノード数が増加し、ノードの消費速度は遅くなるので、緑の曲線が赤の曲線に追いつきにくくなると考えられ、より確かに全体を巡回しやすくなるように思われる。しかし、全体の実行時間が長くなるため、稼動ノード数のスナップショットとしては不正確さが増してしまう(もし24時間かかるようであれば、午前と午後の比較ができない)。

今後の方向性として次が考えられる。

  • 一巡して終わりというこれまでの方法ではなく、繰り返し巡回するようにして、複数回にまたがって既知ノードの情報を活用する。過去何日間かに見つかったことのあるノードの全部に対して接続を試行して、そこで得られたノード情報からその回のノード総数を計数する。(これでも、本当に全体を一巡できたとはいえない問題は残る。)

  • 新鮮なノード情報が得られたのにそのノードに接続できなかったという、「Port0」ノードの割合を求め、その値と上の巡回時の接続可能状況から、稼動ノード数を統計的に推定する。

  • 無作為に抽出した1万程度のノードに対して、定期的に接続を試行して、ノードの稼働中、非稼働中の変化を観察して特徴を求め、全体の規模の推定に活用する。

訂正

上記には誤りがあり、18日の日記で訂正した。


2006年07月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();

2006年07月23日

WASFで三井住友銀行におけるS/MIME運用経験のご講演

こんどの金曜日は、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の導入と運用』
山口 賢二 株式会社三井住友銀行マスリテール事業部ネットバンキンググループ


2006年07月24日

Windowsの「.p7s」拡張子に対するデフォルトの関連付けは不適切

S/MIME署名されたメールをS/MIMEに対応していないMUA(電子メール利用ソフト)で表示すると、「smime.p7s」という添付ファイル付きの形で表示される(図1)。

図1: Becky!でS/MIME署名メールを表示した様子

これは、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--

RFC 2633

この添付ファイルをダブルクリックで開くとどうなるか。Windowsでは証明書ビューアが起動する(図2)。

図2: 添付ファイル「smime.p7s」を開いた様子(Windowsの場合)

ここで中身を見ていくと署名者の証明書が見つかる。ダブルクリックすると証明書が表示される(図3)。

図3: 含まれている証明書を表示した様子

ここで、証明書が正当なものと確認できたからといって、S/MIME署名メールが正当なものと判定できたと誤解してはいけない

これは単に署名に用いられた証明書を表示しただけであり、元々証明書自体は誰にでもコピーできるものだ。たとえば私が、三井住友銀行から送られてきたメールに添付されている「smine.p7s」ファイルを保存して、自分のメールに添付して誰かに送信すると、受け取った人は上の手順でダブルクリックしていくと、正当な三井住友銀行の証明書を目にすることになるだろう*1

三井住友銀行のサイトにある「メール受信用ソフト毎の確認手順」の説明でも、添付ファイルを開く方法で確認してはいけない旨の注意書きがなされている(図4)。

図4: 三井住友銀行による注意書き

このような誤解されかねない挙動をするのは、Windowsのデフォルトの拡張子設定によるものだ*2。図5のように、「.p7s」に「Crypto Shell Extensions」が関連付けされている。

図5: Windowsにおける拡張子「.p7s」のデフォルトの関連付け

署名データは署名対象の本文とセットになったときのみ意味を持つのだから、署名データ(.p7s)単体で何らかの処理ができるようになっている Windowsの設計が不適切だ*3。何にも関連付けないのが正しいのではないか。

内閣官房のメルマガのS/MIME証明書がfree 60-day trial editionというセコさ

内閣官房情報セキュリティセンター発行 のメールマガジンは、創刊当初から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

図6: 内閣官房情報セキュリティセンター発行のメルマガの証明書(第2号のもの)

「まあ、最初のうちだけ取り急ぎかな?」と思っていたところ、第3号の冒頭で、

○ 当センターのドメインは6月1日を以て bits.go.jp から nisc.go.jp に変更になりました。

内閣官房情報セキュリティセンター NISC NEWS 第3号

と書かれていた*6。「なるほど、ドメイン変更が予定されていたから臨時だったのか」と一瞬思ったが、第3号のS/MIME署名メールも、ふたたび2か月期限の証明書になっていた(図7)。

図7: 同 第3号のもの

もしかして、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ドメインの管理ってどうなってるの?)


2006年07月25日

サーバ証明書の期限切れ事故を防止するには?

JPNIC認証局なるものが実験運用されているようだ。利用対象者は「IPアドレス管理指定事業者」に限定されているため、物理的に配布されている「JPNIC認証局証明書の入手と確認の手順」に従うことになっているらしい。「誤った認証局証明書の組み込みは、Webサーバのなりすましの原因になります」との警告もされている。

それでもなお、fingerprintがWebにも掲示されている*1。ただし、https://serv.nic.ad.jp/capub/fingerprint.html という https のページに。そして、そのサーバのサーバ証明書は VeriSign発行のものになっている。そこまではよい。

だが、その証明書が7月20日で期限切れだ。

図1: JPNICの運用中のhttpsサーバで証明書が期限切れとなっている様子

こうした事故はしばしば見かけるが、どうやって防止するのがよいのだろう?

  • 担当者が年がら年中、気にかける。

  • 期限がくると自動的に証明書が消滅するようにしておく。

  • 1年で故障するタイマー内蔵のサーバコンピュータを採用する。

  • 最初に見つけた人が意地悪せずこっそり教えてくれるよう徳を積んでおく。

  • 期限切れが近づくと教えてくれる証明書ベンダから証明書を買う。

*1 MD5はもうやめたほうがいいと思うが。(LGPKIは昨年12月にMD5を廃止している。)


最新 追記

最近のタイトル

2000|01|
2003|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|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|05|06|07|08|09|10|11|12|
2012|02|03|04|05|06|07|08|09|
2013|01|02|03|04|05|06|07|
2014|01|04|07|09|11|12|
2015|01|03|06|07|10|11|12|
2016|01|02|03|04|06|07|08|10|11|12|
2017|01|02|03|04|05|06|07|10|12|
2018|03|05|06|10|12|
2019|02|03|05|06|07|08|10|
2020|08|09|
2021|07|08|10|12|
2022|01|04|06|12|
2023|03|
2024|03|04|07|11|12|
2025|01|02|03|04|05|06|10|11|12|
最新 追記